fa472a06a7
后端: - 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>
71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
import { readFile } from 'node:fs/promises';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, extname, join, resolve, sep } from 'node:path';
|
||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
||
|
||
/**
|
||
* Web 看板静态文件服务(零依赖,手写)。
|
||
* web/ 不参与 tsc 构建,直接相对仓库根定位:
|
||
* dev 时本文件在 src/api/,build 后在 dist/api/,两者到仓库根都是 ../..(与 store/db.ts 同思路)。
|
||
*/
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const WEB_ROOT = resolve(HERE, '..', '..', 'web');
|
||
|
||
const MIME: Record<string, string> = {
|
||
'.html': 'text/html; charset=utf-8',
|
||
'.js': 'text/javascript; charset=utf-8',
|
||
'.mjs': 'text/javascript; charset=utf-8',
|
||
'.css': 'text/css; charset=utf-8',
|
||
'.json': 'application/json; charset=utf-8',
|
||
'.svg': 'image/svg+xml',
|
||
'.png': 'image/png',
|
||
'.jpg': 'image/jpeg',
|
||
'.gif': 'image/gif',
|
||
'.ico': 'image/x-icon',
|
||
'.webp': 'image/webp',
|
||
'.woff': 'font/woff',
|
||
'.woff2': 'font/woff2',
|
||
'.map': 'application/json',
|
||
'.txt': 'text/plain; charset=utf-8',
|
||
};
|
||
|
||
async function serveFile(reply: FastifyReply, rawPath: string): Promise<FastifyReply> {
|
||
let rel: string;
|
||
try {
|
||
rel = decodeURIComponent(rawPath);
|
||
} catch {
|
||
return reply.code(400).type('text/plain; charset=utf-8').send('Bad Request');
|
||
}
|
||
if (rel.includes('\0')) {
|
||
return reply.code(400).type('text/plain; charset=utf-8').send('Bad Request');
|
||
}
|
||
|
||
// 路径穿越防护:解析后必须仍落在 WEB_ROOT 之内
|
||
const abs = resolve(WEB_ROOT, '.' + sep + rel);
|
||
if (abs !== WEB_ROOT && !abs.startsWith(WEB_ROOT + sep)) {
|
||
return reply.code(403).type('text/plain; charset=utf-8').send('Forbidden');
|
||
}
|
||
|
||
const file = abs === WEB_ROOT ? join(WEB_ROOT, 'index.html') : abs;
|
||
try {
|
||
const buf = await readFile(file);
|
||
const type = MIME[extname(file).toLowerCase()] ?? 'application/octet-stream';
|
||
// 本地开发工具:禁启发式缓存,每次重新验证(否则浏览器会吃旧 CSS/JS)
|
||
return reply.header('cache-control', 'no-cache').type(type).send(buf);
|
||
} catch {
|
||
return reply.code(404).type('text/plain; charset=utf-8').send('Not Found');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在 Fastify 上注册看板静态路由。
|
||
* `/` → web/index.html;`/*` → web/ 下对应文件(/api/**、/ws 等更具体路由优先匹配,不受影响)。
|
||
*/
|
||
export function registerStatic(app: FastifyInstance): void {
|
||
app.get('/', (_req, reply) => serveFile(reply, 'index.html'));
|
||
app.get('/*', (req, reply) => {
|
||
const params = req.params as { '*': string };
|
||
return serveFile(reply, params['*'] ?? '');
|
||
});
|
||
}
|