autosnippet 2.5.0 → 2.7.0

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 (72) hide show
  1. package/bin/cli.js +35 -0
  2. package/dashboard/dist/assets/{icons-Dtm0E6DS.js → icons-Cq4-iQhP.js} +152 -87
  3. package/dashboard/dist/assets/index-DBxH7pVn.css +1 -0
  4. package/dashboard/dist/assets/index-Dw2F6qAS.js +197 -0
  5. package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
  6. package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
  7. package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
  8. package/dashboard/dist/index.html +6 -6
  9. package/lib/bootstrap.js +1 -1
  10. package/lib/cli/SetupService.js +33 -8
  11. package/lib/cli/UpgradeService.js +139 -2
  12. package/lib/core/ast/ProjectGraph.js +599 -0
  13. package/lib/core/gateway/Gateway.js +19 -4
  14. package/lib/core/gateway/GatewayActionRegistry.js +2 -2
  15. package/lib/domain/recipe/Recipe.js +3 -0
  16. package/lib/external/ai/AiProvider.js +117 -10
  17. package/lib/external/ai/providers/ClaudeProvider.js +197 -0
  18. package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
  19. package/lib/external/ai/providers/OpenAiProvider.js +131 -0
  20. package/lib/external/mcp/McpServer.js +2 -1
  21. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  22. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
  23. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
  24. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  25. package/lib/external/mcp/handlers/bootstrap.js +151 -1634
  26. package/lib/external/mcp/handlers/browse.js +1 -1
  27. package/lib/external/mcp/handlers/candidate.js +1 -33
  28. package/lib/external/mcp/handlers/skill.js +126 -31
  29. package/lib/external/mcp/tools.js +25 -3
  30. package/lib/http/middleware/requestLogger.js +23 -4
  31. package/lib/http/routes/ai.js +3 -1
  32. package/lib/http/routes/auth.js +3 -2
  33. package/lib/http/routes/candidates.js +49 -25
  34. package/lib/http/routes/commands.js +0 -8
  35. package/lib/http/routes/guardRules.js +1 -16
  36. package/lib/http/routes/recipes.js +4 -17
  37. package/lib/http/routes/search.js +16 -22
  38. package/lib/http/routes/skills.js +40 -3
  39. package/lib/http/routes/snippets.js +0 -33
  40. package/lib/http/routes/spm.js +37 -63
  41. package/lib/http/utils/routeHelpers.js +31 -0
  42. package/lib/infrastructure/audit/AuditStore.js +18 -0
  43. package/lib/infrastructure/config/Paths.js +9 -0
  44. package/lib/infrastructure/logging/Logger.js +86 -3
  45. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  46. package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
  47. package/lib/injection/ServiceContainer.js +62 -3
  48. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  49. package/lib/service/candidate/CandidateFileWriter.js +68 -27
  50. package/lib/service/candidate/CandidateService.js +156 -10
  51. package/lib/service/chat/AnalystAgent.js +216 -0
  52. package/lib/service/chat/CandidateGuardrail.js +134 -0
  53. package/lib/service/chat/ChatAgent.js +1272 -155
  54. package/lib/service/chat/ContextWindow.js +730 -0
  55. package/lib/service/chat/ConversationStore.js +377 -0
  56. package/lib/service/chat/HandoffProtocol.js +180 -0
  57. package/lib/service/chat/Memory.js +40 -10
  58. package/lib/service/chat/ProducerAgent.js +240 -0
  59. package/lib/service/chat/ToolRegistry.js +149 -5
  60. package/lib/service/chat/tools.js +1493 -60
  61. package/lib/service/recipe/RecipeFileWriter.js +12 -1
  62. package/lib/service/skills/EventAggregator.js +187 -0
  63. package/lib/service/skills/SignalCollector.js +549 -0
  64. package/lib/service/skills/SkillAdvisor.js +324 -0
  65. package/lib/service/skills/SkillHooks.js +13 -5
  66. package/lib/service/spm/SpmService.js +2 -2
  67. package/package.json +1 -1
  68. package/templates/copilot-instructions.md +20 -3
  69. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  70. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  71. package/dashboard/dist/assets/index-B7VpZOCz.css +0 -1
  72. package/dashboard/dist/assets/index-D87IZTmZ.js +0 -187
@@ -51,8 +51,13 @@ import { ChatAgent } from '../service/chat/ChatAgent.js';
51
51
  import { ALL_TOOLS } from '../service/chat/tools.js';
52
52
  import { SkillHooks } from '../service/skills/SkillHooks.js';
53
53
 
54
+ // ─── v3.0: AST ProjectGraph ──────────────────────────
55
+ import ProjectGraph from '../core/ast/ProjectGraph.js';
56
+
54
57
  // ─── P3: Infrastructure ──────────────────────────────
55
- // EventBus / PluginManager imports removed — source files retained for future use
58
+ import { EventBus } from '../infrastructure/event/EventBus.js';
59
+ import { BootstrapTaskManager } from '../service/bootstrap/BootstrapTaskManager.js';
60
+ import { getRealtimeService as _getRealtimeService } from '../infrastructure/realtime/RealtimeService.js';
56
61
 
57
62
  /**
58
63
  * DependencyInjection 容器
@@ -201,7 +206,26 @@ export class ServiceContainer {
201
206
  return this.singletons.gateway;
202
207
  });
203
208
 
209
+ // EventBus(全局事件总线)
210
+ this.register('eventBus', () => {
211
+ if (!this.singletons.eventBus) {
212
+ this.singletons.eventBus = new EventBus({ maxListeners: 30 });
213
+ }
214
+ return this.singletons.eventBus;
215
+ });
204
216
 
217
+ // BootstrapTaskManager(冷启动异步任务管理器 — 单例)
218
+ this.register('bootstrapTaskManager', () => {
219
+ if (!this.singletons.bootstrapTaskManager) {
220
+ const eventBus = this.get('eventBus');
221
+ // 延迟 getter: RealtimeService 在 HTTP server 启动后才可用,CLI 模式下不可用
222
+ const getRS = () => {
223
+ try { return _getRealtimeService(); } catch { return null; }
224
+ };
225
+ this.singletons.bootstrapTaskManager = new BootstrapTaskManager({ eventBus, getRealtimeService: getRS });
226
+ }
227
+ return this.singletons.bootstrapTaskManager;
228
+ });
205
229
  }
206
230
 
207
231
  /**
@@ -334,11 +358,13 @@ export class ServiceContainer {
334
358
  return this.singletons.retrievalFunnel;
335
359
  });
336
360
 
337
- // JsonVectorAdapter
361
+ // JsonVectorAdapter(同步构造 + 同步 init — 从磁盘 JSON 加载历史向量数据)
338
362
  this.register('vectorStore', () => {
339
363
  if (!this.singletons.vectorStore) {
340
364
  const projectRoot = this.singletons._projectRoot || process.cwd();
341
- this.singletons.vectorStore = new JsonVectorAdapter(projectRoot);
365
+ const store = new JsonVectorAdapter(projectRoot);
366
+ store.initSync(); // 从磁盘加载已有 vector_index.json
367
+ this.singletons.vectorStore = store;
342
368
  }
343
369
  return this.singletons.vectorStore;
344
370
  });
@@ -475,6 +501,12 @@ export class ServiceContainer {
475
501
  return this.singletons.toolRegistry;
476
502
  });
477
503
 
504
+ // ProjectGraph (v3.0 AST 结构图 — 懒初始化,首次 get 时构建)
505
+ this.register('projectGraph', () => {
506
+ // 返回已构建的实例;需要外部先调用 buildProjectGraph() 构建
507
+ return this.singletons.projectGraph || null;
508
+ });
509
+
478
510
  // AI Provider(供 MCP handler / ChatAgent / 任意服务层使用)
479
511
  this.register('aiProvider', () => this.singletons.aiProvider || null);
480
512
 
@@ -531,6 +563,33 @@ export class ServiceContainer {
531
563
  getServiceNames() {
532
564
  return Object.keys(this.services);
533
565
  }
566
+
567
+ /**
568
+ * 构建 ProjectGraph (v3.0 AST 结构图)
569
+ * 应在 bootstrap 流程开始前调用一次
570
+ * @param {string} projectRoot 项目根目录
571
+ * @param {object} [options] 传递给 ProjectGraph.build() 的选项
572
+ * @returns {Promise<import('../core/ast/ProjectGraph.js').default|null>}
573
+ */
574
+ async buildProjectGraph(projectRoot, options = {}) {
575
+ if (this.singletons.projectGraph) {
576
+ return this.singletons.projectGraph;
577
+ }
578
+ try {
579
+ const graph = await ProjectGraph.build(projectRoot, options);
580
+ this.singletons.projectGraph = graph;
581
+ const overview = graph.getOverview();
582
+ this.logger.info(
583
+ `[ServiceContainer] ProjectGraph built: ${overview.totalClasses} classes, ` +
584
+ `${overview.totalProtocols} protocols, ${overview.totalCategories} categories ` +
585
+ `(${overview.buildTimeMs}ms)`
586
+ );
587
+ return graph;
588
+ } catch (err) {
589
+ this.logger.warn(`[ServiceContainer] ProjectGraph build failed: ${err.message}`);
590
+ return null;
591
+ }
592
+ }
534
593
  }
535
594
 
536
595
  let containerInstance = null;
@@ -0,0 +1,400 @@
1
+ /**
2
+ * BootstrapTaskManager — 冷启动异步任务管理器
3
+ *
4
+ * 核心职责:
5
+ * 1. 管理 bootstrap 异步任务的生命周期(skeleton → filling → completed)
6
+ * 2. 通过 EventBus 发射进度事件
7
+ * 3. 通过 RealtimeService 推送进度到前端 (Socket.io)
8
+ * 4. 支持查询当前 bootstrap 会话状态
9
+ *
10
+ * 任务状态流:
11
+ * skeleton → filling → completed / failed
12
+ *
13
+ * 事件类型:
14
+ * bootstrap:started — 冷启动开始,携带任务清单
15
+ * bootstrap:task-started — 单个维度/Skill 开始填充
16
+ * bootstrap:task-completed — 单个维度/Skill 填充完成
17
+ * bootstrap:task-failed — 单个任务失败
18
+ * bootstrap:all-completed — 全部任务完成
19
+ */
20
+
21
+ import Logger from '../../infrastructure/logging/Logger.js';
22
+
23
+ /** 任务状态枚举 */
24
+ export const TaskStatus = Object.freeze({
25
+ SKELETON: 'skeleton', // 骨架已创建,等待填充
26
+ FILLING: 'filling', // 内容正在填充中
27
+ COMPLETED: 'completed', // 填充完成
28
+ FAILED: 'failed', // 填充失败
29
+ });
30
+
31
+ /**
32
+ * 单个 Bootstrap 会话(一次冷启动的全部上下文)
33
+ */
34
+ class BootstrapSession {
35
+ constructor(sessionId) {
36
+ this.id = sessionId;
37
+ this.startedAt = Date.now();
38
+ this.completedAt = null;
39
+ this.status = 'running'; // running | completed | failed
40
+ this.tasks = new Map(); // taskId → TaskInfo
41
+ this.summary = null; // 完成后的摘要
42
+ }
43
+
44
+ addTask(taskId, meta) {
45
+ this.tasks.set(taskId, {
46
+ id: taskId,
47
+ status: TaskStatus.SKELETON,
48
+ meta, // { type: 'dimension'|'skill', dimId, label, skillWorthy }
49
+ startedAt: null,
50
+ completedAt: null,
51
+ result: null, // 填充结果摘要
52
+ error: null,
53
+ });
54
+ }
55
+
56
+ getTask(taskId) {
57
+ return this.tasks.get(taskId);
58
+ }
59
+
60
+ get totalTasks() { return this.tasks.size; }
61
+ get completedTasks() { return [...this.tasks.values()].filter(t => t.status === TaskStatus.COMPLETED).length; }
62
+ get failedTasks() { return [...this.tasks.values()].filter(t => t.status === TaskStatus.FAILED).length; }
63
+ get fillingTasks() { return [...this.tasks.values()].filter(t => t.status === TaskStatus.FILLING).length; }
64
+ get skeletonTasks() { return [...this.tasks.values()].filter(t => t.status === TaskStatus.SKELETON).length; }
65
+
66
+ get isAllDone() {
67
+ return this.skeletonTasks === 0 && this.fillingTasks === 0;
68
+ }
69
+
70
+ get totalToolCalls() {
71
+ let count = 0;
72
+ for (const t of this.tasks.values()) {
73
+ if (t.result?.toolCallCount) count += t.result.toolCallCount;
74
+ }
75
+ return count;
76
+ }
77
+
78
+ get progress() {
79
+ const total = this.totalTasks;
80
+ if (total === 0) return 100;
81
+ return Math.round(((this.completedTasks + this.failedTasks) / total) * 100);
82
+ }
83
+
84
+ toJSON() {
85
+ return {
86
+ id: this.id,
87
+ status: this.status,
88
+ startedAt: this.startedAt,
89
+ completedAt: this.completedAt,
90
+ progress: this.progress,
91
+ total: this.totalTasks,
92
+ completed: this.completedTasks,
93
+ failed: this.failedTasks,
94
+ filling: this.fillingTasks,
95
+ skeleton: this.skeletonTasks,
96
+ totalToolCalls: this.totalToolCalls,
97
+ tasks: [...this.tasks.values()].map(t => ({
98
+ id: t.id,
99
+ status: t.status,
100
+ meta: t.meta,
101
+ startedAt: t.startedAt,
102
+ completedAt: t.completedAt,
103
+ result: t.result,
104
+ error: t.error,
105
+ })),
106
+ summary: this.summary,
107
+ };
108
+ }
109
+ }
110
+
111
+ export class BootstrapTaskManager {
112
+ /** @type {BootstrapSession|null} */
113
+ #currentSession = null;
114
+
115
+ /** @type {import('../../infrastructure/event/EventBus.js').EventBus|null} */
116
+ #eventBus = null;
117
+
118
+ /** @type {Function|null} 获取 RealtimeService 的 getter(延迟获取,避免循环依赖) */
119
+ #getRealtimeService = null;
120
+
121
+ constructor({ eventBus, getRealtimeService } = {}) {
122
+ this.#eventBus = eventBus || null;
123
+ this.#getRealtimeService = getRealtimeService || null;
124
+ }
125
+
126
+ // ═══════════════════════════════════════════════════════════
127
+ // Session 管理
128
+ // ═══════════════════════════════════════════════════════════
129
+
130
+ /**
131
+ * 启动新的 bootstrap 会话
132
+ *
133
+ * 如果上一个会话仍在运行,自动 abort 后再创建新会话(防止重复触发产出重复 Candidate)。
134
+ *
135
+ * @param {Array<{id: string, meta: object}>} taskDefs — 任务定义列表
136
+ * @returns {BootstrapSession}
137
+ */
138
+ startSession(taskDefs) {
139
+ // ── 并发锁:如果上一个 session 还在运行,先中止 ──
140
+ if (this.isRunning) {
141
+ Logger.warn(`[Bootstrap] Previous session ${this.#currentSession.id} still running — aborting before starting new session`);
142
+ this.abortSession('Superseded by new bootstrap request');
143
+ }
144
+
145
+ const sessionId = `bs_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
146
+ this.#currentSession = new BootstrapSession(sessionId);
147
+
148
+ for (const { id, meta } of taskDefs) {
149
+ this.#currentSession.addTask(id, meta);
150
+ }
151
+
152
+ Logger.info(`[Bootstrap] Session ${sessionId} started with ${taskDefs.length} tasks`);
153
+ this.#emit('bootstrap:started', {
154
+ sessionId,
155
+ tasks: taskDefs.map(t => ({ id: t.id, ...t.meta })),
156
+ total: taskDefs.length,
157
+ startedAt: this.#currentSession.startedAt,
158
+ });
159
+
160
+ return this.#currentSession;
161
+ }
162
+
163
+ /**
164
+ * 中止当前 bootstrap 会话
165
+ *
166
+ * 将所有未完成的任务标记为 failed,并将 session 标记为 aborted。
167
+ * 异步填充函数通过 `isSessionValid(sessionId)` 检测到 session 已变更后自动退出。
168
+ *
169
+ * @param {string} [reason='Aborted by user']
170
+ */
171
+ abortSession(reason = 'Aborted by user') {
172
+ const session = this.#currentSession;
173
+ if (!session || session.status !== 'running') return;
174
+
175
+ // 将未完成的任务全部标记 FAILED
176
+ for (const task of session.tasks.values()) {
177
+ if (task.status === TaskStatus.SKELETON || task.status === TaskStatus.FILLING) {
178
+ task.status = TaskStatus.FAILED;
179
+ task.completedAt = Date.now();
180
+ task.error = reason;
181
+ }
182
+ }
183
+
184
+ session.status = 'aborted';
185
+ session.completedAt = Date.now();
186
+ session.summary = {
187
+ duration: session.completedAt - session.startedAt,
188
+ totalTasks: session.totalTasks,
189
+ completed: session.completedTasks,
190
+ failed: session.failedTasks,
191
+ aborted: true,
192
+ reason,
193
+ };
194
+
195
+ Logger.info(`[Bootstrap] Session ${session.id} aborted: ${reason}`);
196
+ this.#emit('bootstrap:all-completed', {
197
+ sessionId: session.id,
198
+ summary: session.summary,
199
+ tasks: [...session.tasks.values()].map(t => ({
200
+ id: t.id, status: t.status, meta: t.meta, result: t.result, error: t.error,
201
+ })),
202
+ });
203
+ }
204
+
205
+ /**
206
+ * 验证 sessionId 是否仍然是活跃 session
207
+ *
208
+ * 用于异步填充函数在每次循环迭代前检测:如果 session 已被新请求覆盖,
209
+ * 则当前异步填充应立即停止,避免产出重复内容。
210
+ *
211
+ * @param {string} sessionId
212
+ * @returns {boolean}
213
+ */
214
+ isSessionValid(sessionId) {
215
+ // Session 在 running 或 completed 状态都有效 — completed 表示维度填充完成,
216
+ // 但 Phase 5.5 Skills 生成仍需运行。只有被新 session 替代时才无效。
217
+ return this.#currentSession?.id === sessionId &&
218
+ (this.#currentSession?.status === 'running' ||
219
+ this.#currentSession?.status === 'completed' ||
220
+ this.#currentSession?.status === 'completed_with_errors');
221
+ }
222
+
223
+ /**
224
+ * 标记单个任务开始填充
225
+ */
226
+ markTaskFilling(taskId) {
227
+ const session = this.#currentSession;
228
+ if (!session) return;
229
+ const task = session.getTask(taskId);
230
+ if (!task) return;
231
+
232
+ task.status = TaskStatus.FILLING;
233
+ task.startedAt = Date.now();
234
+
235
+ Logger.info(`[Bootstrap] Task "${taskId}" filling started`);
236
+ this.#emit('bootstrap:task-started', {
237
+ sessionId: session.id,
238
+ taskId,
239
+ meta: task.meta,
240
+ progress: session.progress,
241
+ });
242
+ }
243
+
244
+ /**
245
+ * 标记单个任务完成
246
+ * @param {string} taskId
247
+ * @param {object} result — 填充结果摘要 { created, items, ... }
248
+ */
249
+ markTaskCompleted(taskId, result = {}) {
250
+ const session = this.#currentSession;
251
+ if (!session) return;
252
+ const task = session.getTask(taskId);
253
+ if (!task) return;
254
+
255
+ task.status = TaskStatus.COMPLETED;
256
+ task.completedAt = Date.now();
257
+ task.result = result;
258
+
259
+ Logger.info(`[Bootstrap] Task "${taskId}" completed (${session.completedTasks}/${session.totalTasks})`);
260
+ this.#emit('bootstrap:task-completed', {
261
+ sessionId: session.id,
262
+ taskId,
263
+ meta: task.meta,
264
+ result,
265
+ progress: session.progress,
266
+ completed: session.completedTasks,
267
+ total: session.totalTasks,
268
+ totalToolCalls: session.totalToolCalls,
269
+ elapsedMs: Date.now() - session.startedAt,
270
+ });
271
+
272
+ // 检查是否所有任务都已完成
273
+ if (session.isAllDone) {
274
+ this.#finishSession();
275
+ }
276
+ }
277
+
278
+ /**
279
+ * 标记单个任务失败
280
+ */
281
+ markTaskFailed(taskId, error) {
282
+ const session = this.#currentSession;
283
+ if (!session) return;
284
+ const task = session.getTask(taskId);
285
+ if (!task) return;
286
+
287
+ task.status = TaskStatus.FAILED;
288
+ task.completedAt = Date.now();
289
+ task.error = typeof error === 'string' ? error : error?.message || 'Unknown error';
290
+
291
+ Logger.warn(`[Bootstrap] Task "${taskId}" failed: ${task.error}`);
292
+ this.#emit('bootstrap:task-failed', {
293
+ sessionId: session.id,
294
+ taskId,
295
+ meta: task.meta,
296
+ error: task.error,
297
+ progress: session.progress,
298
+ });
299
+
300
+ // 检查是否所有任务都已完成(包括失败)
301
+ if (session.isAllDone) {
302
+ this.#finishSession();
303
+ }
304
+ }
305
+
306
+ // ═══════════════════════════════════════════════════════════
307
+ // 查询接口
308
+ // ═══════════════════════════════════════════════════════════
309
+
310
+ /**
311
+ * 获取当前 session 状态(供 HTTP 轮询)
312
+ */
313
+ getSessionStatus() {
314
+ if (!this.#currentSession) {
315
+ return { status: 'idle', message: 'No active bootstrap session' };
316
+ }
317
+ return this.#currentSession.toJSON();
318
+ }
319
+
320
+ /**
321
+ * 是否有正在进行的 bootstrap
322
+ */
323
+ get isRunning() {
324
+ return this.#currentSession?.status === 'running';
325
+ }
326
+
327
+ // ═══════════════════════════════════════════════════════════
328
+ // 通用进度推送(供 refine 等非 bootstrap 流程复用双通道)
329
+ // ═══════════════════════════════════════════════════════════
330
+
331
+ /**
332
+ * 向 EventBus + Socket.io 发射任意进度事件
333
+ *
334
+ * 用途:不走 bootstrap session 模型的长操作(如 AI 润色)也能复用
335
+ * 同一套 EventBus + RealtimeService 双通道推送。
336
+ *
337
+ * @param {string} eventName — 事件名(如 'refine:started')
338
+ * @param {object} data — 事件负载
339
+ */
340
+ emitProgress(eventName, data) {
341
+ this.#emit(eventName, data);
342
+ }
343
+
344
+ // ═══════════════════════════════════════════════════════════
345
+ // 内部方法
346
+ // ═══════════════════════════════════════════════════════════
347
+
348
+ #finishSession() {
349
+ const session = this.#currentSession;
350
+ if (!session) return;
351
+
352
+ session.status = session.failedTasks > 0 ? 'completed_with_errors' : 'completed';
353
+ session.completedAt = Date.now();
354
+ session.summary = {
355
+ duration: session.completedAt - session.startedAt,
356
+ totalTasks: session.totalTasks,
357
+ completed: session.completedTasks,
358
+ failed: session.failedTasks,
359
+ };
360
+
361
+ const durationSec = ((session.completedAt - session.startedAt) / 1000).toFixed(1);
362
+ Logger.info(`[Bootstrap] Session ${session.id} finished: ${session.completedTasks} completed, ${session.failedTasks} failed (${durationSec}s)`);
363
+
364
+ this.#emit('bootstrap:all-completed', {
365
+ sessionId: session.id,
366
+ summary: session.summary,
367
+ tasks: [...session.tasks.values()].map(t => ({
368
+ id: t.id,
369
+ status: t.status,
370
+ meta: t.meta,
371
+ result: t.result,
372
+ error: t.error,
373
+ })),
374
+ });
375
+ }
376
+
377
+ /**
378
+ * 发射事件到 EventBus + 推送到前端 Socket.io
379
+ */
380
+ #emit(eventName, data) {
381
+ // EventBus(供后端监听者使用)
382
+ if (this.#eventBus) {
383
+ try {
384
+ this.#eventBus.emit(eventName, data);
385
+ } catch (e) {
386
+ Logger.warn(`[Bootstrap] EventBus emit failed: ${e.message}`);
387
+ }
388
+ }
389
+
390
+ // RealtimeService(推送到前端 Socket.io)
391
+ if (this.#getRealtimeService) {
392
+ try {
393
+ const realtime = this.#getRealtimeService();
394
+ realtime.broadcastEvent(eventName, data);
395
+ } catch {
396
+ // RealtimeService 可能未初始化(CLI 模式),静默忽略
397
+ }
398
+ }
399
+ }
400
+ }
@@ -79,6 +79,15 @@ export class CandidateFileWriter {
79
79
  if (h !== '[]') lines.push(`_statusHistory: ${h}`);
80
80
  }
81
81
 
82
+ // ── 判断 code 是否为 Markdown 内容(项目特写)──
83
+ // 注意: 只匹配 Markdown heading (# + 空格),避免 ObjC 的 #import/#define 误判
84
+ const isSnapshot = candidate.code &&
85
+ (candidate.code.includes('— 项目特写') || /^#{1,3}\s/.test(candidate.code.trimStart()));
86
+
87
+ if (isSnapshot) {
88
+ lines.push('_format: snapshot');
89
+ }
90
+
82
91
  // _contentHash 占位索引(后续替换为真实 hash)
83
92
  const hashIdx = lines.length;
84
93
  lines.push(''); // 占位行
@@ -86,35 +95,57 @@ export class CandidateFileWriter {
86
95
  lines.push('---');
87
96
  lines.push('');
88
97
 
89
- // ── Title ──
90
- const title = metadata.title || metadata.description || `Candidate ${candidate.id.slice(0, 8)}`;
91
- lines.push(`## ${title}`);
92
- lines.push('');
93
-
94
- // ── Code Block ──
95
- if (candidate.code) {
96
- lines.push(`\`\`\`${candidate.language || 'swift'}`);
97
- lines.push(candidate.code);
98
- lines.push('```');
98
+ if (isSnapshot) {
99
+ // ── 项目特写:Markdown 内容直接输出,不包裹 code fence ──
100
+ // 修复: AI 有时在 code 字段中输出字面 \n 而非真实换行 — 统一转换
101
+ let snapshotCode = candidate.code;
102
+ if (snapshotCode.includes('\\n') && snapshotCode.split('\n').length < 5) {
103
+ snapshotCode = snapshotCode.replace(/\\n/g, '\n');
104
+ }
105
+ lines.push(snapshotCode);
99
106
  lines.push('');
100
- }
101
107
 
102
- // ── Reasoning ──
103
- if (reasoning) {
104
- const r = typeof reasoning === 'string' ? JSON.parse(reasoning) : reasoning;
105
- if (r.whyStandard) {
106
- lines.push('## Why Standard');
107
- lines.push('');
108
- lines.push(r.whyStandard);
108
+ // ── Reasoning → blockquote ──
109
+ if (reasoning) {
110
+ const r = typeof reasoning === 'string' ? JSON.parse(reasoning) : reasoning;
111
+ lines.push('<!-- reasoning -->');
112
+ if (r.whyStandard) {
113
+ lines.push(`> **审核理由**: ${r.whyStandard}`);
114
+ }
115
+ if (r.sources?.length > 0) {
116
+ lines.push(`> **来源**: ${r.sources.join(', ')}`);
117
+ }
109
118
  lines.push('');
110
119
  }
111
- if (r.sources?.length > 0) {
112
- lines.push('## Sources');
120
+ } else {
121
+ // ── 传统代码:保持原有 fence + heading 格式 ──
122
+ const title = metadata.title || metadata.description || `Candidate ${candidate.id.slice(0, 8)}`;
123
+ lines.push(`## ${title}`);
124
+ lines.push('');
125
+
126
+ if (candidate.code) {
127
+ lines.push(`\`\`\`${candidate.language || 'swift'}`);
128
+ lines.push(candidate.code);
129
+ lines.push('```');
113
130
  lines.push('');
114
- for (const src of r.sources) {
115
- lines.push(`- ${src}`);
131
+ }
132
+
133
+ if (reasoning) {
134
+ const r = typeof reasoning === 'string' ? JSON.parse(reasoning) : reasoning;
135
+ if (r.whyStandard) {
136
+ lines.push('## Why Standard');
137
+ lines.push('');
138
+ lines.push(r.whyStandard);
139
+ lines.push('');
140
+ }
141
+ if (r.sources?.length > 0) {
142
+ lines.push('## Sources');
143
+ lines.push('');
144
+ for (const src of r.sources) {
145
+ lines.push(`- ${src}`);
146
+ }
147
+ lines.push('');
116
148
  }
117
- lines.push('');
118
149
  }
119
150
  }
120
151
 
@@ -211,8 +242,10 @@ export class CandidateFileWriter {
211
242
  if (title) {
212
243
  const slug = title
213
244
  .toLowerCase()
214
- .replace(/[^\w\s-]/g, '')
245
+ .replace(/[^\p{L}\p{N}\s-]/gu, '') // 保留 Unicode 字母(含CJK)、数字、空格、连字符
215
246
  .replace(/\s+/g, '-')
247
+ .replace(/-{2,}/g, '-') // 合并连续连字符
248
+ .replace(/^-|-$/g, '') // 去除首尾连字符
216
249
  .slice(0, 60);
217
250
  if (slug.length >= 3) return `${slug}.md`;
218
251
  }
@@ -325,9 +358,17 @@ export function parseCandidateMarkdown(content, relPath) {
325
358
  const bodyMatch = content.match(/^---[\s\S]*?---\s*\r?\n([\s\S]*)$/);
326
359
  if (bodyMatch) {
327
360
  const body = bodyMatch[1];
328
- const codeMatch = body.match(/```\w*\n([\s\S]*?)```/);
329
- if (codeMatch) {
330
- data._bodyCode = codeMatch[1].trimEnd();
361
+
362
+ if (data._format === 'snapshot') {
363
+ // 项目特写格式:Markdown 内容直接输出,reasoning 在 <!-- reasoning --> 之后
364
+ const parts = body.split('<!-- reasoning -->');
365
+ data._bodyCode = parts[0].trim();
366
+ } else {
367
+ // 传统格式:从 code fence 提取
368
+ const codeMatch = body.match(/```\w*\n([\s\S]*?)```/);
369
+ if (codeMatch) {
370
+ data._bodyCode = codeMatch[1].trimEnd();
371
+ }
331
372
  }
332
373
  }
333
374