feat(api): 项目任务状态摘要——侧栏状态点 + 需人工关注数
projectOut 此前不返 summary,前端拿到空 {} → 所有项目恒灰、无关注数。新增
store.projectSummary(projectId):running(executor+planner 在跑)/blocked/attention(待审批闸
+needs_attention)/executing/alive 计数,projectOut 带上。前端 adaptProject 用 running 判青点、
attention 进紫徽标。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+7
-7
@@ -26,8 +26,8 @@ function dataDir(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Project 出参:附加 hasTodoJson(<repoPath>/todo/todo.json 是否存在,每次序列化时算) */
|
/** Project 出参:附加 hasTodoJson(<repoPath>/todo/todo.json 是否存在,每次序列化时算) */
|
||||||
function projectOut(p: Project): Project & { hasTodoJson: boolean } {
|
function projectOut(p: Project, store: Store): Project & { hasTodoJson: boolean; summary: ReturnType<Store['projectSummary']> } {
|
||||||
return { ...p, hasTodoJson: hasTodoJson(p.repoPath) };
|
return { ...p, hasTodoJson: hasTodoJson(p.repoPath), summary: store.projectSummary(p.id) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiOptions {
|
export interface ApiOptions {
|
||||||
@@ -59,7 +59,7 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ---------- Projects ----------
|
// ---------- Projects ----------
|
||||||
app.get('/api/projects', () => store.listProjects().map(projectOut));
|
app.get('/api/projects', () => store.listProjects().map((p) => projectOut(p, store)));
|
||||||
|
|
||||||
app.post('/api/projects', (req) => {
|
app.post('/api/projects', (req) => {
|
||||||
const b = req.body as Record<string, unknown>;
|
const b = req.body as Record<string, unknown>;
|
||||||
@@ -83,14 +83,14 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
|||||||
autoApproveExec: Boolean(pick('autoApproveExec', s.autoApproveExec)),
|
autoApproveExec: Boolean(pick('autoApproveExec', s.autoApproveExec)),
|
||||||
budgetUsd: ((): number | null => { const v = pick<number | null>('budgetUsd', s.budgetUsd); return v === null ? null : Number(v); })(),
|
budgetUsd: ((): number | null => { const v = pick<number | null>('budgetUsd', s.budgetUsd); return v === null ? null : Number(v); })(),
|
||||||
budgetPeriod: pick('budgetPeriod', s.budgetPeriod) as 'day' | 'month',
|
budgetPeriod: pick('budgetPeriod', s.budgetPeriod) as 'day' | 'month',
|
||||||
}));
|
}), store);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/projects/:id', (req) => {
|
app.get('/api/projects/:id', (req) => {
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
const p = store.getProject(id);
|
const p = store.getProject(id);
|
||||||
if (!p) throw new StoreError(`项目不存在: ${id}`);
|
if (!p) throw new StoreError(`项目不存在: ${id}`);
|
||||||
return projectOut(p);
|
return projectOut(p, store);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 部分更新项目配置(校验在 Store.patchProject)
|
// 部分更新项目配置(校验在 Store.patchProject)
|
||||||
@@ -113,14 +113,14 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
|||||||
if (b.checks !== undefined) patch.checks = b.checks === null || b.checks === '' ? null : String(b.checks);
|
if (b.checks !== undefined) patch.checks = b.checks === null || b.checks === '' ? null : String(b.checks);
|
||||||
if (b.agentRules !== undefined) patch.agentRules = b.agentRules === null || b.agentRules === '' ? null : String(b.agentRules);
|
if (b.agentRules !== undefined) patch.agentRules = b.agentRules === null || b.agentRules === '' ? null : String(b.agentRules);
|
||||||
if (b.models !== undefined) patch.models = b.models === null || b.models === '' ? null : (b.models as PatchProjectInput['models']);
|
if (b.models !== undefined) patch.models = b.models === null || b.models === '' ? null : (b.models as PatchProjectInput['models']);
|
||||||
return projectOut(store.patchProject(id, patch));
|
return projectOut(store.patchProject(id, patch), store);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 项目重排序(侧栏拖动):body { order: [projectId, ...] }
|
// 项目重排序(侧栏拖动):body { order: [projectId, ...] }
|
||||||
app.post('/api/projects/reorder', (req) => {
|
app.post('/api/projects/reorder', (req) => {
|
||||||
const b = (req.body ?? {}) as { order?: unknown };
|
const b = (req.body ?? {}) as { order?: unknown };
|
||||||
if (!Array.isArray(b.order)) throw new StoreError('order 必须是项目 id 数组');
|
if (!Array.isArray(b.order)) throw new StoreError('order 必须是项目 id 数组');
|
||||||
return store.reorderProjects(b.order.map(String)).map(projectOut);
|
return store.reorderProjects(b.order.map(String)).map((p) => projectOut(p, store));
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- 用户级全局默认配置(per-user,新建项目默认值来源)----------
|
// ---------- 用户级全局默认配置(per-user,新建项目默认值来源)----------
|
||||||
|
|||||||
@@ -218,6 +218,21 @@ export class Store {
|
|||||||
return rows.map(rowToProject);
|
return rows.map(rowToProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 项目任务状态摘要(供侧栏状态点 + 关注数徽标):running=agent 在跑、attention=需人工。 */
|
||||||
|
projectSummary(projectId: string): { executing: number; running: number; blocked: number; alive: number; attention: number } {
|
||||||
|
const rows = this.db.prepare(`SELECT status, COUNT(*) AS n FROM tasks WHERE project_id = ? GROUP BY status`).all(projectId) as Array<{ status: string; n: number }>;
|
||||||
|
const by: Record<string, number> = {};
|
||||||
|
for (const r of rows) by[r.status] = r.n;
|
||||||
|
const g = (s: string): number => by[s] || 0;
|
||||||
|
const executing = g('executing');
|
||||||
|
const running = executing + g('analyzing') + g('speccing'); // agent 在跑:executor + planner
|
||||||
|
const attention = g('plan_review') + g('spec_review') + g('exec_review') + g('needs_attention'); // 待审批闸 + 需人工
|
||||||
|
const TERMINAL = new Set(['done', 'cancelled']);
|
||||||
|
let alive = 0;
|
||||||
|
for (const [s, n] of Object.entries(by)) if (!TERMINAL.has(s)) alive += n;
|
||||||
|
return { executing, running, blocked: g('blocked'), alive, attention };
|
||||||
|
}
|
||||||
|
|
||||||
/** 重排项目(侧栏拖动):按给定 id 顺序写 sort_order;未列出的排在后面、相对顺序不变。 */
|
/** 重排项目(侧栏拖动):按给定 id 顺序写 sort_order;未列出的排在后面、相对顺序不变。 */
|
||||||
reorderProjects(orderedIds: string[]): Project[] {
|
reorderProjects(orderedIds: string[]): Project[] {
|
||||||
const txn = this.db.transaction(() => {
|
const txn = this.db.transaction(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user