aifastdb-devplan 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/autopilot.d.ts +58 -0
  2. package/dist/autopilot.d.ts.map +1 -0
  3. package/dist/autopilot.js +250 -0
  4. package/dist/autopilot.js.map +1 -0
  5. package/dist/dev-plan-document-store.d.ts +2 -0
  6. package/dist/dev-plan-document-store.d.ts.map +1 -1
  7. package/dist/dev-plan-document-store.js +3 -0
  8. package/dist/dev-plan-document-store.js.map +1 -1
  9. package/dist/dev-plan-factory.d.ts +69 -3
  10. package/dist/dev-plan-factory.d.ts.map +1 -1
  11. package/dist/dev-plan-factory.js +111 -18
  12. package/dist/dev-plan-factory.js.map +1 -1
  13. package/dist/dev-plan-graph-store.d.ts +15 -0
  14. package/dist/dev-plan-graph-store.d.ts.map +1 -1
  15. package/dist/dev-plan-graph-store.js +57 -2
  16. package/dist/dev-plan-graph-store.js.map +1 -1
  17. package/dist/index.d.ts +3 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +14 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/mcp-server/index.d.ts +3 -0
  22. package/dist/mcp-server/index.d.ts.map +1 -1
  23. package/dist/mcp-server/index.js +278 -4
  24. package/dist/mcp-server/index.js.map +1 -1
  25. package/dist/types.d.ts +72 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js +9 -1
  28. package/dist/types.js.map +1 -1
  29. package/dist/visualize/graph-canvas/api-compat.d.ts +20 -0
  30. package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -0
  31. package/dist/visualize/graph-canvas/api-compat.js +334 -0
  32. package/dist/visualize/graph-canvas/api-compat.js.map +1 -0
  33. package/dist/visualize/graph-canvas/clusterer.d.ts +16 -0
  34. package/dist/visualize/graph-canvas/clusterer.d.ts.map +1 -0
  35. package/dist/visualize/graph-canvas/clusterer.js +460 -0
  36. package/dist/visualize/graph-canvas/clusterer.js.map +1 -0
  37. package/dist/visualize/graph-canvas/core.d.ts +11 -0
  38. package/dist/visualize/graph-canvas/core.d.ts.map +1 -0
  39. package/dist/visualize/graph-canvas/core.js +844 -0
  40. package/dist/visualize/graph-canvas/core.js.map +1 -0
  41. package/dist/visualize/graph-canvas/index.d.ts +22 -0
  42. package/dist/visualize/graph-canvas/index.d.ts.map +1 -0
  43. package/dist/visualize/graph-canvas/index.js +69 -0
  44. package/dist/visualize/graph-canvas/index.js.map +1 -0
  45. package/dist/visualize/graph-canvas/interaction.d.ts +13 -0
  46. package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -0
  47. package/dist/visualize/graph-canvas/interaction.js +446 -0
  48. package/dist/visualize/graph-canvas/interaction.js.map +1 -0
  49. package/dist/visualize/graph-canvas/layout-worker.d.ts +17 -0
  50. package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -0
  51. package/dist/visualize/graph-canvas/layout-worker.js +541 -0
  52. package/dist/visualize/graph-canvas/layout-worker.js.map +1 -0
  53. package/dist/visualize/graph-canvas/lod.d.ts +10 -0
  54. package/dist/visualize/graph-canvas/lod.d.ts.map +1 -0
  55. package/dist/visualize/graph-canvas/lod.js +111 -0
  56. package/dist/visualize/graph-canvas/lod.js.map +1 -0
  57. package/dist/visualize/graph-canvas/renderer.d.ts +12 -0
  58. package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -0
  59. package/dist/visualize/graph-canvas/renderer.js +682 -0
  60. package/dist/visualize/graph-canvas/renderer.js.map +1 -0
  61. package/dist/visualize/graph-canvas/spatial-index.d.ts +13 -0
  62. package/dist/visualize/graph-canvas/spatial-index.d.ts.map +1 -0
  63. package/dist/visualize/graph-canvas/spatial-index.js +482 -0
  64. package/dist/visualize/graph-canvas/spatial-index.js.map +1 -0
  65. package/dist/visualize/graph-canvas/styles.d.ts +11 -0
  66. package/dist/visualize/graph-canvas/styles.d.ts.map +1 -0
  67. package/dist/visualize/graph-canvas/styles.js +137 -0
  68. package/dist/visualize/graph-canvas/styles.js.map +1 -0
  69. package/dist/visualize/graph-canvas/viewport.d.ts +17 -0
  70. package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -0
  71. package/dist/visualize/graph-canvas/viewport.js +375 -0
  72. package/dist/visualize/graph-canvas/viewport.js.map +1 -0
  73. package/dist/visualize/server.js +619 -6
  74. package/dist/visualize/server.js.map +1 -1
  75. package/dist/visualize/template.d.ts.map +1 -1
  76. package/dist/visualize/template.js +604 -18
  77. package/dist/visualize/template.js.map +1 -1
  78. package/package.json +1 -1
@@ -54,6 +54,8 @@ const path = __importStar(require("path"));
54
54
  const dev_plan_graph_store_1 = require("../dev-plan-graph-store");
55
55
  const dev_plan_factory_1 = require("../dev-plan-factory");
56
56
  const template_1 = require("./template");
57
+ const index_1 = require("./graph-canvas/index");
58
+ const autopilot_1 = require("../autopilot");
57
59
  function parseArgs() {
58
60
  const args = process.argv.slice(2);
59
61
  let project = '';
@@ -87,7 +89,8 @@ function parseArgs() {
87
89
  process.exit(1);
88
90
  }
89
91
  if (!basePath) {
90
- basePath = (0, dev_plan_factory_1.getDefaultBasePath)();
92
+ // 多项目路由:优先使用项目注册表路由的 basePath
93
+ basePath = (0, dev_plan_factory_1.resolveBasePathForProject)(project);
91
94
  }
92
95
  return { project, port, basePath };
93
96
  }
@@ -120,13 +123,137 @@ function createStore(project, basePath) {
120
123
  function createFreshStore(projectName, basePath) {
121
124
  return (0, dev_plan_factory_1.createDevPlan)(projectName, basePath, 'graph');
122
125
  }
126
+ // ============================================================================
127
+ // Meta Question Detection — 元信息智能问答
128
+ // ============================================================================
129
+ /**
130
+ * 检测是否为元信息问题(关于项目/数据库本身的统计类问题)。
131
+ * 如果是,直接生成回答文本;如果不是,返回 null 继续走搜索流程。
132
+ */
133
+ function detectMetaQuestion(store, projectName, query, qLower) {
134
+ // ---- 文档数量 ----
135
+ if (matchAny(qLower, ['多少篇文档', '多少文档', '文档数量', '文档总数', '几篇文档', 'how many doc', 'document count'])) {
136
+ const sections = store.listSections();
137
+ const bySection = {};
138
+ for (const s of sections) {
139
+ bySection[s.section] = (bySection[s.section] || 0) + 1;
140
+ }
141
+ let detail = Object.entries(bySection)
142
+ .sort((a, b) => b[1] - a[1])
143
+ .map(([sec, cnt]) => ` • ${sec}: ${cnt} 篇`)
144
+ .join('\n');
145
+ return `📊 项目 **${projectName}** 共有 **${sections.length}** 篇文档。\n\n按类型分布:\n${detail}`;
146
+ }
147
+ // ---- 项目进度 ----
148
+ if (matchAny(qLower, ['项目进度', '完成进度', '整体进度', '完成率', '完成了多少', '进展如何', 'progress', 'how much done'])) {
149
+ const progress = store.getProgress();
150
+ const tasks = progress.tasks || [];
151
+ const completed = tasks.filter((t) => t.status === 'completed').length;
152
+ const inProgress = tasks.filter((t) => t.status === 'in_progress').length;
153
+ const pending = tasks.filter((t) => t.status === 'pending').length;
154
+ return `📊 项目 **${projectName}** 整体进度:**${progress.overallPercent || 0}%**\n\n` +
155
+ `• 主任务总数: ${progress.mainTaskCount || 0}\n` +
156
+ `• ✅ 已完成: ${completed}\n` +
157
+ `• 🔄 进行中: ${inProgress}\n` +
158
+ `• ⬜ 待开始: ${pending}\n` +
159
+ `• 子任务: ${progress.completedSubTasks || 0} / ${progress.subTaskCount || 0} 已完成`;
160
+ }
161
+ // ---- 主任务/阶段列表 ----
162
+ if (matchAny(qLower, ['有哪些阶段', '有多少阶段', '阶段列表', '任务列表', '所有阶段', 'phase list', 'all phases', '有多少个phase'])) {
163
+ const progress = store.getProgress();
164
+ const tasks = progress.tasks || [];
165
+ const statusIcon = (s) => s === 'completed' ? '✅' : s === 'in_progress' ? '🔄' : '⬜';
166
+ let lines = tasks.map((t) => ` ${statusIcon(t.status)} ${t.taskId}: ${t.title} (${t.completed}/${t.total})`).join('\n');
167
+ return `📋 项目 **${projectName}** 共有 **${tasks.length}** 个开发阶段:\n\n${lines}`;
168
+ }
169
+ // ---- 模块列表 ----
170
+ if (matchAny(qLower, ['有哪些模块', '模块列表', '功能模块', 'module list', 'all modules', '有多少模块'])) {
171
+ const modules = store.listModules();
172
+ if (modules.length === 0) {
173
+ return `📦 项目 **${projectName}** 暂未定义功能模块。`;
174
+ }
175
+ let lines = modules.map((m) => ` • **${m.name}** (${m.moduleId}) — ${m.status} | ${m.completedSubTaskCount || 0}/${m.subTaskCount || 0} 子任务`).join('\n');
176
+ return `📦 项目 **${projectName}** 共有 **${modules.length}** 个功能模块:\n\n${lines}`;
177
+ }
178
+ // ---- 最近完成/更新 ----
179
+ if (matchAny(qLower, ['最近完成', '最近更新', '最新完成', '最新的文档', '最新文档', 'recently completed', 'latest update'])) {
180
+ const sections = store.listSections();
181
+ const sorted = [...sections]
182
+ .filter((s) => s.updatedAt)
183
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
184
+ .slice(0, 8);
185
+ if (sorted.length === 0) {
186
+ return `📄 暂无文档更新记录。`;
187
+ }
188
+ let lines = sorted.map((s) => {
189
+ const d = new Date(s.updatedAt);
190
+ const dateStr = `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, '0')}`;
191
+ return ` • [${dateStr}] **${s.title}** (${s.section}${s.subSection ? '|' + s.subSection : ''})`;
192
+ }).join('\n');
193
+ return `📄 最近更新的文档:\n\n${lines}`;
194
+ }
195
+ // ---- 项目名称/是什么项目 ----
196
+ if (matchAny(qLower, ['什么项目', '项目介绍', '项目名称', '这是什么', 'what project', 'project name', '项目是什么'])) {
197
+ const sections = store.listSections();
198
+ const progress = store.getProgress();
199
+ const modules = store.listModules();
200
+ return `📌 当前项目: **${projectName}**\n\n` +
201
+ `• 文档总数: ${sections.length} 篇\n` +
202
+ `• 开发阶段: ${progress.mainTaskCount || 0} 个 (${progress.overallPercent || 0}% 完成)\n` +
203
+ `• 功能模块: ${modules.length} 个\n` +
204
+ `• 子任务: ${progress.completedSubTasks || 0} / ${progress.subTaskCount || 0}\n\n` +
205
+ `💡 你可以问我关于文档内容的问题,我会在文档库中搜索相关内容。`;
206
+ }
207
+ // ---- 搜索能力说明 ----
208
+ if (matchAny(qLower, ['你能做什么', '你会什么', '怎么用', '使用说明', 'help', '帮助', '功能介绍'])) {
209
+ const isSemanticEnabled = store.isSemanticSearchEnabled?.() || false;
210
+ return `🤖 我是 **DevPlan 文档助手**,可以帮你:\n\n` +
211
+ `📊 **回答项目统计问题**\n` +
212
+ ` 例如: "有多少篇文档"、"项目进度"、"有哪些阶段"\n\n` +
213
+ `🔍 **搜索文档内容**\n` +
214
+ ` 例如: "向量搜索"、"GPU 加速"、"aifastdb vs LanceDB"\n` +
215
+ ` 搜索模式: ${isSemanticEnabled ? '语义+字面混合搜索 (Candle MiniLM)' : '字面匹配'}\n\n` +
216
+ `📄 **查看文档**\n` +
217
+ ` 点击搜索结果卡片可直接查看完整文档\n\n` +
218
+ `⚠️ 注意: 我没有 LLM 推理能力,无法"理解"和"推理",` +
219
+ `只能做文档检索和元信息查询。对于复杂问题建议直接搜索关键词。`;
220
+ }
221
+ return null; // 不是元信息问题,继续搜索流程
222
+ }
223
+ /** 检查 query 是否匹配任意关键词模式 */
224
+ function matchAny(qLower, patterns) {
225
+ return patterns.some(p => qLower.includes(p));
226
+ }
227
+ /**
228
+ * 读取 HTTP POST 请求体并解析为 JSON
229
+ */
230
+ function readRequestBody(req) {
231
+ return new Promise((resolve, reject) => {
232
+ let data = '';
233
+ req.on('data', (chunk) => { data += chunk.toString(); });
234
+ req.on('end', () => {
235
+ if (!data) {
236
+ resolve({});
237
+ return;
238
+ }
239
+ try {
240
+ resolve(JSON.parse(data));
241
+ }
242
+ catch {
243
+ reject(new Error('Invalid JSON body'));
244
+ }
245
+ });
246
+ req.on('error', reject);
247
+ });
248
+ }
123
249
  function startServer(projectName, basePath, port) {
124
250
  const htmlContent = (0, template_1.getVisualizationHTML)(projectName);
125
- const server = http.createServer((req, res) => {
251
+ const graphCanvasJs = (0, index_1.getGraphCanvasScript)();
252
+ const server = http.createServer(async (req, res) => {
126
253
  const url = new URL(req.url || '/', `http://localhost:${port}`);
127
254
  // CORS headers
128
255
  res.setHeader('Access-Control-Allow-Origin', '*');
129
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
256
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
130
257
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
131
258
  // 禁止浏览器缓存 API 响应,确保 F5 刷新时总是获取最新数据
132
259
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
@@ -323,6 +450,487 @@ function startServer(projectName, basePath, port) {
323
450
  }
324
451
  break;
325
452
  }
453
+ case '/api/chat': {
454
+ // POST /api/chat — 智能文档对话(元信息问答 + 语义搜索 + 分数过滤)
455
+ if (req.method !== 'POST') {
456
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
457
+ res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
458
+ break;
459
+ }
460
+ const body = await readRequestBody(req);
461
+ const query = body?.query;
462
+ if (!query || typeof query !== 'string' || query.trim().length === 0) {
463
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
464
+ res.end(JSON.stringify({ error: '缺少 query 参数' }));
465
+ break;
466
+ }
467
+ const store = createFreshStore(projectName, basePath);
468
+ const q = query.trim();
469
+ const qLower = q.toLowerCase();
470
+ // ================================================================
471
+ // 第一步:检测元信息问题,直接回答
472
+ // ================================================================
473
+ const metaAnswer = detectMetaQuestion(store, projectName, q, qLower);
474
+ if (metaAnswer) {
475
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
476
+ res.end(JSON.stringify({
477
+ query: q,
478
+ type: 'meta',
479
+ answer: metaAnswer,
480
+ }));
481
+ break;
482
+ }
483
+ // ================================================================
484
+ // 第二步:文档内容搜索(带分数过滤)
485
+ // ================================================================
486
+ const limit = body.limit || 5;
487
+ const MIN_SCORE = 0.03; // 低于此分数视为不相关
488
+ let results = [];
489
+ let searchMode = 'literal';
490
+ if (store.searchSectionsAdvanced) {
491
+ const isSemanticEnabled = store.isSemanticSearchEnabled?.() || false;
492
+ searchMode = isSemanticEnabled ? 'hybrid' : 'literal';
493
+ const hits = store.searchSectionsAdvanced(q, {
494
+ mode: searchMode,
495
+ limit: limit * 2, // 多取一些,后面过滤
496
+ minScore: 0,
497
+ });
498
+ results = hits
499
+ .filter((doc) => doc.score == null || doc.score >= MIN_SCORE)
500
+ .slice(0, limit)
501
+ .map((doc) => ({
502
+ section: doc.section,
503
+ subSection: doc.subSection || null,
504
+ title: doc.title,
505
+ score: doc.score != null ? Math.round(doc.score * 1000) / 1000 : null,
506
+ snippet: (doc.content || '').substring(0, 300).replace(/\n/g, ' ').trim(),
507
+ updatedAt: doc.updatedAt || null,
508
+ version: doc.version || null,
509
+ }));
510
+ }
511
+ else {
512
+ const hits = store.searchSections(q, limit);
513
+ results = hits.map((doc) => ({
514
+ section: doc.section,
515
+ subSection: doc.subSection || null,
516
+ title: doc.title,
517
+ score: null,
518
+ snippet: (doc.content || '').substring(0, 300).replace(/\n/g, ' ').trim(),
519
+ updatedAt: doc.updatedAt || null,
520
+ version: doc.version || null,
521
+ }));
522
+ }
523
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
524
+ res.end(JSON.stringify({
525
+ query: q,
526
+ type: 'search',
527
+ mode: searchMode,
528
+ count: results.length,
529
+ results,
530
+ }));
531
+ break;
532
+ }
533
+ // ================================================================
534
+ // Autopilot API Endpoints (/api/auto/*)
535
+ // ================================================================
536
+ case '/api/auto/next-action': {
537
+ // GET /api/auto/next-action — 获取下一步该执行什么动作(executor 轮询)
538
+ const store = createFreshStore(projectName, basePath);
539
+ const nextAction = (0, autopilot_1.getAutopilotNextAction)(store);
540
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
541
+ res.end(JSON.stringify(nextAction));
542
+ break;
543
+ }
544
+ case '/api/auto/current-phase': {
545
+ // GET /api/auto/current-phase — 获取当前进行中阶段及全部子任务状态
546
+ const store = createFreshStore(projectName, basePath);
547
+ const status = (0, autopilot_1.getAutopilotStatus)(store);
548
+ if (!status.hasActivePhase || !status.activePhase) {
549
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
550
+ res.end(JSON.stringify({
551
+ hasActivePhase: false,
552
+ message: '当前无进行中的阶段',
553
+ }));
554
+ break;
555
+ }
556
+ // 获取活跃阶段的全部子任务详情
557
+ const subTasks = store.listSubTasks(status.activePhase.taskId).map((s) => ({
558
+ taskId: s.taskId,
559
+ title: s.title,
560
+ status: s.status,
561
+ description: s.description || null,
562
+ order: s.order,
563
+ completedAt: s.completedAt || null,
564
+ }));
565
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
566
+ res.end(JSON.stringify({
567
+ hasActivePhase: true,
568
+ activePhase: status.activePhase,
569
+ currentSubTask: status.currentSubTask || null,
570
+ nextPendingSubTask: status.nextPendingSubTask || null,
571
+ subTasks,
572
+ }));
573
+ break;
574
+ }
575
+ case '/api/auto/complete-task': {
576
+ // POST /api/auto/complete-task — 标记子任务完成
577
+ if (req.method !== 'POST') {
578
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
579
+ res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
580
+ break;
581
+ }
582
+ const body = await readRequestBody(req);
583
+ const taskId = body?.taskId;
584
+ if (!taskId || typeof taskId !== 'string') {
585
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
586
+ res.end(JSON.stringify({ error: '缺少 taskId 参数' }));
587
+ break;
588
+ }
589
+ const store = createFreshStore(projectName, basePath);
590
+ try {
591
+ const result = store.completeSubTask(taskId);
592
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
593
+ res.end(JSON.stringify({
594
+ success: true,
595
+ subTask: {
596
+ taskId: result.subTask.taskId,
597
+ title: result.subTask.title,
598
+ status: result.subTask.status,
599
+ },
600
+ mainTask: {
601
+ taskId: result.mainTask.taskId,
602
+ title: result.mainTask.title,
603
+ status: result.mainTask.status,
604
+ totalSubtasks: result.mainTask.totalSubtasks,
605
+ completedSubtasks: result.mainTask.completedSubtasks,
606
+ },
607
+ mainTaskCompleted: result.mainTaskCompleted,
608
+ completedAtCommit: result.completedAtCommit || null,
609
+ }));
610
+ }
611
+ catch (err) {
612
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
613
+ res.end(JSON.stringify({ error: err.message || String(err) }));
614
+ }
615
+ break;
616
+ }
617
+ case '/api/auto/start-phase': {
618
+ // POST /api/auto/start-phase — 启动新阶段
619
+ if (req.method !== 'POST') {
620
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
621
+ res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
622
+ break;
623
+ }
624
+ const body = await readRequestBody(req);
625
+ const taskId = body?.taskId;
626
+ if (!taskId || typeof taskId !== 'string') {
627
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
628
+ res.end(JSON.stringify({ error: '缺少 taskId 参数' }));
629
+ break;
630
+ }
631
+ const store = createFreshStore(projectName, basePath);
632
+ const mainTask = store.getMainTask(taskId);
633
+ if (!mainTask) {
634
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
635
+ res.end(JSON.stringify({ error: `主任务 "${taskId}" 不存在` }));
636
+ break;
637
+ }
638
+ // 标记为 in_progress(幂等)
639
+ if (mainTask.status === 'pending') {
640
+ store.updateMainTaskStatus(taskId, 'in_progress');
641
+ }
642
+ const subTasks = store.listSubTasks(taskId).map((s) => ({
643
+ taskId: s.taskId,
644
+ title: s.title,
645
+ status: s.status,
646
+ description: s.description || null,
647
+ order: s.order,
648
+ }));
649
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
650
+ res.end(JSON.stringify({
651
+ success: true,
652
+ mainTask: {
653
+ taskId: mainTask.taskId,
654
+ title: mainTask.title,
655
+ status: 'in_progress',
656
+ totalSubtasks: subTasks.length,
657
+ completedSubtasks: subTasks.filter((s) => s.status === 'completed').length,
658
+ },
659
+ subTasks,
660
+ message: `阶段 ${taskId} 已启动,共 ${subTasks.length} 个子任务`,
661
+ }));
662
+ break;
663
+ }
664
+ case '/api/auto/heartbeat': {
665
+ // POST /api/auto/heartbeat — executor 心跳上报
666
+ if (req.method !== 'POST') {
667
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
668
+ res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
669
+ break;
670
+ }
671
+ const body = await readRequestBody(req);
672
+ if (!body?.executorId || !body?.status) {
673
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
674
+ res.end(JSON.stringify({ error: '缺少 executorId 或 status 参数' }));
675
+ break;
676
+ }
677
+ const heartbeat = {
678
+ executorId: body.executorId,
679
+ status: body.status,
680
+ lastScreenState: body.lastScreenState || undefined,
681
+ timestamp: body.timestamp || Date.now(),
682
+ };
683
+ (0, autopilot_1.recordHeartbeat)(projectName, heartbeat);
684
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
685
+ res.end(JSON.stringify({
686
+ success: true,
687
+ receivedAt: Date.now(),
688
+ message: `心跳已接收: executor=${heartbeat.executorId}, status=${heartbeat.status}`,
689
+ }));
690
+ break;
691
+ }
692
+ case '/api/auto/status': {
693
+ // GET /api/auto/status — 获取完整的 autopilot 状态(含心跳信息)
694
+ const store = createFreshStore(projectName, basePath);
695
+ const status = (0, autopilot_1.getAutopilotStatus)(store);
696
+ const heartbeatInfo = (0, autopilot_1.getLastHeartbeat)(projectName);
697
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
698
+ res.end(JSON.stringify({
699
+ ...status,
700
+ executor: {
701
+ lastHeartbeat: heartbeatInfo.heartbeat,
702
+ receivedAt: heartbeatInfo.receivedAt || null,
703
+ isAlive: heartbeatInfo.isAlive,
704
+ },
705
+ }));
706
+ break;
707
+ }
708
+ // ==================================================================
709
+ // Autopilot API Endpoints (/api/auto/*)
710
+ // ==================================================================
711
+ case '/api/auto/next-action': {
712
+ // GET — 获取下一步该执行什么动作(供 executor 轮询)
713
+ const store = createFreshStore(projectName, basePath);
714
+ const nextAction = (0, autopilot_1.getAutopilotNextAction)(store);
715
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
716
+ res.end(JSON.stringify(nextAction));
717
+ break;
718
+ }
719
+ case '/api/auto/current-phase': {
720
+ // GET — 获取当前进行中阶段及全部子任务状态
721
+ const store = createFreshStore(projectName, basePath);
722
+ const status = (0, autopilot_1.getAutopilotStatus)(store);
723
+ if (!status.hasActivePhase || !status.activePhase) {
724
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
725
+ res.end(JSON.stringify({
726
+ hasActivePhase: false,
727
+ message: '当前没有进行中的阶段',
728
+ }));
729
+ break;
730
+ }
731
+ // 获取活跃阶段的全部子任务详情
732
+ const subTasks = store.listSubTasks(status.activePhase.taskId);
733
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
734
+ res.end(JSON.stringify({
735
+ hasActivePhase: true,
736
+ phase: status.activePhase,
737
+ currentSubTask: status.currentSubTask || null,
738
+ nextPendingSubTask: status.nextPendingSubTask || null,
739
+ subTasks: subTasks.map(s => ({
740
+ taskId: s.taskId,
741
+ title: s.title,
742
+ status: s.status,
743
+ description: s.description,
744
+ order: s.order,
745
+ completedAt: s.completedAt,
746
+ })),
747
+ }));
748
+ break;
749
+ }
750
+ case '/api/auto/complete-task': {
751
+ // POST — 标记子任务完成
752
+ if (req.method !== 'POST') {
753
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
754
+ res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
755
+ break;
756
+ }
757
+ const body = await readRequestBody(req);
758
+ const { taskId } = body;
759
+ if (!taskId || typeof taskId !== 'string') {
760
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
761
+ res.end(JSON.stringify({ error: '缺少 taskId 参数' }));
762
+ break;
763
+ }
764
+ const store = createFreshStore(projectName, basePath);
765
+ try {
766
+ const result = store.completeSubTask(taskId);
767
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
768
+ res.end(JSON.stringify({
769
+ success: true,
770
+ taskId,
771
+ mainTaskCompleted: result.mainTaskCompleted,
772
+ completedAtCommit: result.completedAtCommit || null,
773
+ mainTask: {
774
+ taskId: result.mainTask.taskId,
775
+ title: result.mainTask.title,
776
+ totalSubtasks: result.mainTask.totalSubtasks,
777
+ completedSubtasks: result.mainTask.completedSubtasks,
778
+ status: result.mainTask.status,
779
+ },
780
+ }));
781
+ }
782
+ catch (err) {
783
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
784
+ res.end(JSON.stringify({ error: err.message || String(err) }));
785
+ }
786
+ break;
787
+ }
788
+ case '/api/auto/start-phase': {
789
+ // POST — 启动新阶段
790
+ if (req.method !== 'POST') {
791
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
792
+ res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
793
+ break;
794
+ }
795
+ const body = await readRequestBody(req);
796
+ const { taskId } = body;
797
+ if (!taskId || typeof taskId !== 'string') {
798
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
799
+ res.end(JSON.stringify({ error: '缺少 taskId 参数' }));
800
+ break;
801
+ }
802
+ const store = createFreshStore(projectName, basePath);
803
+ const mainTask = store.getMainTask(taskId);
804
+ if (!mainTask) {
805
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
806
+ res.end(JSON.stringify({ error: `主任务 "${taskId}" 未找到` }));
807
+ break;
808
+ }
809
+ if (mainTask.status === 'pending') {
810
+ store.updateMainTaskStatus(taskId, 'in_progress');
811
+ }
812
+ const subTasks = store.listSubTasks(taskId);
813
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
814
+ res.end(JSON.stringify({
815
+ success: true,
816
+ phase: {
817
+ taskId: mainTask.taskId,
818
+ title: mainTask.title,
819
+ status: 'in_progress',
820
+ totalSubtasks: subTasks.length,
821
+ completedSubtasks: subTasks.filter(s => s.status === 'completed').length,
822
+ },
823
+ subTasks: subTasks.map(s => ({
824
+ taskId: s.taskId,
825
+ title: s.title,
826
+ status: s.status,
827
+ description: s.description,
828
+ order: s.order,
829
+ })),
830
+ }));
831
+ break;
832
+ }
833
+ case '/api/auto/heartbeat': {
834
+ // POST — executor 心跳上报
835
+ if (req.method !== 'POST') {
836
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
837
+ res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
838
+ break;
839
+ }
840
+ const body = await readRequestBody(req);
841
+ const heartbeat = {
842
+ executorId: body.executorId || 'unknown',
843
+ status: body.status || 'active',
844
+ lastScreenState: body.lastScreenState,
845
+ timestamp: body.timestamp || Date.now(),
846
+ };
847
+ (0, autopilot_1.recordHeartbeat)(projectName, heartbeat);
848
+ const hbInfo = (0, autopilot_1.getLastHeartbeat)(projectName);
849
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
850
+ res.end(JSON.stringify({
851
+ success: true,
852
+ received: heartbeat,
853
+ isAlive: hbInfo.isAlive,
854
+ }));
855
+ break;
856
+ }
857
+ case '/api/graph/paged': {
858
+ // ── Phase-8C T8C.3: Paginated graph data API ──
859
+ // GET /api/graph/paged?offset=0&limit=5000&sortBy=center
860
+ // Returns a page of { nodes, edges, total, hasMore }
861
+ // sortBy=center: viewport-priority (nodes sorted by distance to center)
862
+ // sortBy=default: natural order
863
+ const store = createFreshStore(projectName, basePath);
864
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
865
+ const limit = parseInt(url.searchParams.get('limit') || '5000', 10);
866
+ const sortBy = url.searchParams.get('sortBy') || 'default';
867
+ const includeDocuments = url.searchParams.get('includeDocuments') !== 'false';
868
+ const includeModules = url.searchParams.get('includeModules') !== 'false';
869
+ if (store.exportGraph) {
870
+ const fullGraph = store.exportGraph({
871
+ includeDocuments,
872
+ includeModules,
873
+ includeNodeDegree: true,
874
+ enableBackendDegreeFallback: true,
875
+ });
876
+ if (!fullGraph) {
877
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
878
+ res.end(JSON.stringify({ error: '图数据导出失败' }));
879
+ break;
880
+ }
881
+ let allNodes = fullGraph.nodes || [];
882
+ const allEdges = fullGraph.edges || [];
883
+ // T8C.4: Viewport-priority sorting — sort by distance from center
884
+ if (sortBy === 'center') {
885
+ // Calculate centroid of all nodes
886
+ let cx = 0, cy = 0;
887
+ for (const n of allNodes) {
888
+ cx += (n.x || 0);
889
+ cy += (n.y || 0);
890
+ }
891
+ if (allNodes.length > 0) {
892
+ cx /= allNodes.length;
893
+ cy /= allNodes.length;
894
+ }
895
+ // Sort by distance to centroid (closest first)
896
+ allNodes = [...allNodes].sort((a, b) => {
897
+ const da = (a.x - cx) ** 2 + (a.y - cy) ** 2;
898
+ const db = (b.x - cx) ** 2 + (b.y - cy) ** 2;
899
+ return da - db;
900
+ });
901
+ }
902
+ // Slice the page
903
+ const pageNodes = allNodes.slice(offset, offset + limit);
904
+ const pageNodeIds = new Set(pageNodes.map((n) => n.id));
905
+ // Include only edges where both endpoints are in this page
906
+ const pageEdges = allEdges.filter((e) => pageNodeIds.has(e.from) && pageNodeIds.has(e.to));
907
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
908
+ res.end(JSON.stringify({
909
+ nodes: pageNodes,
910
+ edges: pageEdges,
911
+ total: allNodes.length,
912
+ totalEdges: allEdges.length,
913
+ offset,
914
+ limit,
915
+ hasMore: offset + limit < allNodes.length,
916
+ nextOffset: Math.min(offset + limit, allNodes.length),
917
+ }));
918
+ }
919
+ else {
920
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
921
+ res.end(JSON.stringify({ error: '当前引擎不支持图导出' }));
922
+ }
923
+ break;
924
+ }
925
+ case '/graph-canvas.js': {
926
+ // Serve the GraphCanvas engine as a JavaScript file
927
+ res.writeHead(200, {
928
+ 'Content-Type': 'application/javascript; charset=utf-8',
929
+ 'Cache-Control': 'public, max-age=3600',
930
+ });
931
+ res.end(graphCanvasJs);
932
+ break;
933
+ }
326
934
  case '/favicon.ico':
327
935
  res.writeHead(204);
328
936
  res.end();
@@ -348,9 +956,14 @@ function startServer(projectName, basePath, port) {
348
956
  console.log(`║ 地址: ${url.padEnd(47)}║`);
349
957
  console.log('╠══════════════════════════════════════════════════════════╣');
350
958
  console.log('║ API 端点: ║');
351
- console.log(`║ GET / 可视化页面 ║`);
352
- console.log(`║ GET /api/graph 图谱数据 (JSON) ║`);
353
- console.log(`║ GET /api/progress 项目进度 (JSON) ║`);
959
+ console.log(`║ GET / 可视化页面 ║`);
960
+ console.log(`║ GET /api/graph 图谱数据 (JSON) ║`);
961
+ console.log(`║ GET /api/progress 项目进度 (JSON) ║`);
962
+ console.log(`║ GET /api/auto/next-action 下一步动作 ║`);
963
+ console.log(`║ GET /api/auto/current-phase 当前阶段状态 ║`);
964
+ console.log(`║ POST /api/auto/complete-task 完成子任务 ║`);
965
+ console.log(`║ POST /api/auto/start-phase 启动新阶段 ║`);
966
+ console.log(`║ POST /api/auto/heartbeat 心跳上报 ║`);
354
967
  console.log('╠══════════════════════════════════════════════════════════╣');
355
968
  console.log('║ 按 Ctrl+C 停止服务器 ║');
356
969
  console.log('╚══════════════════════════════════════════════════════════╝');