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:
@@ -1,12 +1,56 @@
|
||||
import { statSync } from 'node:fs';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { Store } from '../store/index.js';
|
||||
import { buildServer, attachWebSocket } from '../api/server.js';
|
||||
import { registerStatic } from '../api/static.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { syncProject, todoJsonPath } from '../sync/todo-sync.js';
|
||||
|
||||
/**
|
||||
* 定时同步:每轮对 active 项目检查 todo.json 的 mtime,比 last_sync_at 新才 sync。
|
||||
* 间隔由 MAESTRO_SYNC_INTERVAL(秒)控制,默认 300,0=关闭。错误只记日志,不崩 daemon。
|
||||
*/
|
||||
function startSyncLoop(store: Store, app: FastifyInstance): NodeJS.Timeout | null {
|
||||
const intervalSec = Number(process.env.MAESTRO_SYNC_INTERVAL ?? 300);
|
||||
if (!Number.isFinite(intervalSec) || intervalSec <= 0) {
|
||||
app.log.info('定时 todo.json 同步已关闭(MAESTRO_SYNC_INTERVAL=0)');
|
||||
return null;
|
||||
}
|
||||
const tick = (): void => {
|
||||
try {
|
||||
for (const p of store.listProjects()) {
|
||||
if (p.status !== 'active') continue;
|
||||
let mtimeIso: string;
|
||||
try {
|
||||
mtimeIso = statSync(todoJsonPath(p.repoPath)).mtime.toISOString();
|
||||
} catch {
|
||||
continue; // 无 todo.json:跳过
|
||||
}
|
||||
if (p.lastSyncAt && mtimeIso <= p.lastSyncAt) continue;
|
||||
try {
|
||||
const r = syncProject(store, p.id);
|
||||
app.log.info(
|
||||
`定时同步「${p.name}」:created=${r.created} doneAdvanced=${r.doneAdvanced} skipped=${r.skipped} warnings=${r.warnings.length}`,
|
||||
);
|
||||
} catch (e) {
|
||||
app.log.error(`定时同步「${p.name}」失败:${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
app.log.error(`定时同步轮询失败:${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
const timer = setInterval(tick, intervalSec * 1000);
|
||||
timer.unref();
|
||||
app.log.info(`定时 todo.json 同步已启用:每 ${intervalSec}s 一轮`);
|
||||
return timer;
|
||||
}
|
||||
|
||||
/** maestrod:核心 daemon。Phase 1 = Store + REST/WS API(手动驱动;编排器在 Phase 2 接入)。 */
|
||||
async function main(): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
const store = new Store(cfg.dbFile);
|
||||
const rec = store.reconcileDeps(); // 启动对账:ready↔blocked 按依赖纠正存量数据
|
||||
const app = buildServer({ store, logger: true });
|
||||
|
||||
registerStatic(app); // Web 看板(web/ 静态文件)
|
||||
@@ -14,9 +58,13 @@ async function main(): Promise<void> {
|
||||
|
||||
await app.listen({ host: cfg.host, port: cfg.port });
|
||||
app.log.info(`maestrod 就绪 · db=${cfg.dbFile} · http://${cfg.host}:${cfg.port} · ws ${cfg.host}:${cfg.port}/ws`);
|
||||
if (rec.blocked || rec.released) app.log.info(`依赖对账:转入等依赖 ${rec.blocked} · 放行可执行 ${rec.released}`);
|
||||
|
||||
const syncTimer = startSyncLoop(store, app);
|
||||
|
||||
const shutdown = async (): Promise<void> => {
|
||||
app.log.info('收到退出信号,关闭中…');
|
||||
if (syncTimer) clearInterval(syncTimer);
|
||||
await app.close();
|
||||
store.close();
|
||||
process.exit(0);
|
||||
|
||||
Reference in New Issue
Block a user