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:
wangjia
2026-06-12 23:58:28 +08:00
parent c6e66baa19
commit fa472a06a7
18 changed files with 1942 additions and 368 deletions
+71 -5
View File
@@ -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/complexitycomplexity 重置逻辑在 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);