autosnippet 2.6.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 (66) hide show
  1. package/bin/cli.js +1 -1
  2. package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-Cq4-iQhP.js} +148 -88
  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/GatewayActionRegistry.js +2 -2
  14. package/lib/domain/recipe/Recipe.js +3 -0
  15. package/lib/external/ai/AiProvider.js +83 -20
  16. package/lib/external/ai/providers/ClaudeProvider.js +197 -0
  17. package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
  18. package/lib/external/ai/providers/OpenAiProvider.js +131 -0
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  20. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
  21. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
  22. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  23. package/lib/external/mcp/handlers/bootstrap.js +151 -1634
  24. package/lib/external/mcp/handlers/browse.js +1 -1
  25. package/lib/external/mcp/handlers/candidate.js +1 -33
  26. package/lib/external/mcp/handlers/skill.js +54 -17
  27. package/lib/external/mcp/tools.js +4 -3
  28. package/lib/http/middleware/requestLogger.js +23 -4
  29. package/lib/http/routes/ai.js +3 -1
  30. package/lib/http/routes/auth.js +3 -2
  31. package/lib/http/routes/candidates.js +49 -25
  32. package/lib/http/routes/commands.js +0 -8
  33. package/lib/http/routes/guardRules.js +1 -16
  34. package/lib/http/routes/recipes.js +4 -17
  35. package/lib/http/routes/search.js +11 -19
  36. package/lib/http/routes/skills.js +2 -0
  37. package/lib/http/routes/snippets.js +0 -33
  38. package/lib/http/routes/spm.js +37 -63
  39. package/lib/http/utils/routeHelpers.js +31 -0
  40. package/lib/infrastructure/config/Paths.js +9 -0
  41. package/lib/infrastructure/logging/Logger.js +86 -3
  42. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  43. package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
  44. package/lib/injection/ServiceContainer.js +55 -2
  45. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  46. package/lib/service/candidate/CandidateFileWriter.js +68 -27
  47. package/lib/service/candidate/CandidateService.js +156 -10
  48. package/lib/service/chat/AnalystAgent.js +216 -0
  49. package/lib/service/chat/CandidateGuardrail.js +134 -0
  50. package/lib/service/chat/ChatAgent.js +1036 -167
  51. package/lib/service/chat/ContextWindow.js +730 -0
  52. package/lib/service/chat/HandoffProtocol.js +180 -0
  53. package/lib/service/chat/ProducerAgent.js +240 -0
  54. package/lib/service/chat/ToolRegistry.js +149 -5
  55. package/lib/service/chat/tools.js +1397 -61
  56. package/lib/service/recipe/RecipeFileWriter.js +12 -1
  57. package/lib/service/skills/SignalCollector.js +31 -6
  58. package/lib/service/skills/SkillAdvisor.js +2 -1
  59. package/lib/service/skills/SkillHooks.js +13 -5
  60. package/lib/service/spm/SpmService.js +2 -2
  61. package/package.json +1 -1
  62. package/templates/copilot-instructions.md +20 -3
  63. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  64. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  65. package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
  66. package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
@@ -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