feat: 同步引擎+Agent配置+依赖自动落位+看板大改(预览/md渲染/筛选/徽章组)
后端: - src/sync/todo-sync.ts: todo.json 单向同步引擎(导入=首次同步,幂等,source_ref 映射, subs 复杂度修正为 easy,旧侧 done 历史事实优先 forceDone) - 依赖自动落位:ready 意图按 deps 落位 blocked,依赖全 done 自动放行, daemon 启动 reconcileDeps 对账,手动绕过会弹回 - 新 API: PATCH projects/:id(autonomy/concurrency)、POST :id/sync、GET /api/agents、 PATCH tasks/:id(title/priority/complexity 重置) - daemon 定时同步(MAESTRO_SYNC_INTERVAL 默认 300s) + project.synced 事件 - priority 语义翻转: P0 最高/P1 默认/P2 最低,取值限 0..2,排序/映射/MCP/CLI 全跟进 - 静态服务发 no-cache 头(修浏览器吃旧 CSS/JS) - schema 迁移: tasks.source_ref / projects.last_sync_at(ensureColumn 平滑升级旧库) 看板: - 全屏预览模式(94vh 读完整方案+就地裁决,Esc/遮罩/裁决自动关闭) - 产出 markdown 渲染为 HTML(零依赖渲染器,转义优先) - 任务树筛选(复杂度/状态分组/关键字)+ 顶栏徽章组(待审批/可执行/执行中,hover 展开) - 依赖可视化:详情 DEPS 区块 + 行内⛓等依赖 + 锚点跳转定位 - 按钮收敛:提交评审/编辑产出移除(CC 经 MCP 操作),界面只留用户动作 - Agent 执行面板 + 项目配置(并发/工作模式)+ 同步按钮 测试:21 个全过(新增 sync 幂等/迁移/patch/依赖落位/对账幂等) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+71
-5
@@ -1,9 +1,16 @@
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import { WebSocketServer, type WebSocket } from 'ws';
|
||||
import { Store, StoreError } from '../store/index.js';
|
||||
import { Store, StoreError, type ActiveRun, type PatchProjectInput, type PatchTaskInput } from '../store/index.js';
|
||||
import type { Complexity } from '../model/complexity.js';
|
||||
import { isComplexity } from '../model/complexity.js';
|
||||
import type { TaskStatus } from '../model/status.js';
|
||||
import type { Project, Autonomy } from '../model/types.js';
|
||||
import { syncProject, hasTodoJson } from '../sync/todo-sync.js';
|
||||
|
||||
/** Project 出参:附加 hasTodoJson(<repoPath>/todo/todo.json 是否存在,每次序列化时算) */
|
||||
function projectOut(p: Project): Project & { hasTodoJson: boolean } {
|
||||
return { ...p, hasTodoJson: hasTodoJson(p.repoPath) };
|
||||
}
|
||||
|
||||
export interface ApiOptions {
|
||||
store: Store;
|
||||
@@ -26,25 +33,66 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
});
|
||||
|
||||
// ---------- Projects ----------
|
||||
app.get('/api/projects', () => store.listProjects());
|
||||
app.get('/api/projects', () => store.listProjects().map(projectOut));
|
||||
|
||||
app.post('/api/projects', (req) => {
|
||||
const b = req.body as Record<string, unknown>;
|
||||
if (!b?.name || !b?.repoPath) throw new StoreError('name 与 repoPath 必填');
|
||||
return store.createProject({
|
||||
return projectOut(store.createProject({
|
||||
name: String(b.name), repoPath: String(b.repoPath),
|
||||
defaultBranch: b.defaultBranch ? String(b.defaultBranch) : undefined,
|
||||
verifyCmd: b.verifyCmd === undefined ? undefined : (b.verifyCmd === null ? null : String(b.verifyCmd)),
|
||||
autonomy: b.autonomy as never, model: b.model === undefined ? undefined : (b.model === null ? null : String(b.model)),
|
||||
concurrency: b.concurrency === undefined ? undefined : Number(b.concurrency),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/api/projects/:id', (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const p = store.getProject(id);
|
||||
if (!p) throw new StoreError(`项目不存在: ${id}`);
|
||||
return p;
|
||||
return projectOut(p);
|
||||
});
|
||||
|
||||
// 部分更新项目配置(校验在 Store.patchProject)
|
||||
app.patch('/api/projects/:id', (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const b = (req.body ?? {}) as Record<string, unknown>;
|
||||
const patch: PatchProjectInput = {};
|
||||
if (b.autonomy !== undefined) patch.autonomy = b.autonomy as Autonomy;
|
||||
if (b.concurrency !== undefined) patch.concurrency = Number(b.concurrency);
|
||||
if (b.status !== undefined) patch.status = b.status as 'active' | 'paused';
|
||||
if (b.verifyCmd !== undefined) patch.verifyCmd = b.verifyCmd === null ? null : String(b.verifyCmd);
|
||||
if (b.model !== undefined) patch.model = b.model === null ? null : String(b.model);
|
||||
return projectOut(store.patchProject(id, patch));
|
||||
});
|
||||
|
||||
// 单向同步 <repoPath>/todo/todo.json → maestro(导入=首次同步,幂等)
|
||||
app.post('/api/projects/:id/sync', (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
return syncProject(store, id);
|
||||
});
|
||||
|
||||
// ---------- Agents(每项目一条;active 来自 runs 表 status='started',执行器 Phase 2 前通常为空) ----------
|
||||
app.get('/api/agents', () => {
|
||||
const runs = store.activeRuns();
|
||||
const byProject = new Map<string, ActiveRun[]>();
|
||||
for (const r of runs) {
|
||||
const list = byProject.get(r.projectId) ?? [];
|
||||
list.push(r);
|
||||
byProject.set(r.projectId, list);
|
||||
}
|
||||
const agents = store.listProjects().map((p) => ({
|
||||
projectId: p.id,
|
||||
projectName: p.name,
|
||||
autonomy: p.autonomy,
|
||||
concurrency: p.concurrency,
|
||||
status: p.status,
|
||||
active: (byProject.get(p.id) ?? []).map((r) => ({
|
||||
runId: r.runId, taskId: r.taskId, taskTitle: r.taskTitle, kind: r.kind, startedAt: r.startedAt,
|
||||
})),
|
||||
}));
|
||||
return { totalActive: runs.length, agents };
|
||||
});
|
||||
|
||||
app.get('/api/projects/:id/tasks', (req) => {
|
||||
@@ -83,6 +131,24 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
return t;
|
||||
});
|
||||
|
||||
// 部分更新任务(title/priority/complexity;complexity 重置逻辑在 Store.patchTask)
|
||||
app.patch('/api/tasks/:id', (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const b = (req.body ?? {}) as Record<string, unknown>;
|
||||
const patch: PatchTaskInput = {};
|
||||
if (b.title !== undefined) patch.title = String(b.title);
|
||||
if (b.priority !== undefined) {
|
||||
const n = Number(b.priority);
|
||||
if (!Number.isFinite(n)) throw new StoreError('priority 必须是数字');
|
||||
patch.priority = n;
|
||||
}
|
||||
if (b.complexity !== undefined) {
|
||||
if (!isComplexity(b.complexity)) throw new StoreError('complexity 必须是 hard|medium|easy');
|
||||
patch.complexity = b.complexity;
|
||||
}
|
||||
return store.patchTask(id, patch);
|
||||
});
|
||||
|
||||
app.get('/api/tasks/:id/children', (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
return store.childrenOf(id);
|
||||
|
||||
Reference in New Issue
Block a user