feat: 重构项目心跳数据结构并实现项目列表API

- 新增统一项目列表Redis键和迁移工具
- 实现GET /api/projects端点获取项目列表
- 实现POST /api/projects/migrate端点支持数据迁移
- 更新前端ProjectSelector组件使用真实项目数据
- 扩展projectStore状态管理
- 更新相关文档和OpenSpec规范
- 添加测试用例验证新功能
This commit is contained in:
2026-01-13 19:45:05 +08:00
parent 19e65d78dc
commit 282f7268ed
66 changed files with 4378 additions and 456 deletions

View File

@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest';
import request from 'supertest';
import { createApp } from '../app.js';
import { createFakeRedis } from '../test/fakeRedis.js';
describe('projects API', () => {
it('GET /api/projects returns projects from unified list', async () => {
const now = Date.now();
const redis = createFakeRedis({
项目心跳: JSON.stringify([
{ projectName: 'Demo', apiBaseUrl: 'http://localhost:8080', lastActiveAt: now },
]),
});
const app = createApp({ redis });
const resp = await request(app).get('/api/projects');
expect(resp.status).toBe(200);
expect(resp.body.success).toBe(true);
expect(resp.body.count).toBe(1);
expect(resp.body.projects[0]).toMatchObject({
id: 'Demo',
name: 'Demo',
apiBaseUrl: 'http://localhost:8080',
});
expect([ 'online', 'offline', 'unknown' ]).toContain(
resp.body.projects[0].status,
);
});
it('POST /api/projects/migrate migrates old *_项目心跳 keys into 项目心跳 list', async () => {
const now = Date.now();
const redis = createFakeRedis({
'A_项目心跳': JSON.stringify({
apiBaseUrl: 'http://a',
lastActiveAt: now,
}),
});
const app = createApp({ redis });
const resp = await request(app)
.post('/api/projects/migrate')
.send({ dryRun: false, deleteOldKeys: true });
expect(resp.status).toBe(200);
expect(resp.body.success).toBe(true);
expect(resp.body.migrated).toBe(1);
const listRaw = await redis.get('项目心跳');
expect(typeof listRaw).toBe('string');
const list = JSON.parse(listRaw);
expect(Array.isArray(list)).toBe(true);
expect(list[0]).toMatchObject({
projectName: 'A',
apiBaseUrl: 'http://a',
});
const old = await redis.get('A_项目心跳');
expect(old).toBeNull();
});
});