autosnippet 2.8.3 → 2.10.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 (110) hide show
  1. package/README.md +5 -5
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +9 -27
  11. package/lib/core/ast/ProjectGraph.js +160 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +109 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +125 -0
  19. package/lib/domain/knowledge/values/Content.js +86 -0
  20. package/lib/domain/knowledge/values/Quality.js +93 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  22. package/lib/domain/knowledge/values/Relations.js +168 -0
  23. package/lib/domain/knowledge/values/Stats.js +87 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +48 -0
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
  27. package/lib/external/mcp/McpServer.js +7 -5
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
  29. package/lib/external/mcp/handlers/bootstrap.js +121 -12
  30. package/lib/external/mcp/handlers/browse.js +77 -73
  31. package/lib/external/mcp/handlers/candidate.js +29 -276
  32. package/lib/external/mcp/handlers/guard.js +2 -0
  33. package/lib/external/mcp/handlers/knowledge.js +205 -0
  34. package/lib/external/mcp/handlers/skill.js +4 -2
  35. package/lib/external/mcp/handlers/structure.js +25 -23
  36. package/lib/external/mcp/handlers/system.js +10 -12
  37. package/lib/external/mcp/tools.js +125 -138
  38. package/lib/http/HttpServer.js +4 -8
  39. package/lib/http/middleware/requestLogger.js +3 -3
  40. package/lib/http/routes/ai.js +17 -1
  41. package/lib/http/routes/extract.js +48 -4
  42. package/lib/http/routes/knowledge.js +246 -0
  43. package/lib/http/routes/search.js +12 -17
  44. package/lib/http/routes/skills.js +44 -1
  45. package/lib/infrastructure/cache/GraphCache.js +143 -0
  46. package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
  47. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  48. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  49. package/lib/infrastructure/realtime/RealtimeService.js +14 -2
  50. package/lib/injection/ServiceContainer.js +164 -63
  51. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  52. package/lib/repository/token/TokenUsageStore.js +162 -0
  53. package/lib/service/automation/DirectiveDetector.js +2 -3
  54. package/lib/service/automation/FileWatcher.js +67 -28
  55. package/lib/service/automation/XcodeIntegration.js +931 -156
  56. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  57. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  58. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  59. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  60. package/lib/service/chat/AnalystAgent.js +25 -14
  61. package/lib/service/chat/CandidateGuardrail.js +1 -1
  62. package/lib/service/chat/ChatAgent.js +280 -48
  63. package/lib/service/chat/ContextWindow.js +92 -8
  64. package/lib/service/chat/HandoffProtocol.js +26 -1
  65. package/lib/service/chat/ProducerAgent.js +11 -9
  66. package/lib/service/chat/tools.js +298 -194
  67. package/lib/service/guard/GuardCheckEngine.js +114 -10
  68. package/lib/service/guard/GuardService.js +59 -48
  69. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  70. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  71. package/lib/service/knowledge/KnowledgeService.js +725 -0
  72. package/lib/service/search/SearchEngine.js +92 -19
  73. package/lib/service/skills/SignalCollector.js +15 -9
  74. package/lib/service/skills/SkillAdvisor.js +13 -11
  75. package/lib/service/snippet/SnippetFactory.js +5 -5
  76. package/lib/service/spm/SpmService.js +119 -18
  77. package/package.json +1 -1
  78. package/scripts/install-cursor-skill.js +0 -6
  79. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  80. package/skills/autosnippet-analysis/SKILL.md +15 -7
  81. package/skills/autosnippet-candidates/SKILL.md +6 -6
  82. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  83. package/skills/autosnippet-concepts/SKILL.md +7 -6
  84. package/skills/autosnippet-create/SKILL.md +13 -13
  85. package/skills/autosnippet-intent/SKILL.md +3 -2
  86. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  87. package/skills/autosnippet-recipes/SKILL.md +16 -4
  88. package/templates/constitution.yaml +1 -1
  89. package/templates/copilot-instructions.md +6 -6
  90. package/templates/recipes-setup/README.md +3 -3
  91. package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
  92. package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
  93. package/lib/cli/CandidateSyncService.js +0 -261
  94. package/lib/cli/SyncService.js +0 -356
  95. package/lib/domain/candidate/Candidate.js +0 -196
  96. package/lib/domain/candidate/CandidateRepository.js +0 -107
  97. package/lib/domain/candidate/Reasoning.js +0 -52
  98. package/lib/domain/recipe/Recipe.js +0 -421
  99. package/lib/domain/recipe/RecipeRepository.js +0 -54
  100. package/lib/domain/types/CandidateStatus.js +0 -52
  101. package/lib/http/routes/candidates.js +0 -559
  102. package/lib/http/routes/recipes.js +0 -397
  103. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  104. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  105. package/lib/service/candidate/CandidateAggregator.js +0 -52
  106. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  107. package/lib/service/candidate/CandidateService.js +0 -973
  108. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  109. package/lib/service/recipe/RecipeService.js +0 -786
  110. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -0,0 +1,602 @@
1
+ /**
2
+ * KnowledgeFileWriter — 将 KnowledgeEntry 序列化为 .md 文件 / 从 .md 解析回实体
3
+ *
4
+ * 统一替代 CandidateFileWriter + RecipeFileWriter。
5
+ *
6
+ * 职责:
7
+ * - KnowledgeEntry → YAML frontmatter + Markdown body (serialize)
8
+ * - .md 内容 → wire format JSON → KnowledgeEntry.fromJSON() (parse)
9
+ * - 落盘到 AutoSnippet/{candidates|recipes}/{category}/ 目录
10
+ * - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
11
+ *
12
+ * Frontmatter 分层:
13
+ * - 标量字段(人类可读/可编辑):id, title, lifecycle, language, ...
14
+ * - 简单数组字段(行内 JSON):tags, headers, header_paths
15
+ * - 值对象(_ 前缀,单行 JSON):_content, _relations, _constraints, ...
16
+ *
17
+ * 文件名策略:trigger slug > title slug > id[:8]
18
+ * 落盘目录:isCandidate() → candidates/ | isActive()/deprecated → recipes/
19
+ */
20
+
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import { createHash } from 'node:crypto';
24
+ import { RECIPES_DIR, CANDIDATES_DIR } from '../../infrastructure/config/Defaults.js';
25
+ import Logger from '../../infrastructure/logging/Logger.js';
26
+ import pathGuard from '../../shared/PathGuard.js';
27
+
28
+ /* ═══════════════════════════════════════════════════════════
29
+ * 标量字段定义 — frontmatter 中直接输出为 key: value
30
+ * ═══════════════════════════════════════════════════════════ */
31
+
32
+ const SCALAR_FIELDS = [
33
+ 'id', 'title', 'trigger', 'lifecycle', 'language', 'category',
34
+ 'kind', 'knowledge_type', 'complexity', 'scope', 'difficulty',
35
+ 'summary_cn', 'summary_en', 'usage_guide_cn', 'usage_guide_en',
36
+ 'description', 'source', 'module_name',
37
+ 'created_by', 'created_at', 'updated_at',
38
+ 'published_at', 'published_by', 'reviewed_by', 'reviewed_at',
39
+ 'rejection_reason', 'source_file', 'source_candidate_id',
40
+ ];
41
+
42
+ /* ═══════════════════════════════════════════════════════════
43
+ * KnowledgeFileWriter 类
44
+ * ═══════════════════════════════════════════════════════════ */
45
+
46
+ export class KnowledgeFileWriter {
47
+ /**
48
+ * @param {string} projectRoot 项目根目录
49
+ */
50
+ constructor(projectRoot) {
51
+ this.projectRoot = projectRoot;
52
+ this.recipesDir = path.join(projectRoot, RECIPES_DIR);
53
+ this.candidatesDir = path.join(projectRoot, CANDIDATES_DIR);
54
+ this.logger = Logger.getInstance();
55
+ }
56
+
57
+ /* ═══ 序列化 ═══════════════════════════════════════════ */
58
+
59
+ /**
60
+ * 将 KnowledgeEntry 序列化为完整 .md(YAML frontmatter + body)
61
+ * @param {import('../../domain/knowledge/KnowledgeEntry.js').KnowledgeEntry} entry
62
+ * @returns {string}
63
+ */
64
+ serialize(entry) {
65
+ const json = entry.toJSON();
66
+ const lines = ['---'];
67
+
68
+ // ── 标量字段(人类可读)──
69
+ for (const key of SCALAR_FIELDS) {
70
+ const val = json[key];
71
+ if (val != null && val !== '') {
72
+ lines.push(`${key}: ${_yamlValue(key, val)}`);
73
+ }
74
+ }
75
+
76
+ // ── 简单数组字段(行内 JSON)──
77
+ if (json.tags?.length) lines.push(`tags: ${JSON.stringify(json.tags)}`);
78
+ if (json.headers?.length) lines.push(`headers: ${JSON.stringify(json.headers)}`);
79
+ if (json.header_paths?.length) lines.push(`header_paths: ${JSON.stringify(json.header_paths)}`);
80
+ if (json.include_headers) lines.push(`include_headers: true`);
81
+ if (json.auto_approvable) lines.push(`auto_approvable: true`);
82
+
83
+ // ── JSON 值对象(_ 前缀,单行 JSON)──
84
+ const JSON_FIELDS = [
85
+ ['_content', json.content],
86
+ ['_relations', json.relations],
87
+ ['_constraints', json.constraints],
88
+ ['_reasoning', json.reasoning],
89
+ ['_quality', json.quality],
90
+ ['_stats', json.stats],
91
+ ['_lifecycle_history', json.lifecycle_history],
92
+ ];
93
+ for (const [key, val] of JSON_FIELDS) {
94
+ if (val && typeof val === 'object') {
95
+ // 跳过空对象和空数组
96
+ const hasContent = Array.isArray(val) ? val.length > 0 : Object.keys(val).length > 0;
97
+ if (hasContent) {
98
+ lines.push(`${key}: ${JSON.stringify(val)}`);
99
+ }
100
+ }
101
+ }
102
+ if (json.agent_notes) lines.push(`_agent_notes: ${JSON.stringify(json.agent_notes)}`);
103
+ if (json.ai_insight) lines.push(`_ai_insight: ${JSON.stringify(json.ai_insight)}`);
104
+
105
+ // _content_hash 占位(后续替换为真实 hash)
106
+ const hashPlaceholder = '__HASH_PLACEHOLDER__';
107
+ lines.push(`_content_hash: ${hashPlaceholder}`);
108
+
109
+ lines.push('---');
110
+ lines.push('');
111
+
112
+ // ── Body ──
113
+ lines.push(this._buildBody(entry));
114
+ lines.push('');
115
+
116
+ // ── 计算 content hash 并替换 placeholder ──
117
+ const md = lines.join('\n');
118
+ const cleanedForHash = md.replace(`_content_hash: ${hashPlaceholder}`, '');
119
+ const hash = computeKnowledgeHash(cleanedForHash);
120
+ return md.replace(hashPlaceholder, hash);
121
+ }
122
+
123
+ /**
124
+ * 构建 Markdown body
125
+ * @param {import('../../domain/knowledge/KnowledgeEntry.js').KnowledgeEntry} entry
126
+ * @returns {string}
127
+ */
128
+ _buildBody(entry) {
129
+ const c = entry.content;
130
+ const lines = [];
131
+
132
+ if (c.markdown) {
133
+ // Markdown 项目特写 / 完整文章 → 直接输出(去掉可能残留的 frontmatter)
134
+ const body = c.markdown.replace(/^---[\s\S]*?---\s*/, '').trim();
135
+ lines.push(body);
136
+ } else {
137
+ // 结构化构建
138
+ lines.push(`## ${entry.title}`);
139
+ lines.push('');
140
+
141
+ if (entry.summaryCn) {
142
+ lines.push(`> ${entry.summaryCn}`);
143
+ lines.push('');
144
+ }
145
+
146
+ if (c.pattern) {
147
+ lines.push('```' + (entry.language || 'swift'));
148
+ lines.push(c.pattern);
149
+ lines.push('```');
150
+ lines.push('');
151
+ }
152
+
153
+ if (entry.usageGuideCn) {
154
+ lines.push('## 使用指南');
155
+ lines.push('');
156
+ lines.push(entry.usageGuideCn);
157
+ lines.push('');
158
+ }
159
+
160
+ if (c.rationale) {
161
+ lines.push('## 设计原理');
162
+ lines.push('');
163
+ lines.push(c.rationale);
164
+ lines.push('');
165
+ }
166
+
167
+ if (c.steps?.length > 0) {
168
+ lines.push('## 实施步骤');
169
+ lines.push('');
170
+ for (const [i, step] of c.steps.entries()) {
171
+ if (typeof step === 'string') {
172
+ lines.push(`${i + 1}. ${step}`);
173
+ } else {
174
+ const title = step.title || '步骤';
175
+ const desc = step.description || '';
176
+ lines.push(`${i + 1}. **${title}**: ${desc}`);
177
+ if (step.code) {
178
+ lines.push('');
179
+ lines.push('```');
180
+ lines.push(step.code);
181
+ lines.push('```');
182
+ }
183
+ }
184
+ }
185
+ lines.push('');
186
+ }
187
+
188
+ if (entry.constraints.boundaries?.length > 0) {
189
+ lines.push('## 约束与边界');
190
+ lines.push('');
191
+ for (const b of entry.constraints.boundaries) {
192
+ lines.push(`- ${b}`);
193
+ }
194
+ lines.push('');
195
+ }
196
+
197
+ if (entry.reasoning.whyStandard) {
198
+ lines.push('## Why Standard');
199
+ lines.push('');
200
+ lines.push(entry.reasoning.whyStandard);
201
+ lines.push('');
202
+ }
203
+
204
+ if (entry.reasoning.sources?.length > 0) {
205
+ lines.push('## Sources');
206
+ lines.push('');
207
+ for (const src of entry.reasoning.sources) {
208
+ lines.push(`- ${src}`);
209
+ }
210
+ lines.push('');
211
+ }
212
+ }
213
+
214
+ return lines.join('\n');
215
+ }
216
+
217
+ /* ═══ 文件操作 ═══════════════════════════════════════════ */
218
+
219
+ /**
220
+ * 将 KnowledgeEntry 落盘到对应目录
221
+ * - isCandidate() → AutoSnippet/candidates/{category}/
222
+ * - isActive()/deprecated → AutoSnippet/recipes/{category}/
223
+ *
224
+ * @param {import('../../domain/knowledge/KnowledgeEntry.js').KnowledgeEntry} entry
225
+ * @returns {string|null} 写入的文件路径,失败返回 null
226
+ */
227
+ persist(entry) {
228
+ try {
229
+ if (!entry?.id || !entry?.title) {
230
+ this.logger.warn('Cannot persist knowledge entry: missing id or title');
231
+ return null;
232
+ }
233
+
234
+ const { dir, filename } = this._resolveFilePath(entry);
235
+
236
+ // 路径安全检查
237
+ pathGuard.assertProjectWriteSafe(dir);
238
+
239
+ if (!fs.existsSync(dir)) {
240
+ fs.mkdirSync(dir, { recursive: true });
241
+ }
242
+
243
+ // 清理旧文件(lifecycle 切换或 category 变更场景)
244
+ this._cleanupOldFile(entry, path.join(dir, filename));
245
+
246
+ const filePath = path.join(dir, filename);
247
+ const markdown = this.serialize(entry);
248
+ fs.writeFileSync(filePath, markdown, 'utf8');
249
+
250
+ // 更新 entry 的 sourceFile 溯源
251
+ entry.sourceFile = path.relative(this.projectRoot, filePath);
252
+
253
+ this.logger.info('Knowledge entry persisted to file', {
254
+ entryId: entry.id,
255
+ lifecycle: entry.lifecycle,
256
+ path: entry.sourceFile,
257
+ });
258
+
259
+ return filePath;
260
+ } catch (error) {
261
+ this.logger.error('Failed to persist knowledge entry to file', {
262
+ entryId: entry?.id,
263
+ error: error.message,
264
+ });
265
+ return null;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * 删除 KnowledgeEntry 对应的 .md 文件
271
+ * @param {import('../../domain/knowledge/KnowledgeEntry.js').KnowledgeEntry} entry
272
+ * @returns {boolean}
273
+ */
274
+ remove(entry) {
275
+ if (!entry?.id) return false;
276
+
277
+ // 先尝试 sourceFile 精确删除
278
+ if (entry.sourceFile) {
279
+ const fullPath = path.join(this.projectRoot, entry.sourceFile);
280
+ if (fs.existsSync(fullPath)) {
281
+ fs.unlinkSync(fullPath);
282
+ this.logger.info('Knowledge entry file removed', {
283
+ entryId: entry.id,
284
+ path: entry.sourceFile,
285
+ });
286
+ return true;
287
+ }
288
+ }
289
+
290
+ // fallback: 按文件名在 candidates/ 和 recipes/ 中扫描
291
+ const { filename } = this._resolveFilePath(entry);
292
+ const searchDirs = [
293
+ path.join(this.candidatesDir, (entry.category || 'general').toLowerCase()),
294
+ path.join(this.recipesDir, (entry.category || 'general').toLowerCase()),
295
+ ];
296
+
297
+ for (const dir of searchDirs) {
298
+ const fp = path.join(dir, filename);
299
+ if (fs.existsSync(fp)) {
300
+ fs.unlinkSync(fp);
301
+ this.logger.info('Knowledge entry file removed', { entryId: entry.id, path: fp });
302
+ return true;
303
+ }
304
+ }
305
+
306
+ // 最终 fallback: id 扫描
307
+ return this._removeByIdScan(entry.id);
308
+ }
309
+
310
+ /**
311
+ * 当 lifecycle 切换时,移动 .md 文件到正确目录
312
+ * candidates/ ↔ recipes/
313
+ *
314
+ * @param {import('../../domain/knowledge/KnowledgeEntry.js').KnowledgeEntry} entry
315
+ * @returns {string|null} 新的文件路径
316
+ */
317
+ moveOnLifecycleChange(entry) {
318
+ const oldPath = entry.sourceFile
319
+ ? path.join(this.projectRoot, entry.sourceFile)
320
+ : null;
321
+
322
+ const { dir: newDir, filename } = this._resolveFilePath(entry);
323
+ const newPath = path.join(newDir, filename);
324
+
325
+ // 如果路径没变,直接重新序列化
326
+ if (oldPath && path.resolve(oldPath) === path.resolve(newPath)) {
327
+ return this.persist(entry);
328
+ }
329
+
330
+ // 删除旧文件
331
+ if (oldPath && fs.existsSync(oldPath)) {
332
+ fs.unlinkSync(oldPath);
333
+ this.logger.info('Removed old knowledge entry file on lifecycle change', {
334
+ entryId: entry.id,
335
+ oldPath: entry.sourceFile,
336
+ });
337
+ }
338
+
339
+ // 写入新位置
340
+ return this.persist(entry);
341
+ }
342
+
343
+ /* ═══ 内部工具 ═══════════════════════════════════════════ */
344
+
345
+ /**
346
+ * 计算文件存储路径
347
+ * @returns {{ dir: string, filename: string }}
348
+ */
349
+ _resolveFilePath(entry) {
350
+ const baseDir = entry.isCandidate() ? this.candidatesDir : this.recipesDir;
351
+ const category = (entry.category || 'general').toLowerCase();
352
+ const dir = path.join(baseDir, category);
353
+ const filename = _slugFilename(entry.trigger, entry.title, entry.id);
354
+ return { dir, filename };
355
+ }
356
+
357
+ /**
358
+ * 清理旧文件(category 变更或 lifecycle 切换场景)
359
+ */
360
+ _cleanupOldFile(entry, newPath) {
361
+ if (!entry.sourceFile) return;
362
+ const oldPath = path.join(this.projectRoot, entry.sourceFile);
363
+ if (oldPath !== newPath && fs.existsSync(oldPath)) {
364
+ fs.unlinkSync(oldPath);
365
+ this.logger.info('Cleaned up old knowledge entry file', {
366
+ entryId: entry.id,
367
+ oldPath: entry.sourceFile,
368
+ });
369
+ }
370
+ }
371
+
372
+ /**
373
+ * 通过 id 扫描所有 .md 文件来删除
374
+ * @returns {boolean}
375
+ */
376
+ _removeByIdScan(id) {
377
+ for (const baseDir of [this.candidatesDir, this.recipesDir]) {
378
+ if (!fs.existsSync(baseDir)) continue;
379
+ try {
380
+ const found = _walkAndRemoveById(baseDir, id);
381
+ if (found) {
382
+ this.logger.info('Knowledge entry file removed by id scan', { id });
383
+ return true;
384
+ }
385
+ } catch { /* ignore scan errors */ }
386
+ }
387
+ return false;
388
+ }
389
+ }
390
+
391
+ /* ═══════════════════════════════════════════════════════════
392
+ * 公共工具函数
393
+ * ═══════════════════════════════════════════════════════════ */
394
+
395
+ /**
396
+ * 计算 .md 内容的 SHA-256 hash(去除 _content_hash 行后)
397
+ * @param {string} content
398
+ * @returns {string} 16 字符 hex
399
+ */
400
+ export function computeKnowledgeHash(content) {
401
+ const cleaned = content.replace(/^_content_hash:.*\n?/m, '').trim();
402
+ return createHash('sha256').update(cleaned, 'utf8').digest('hex').slice(0, 16);
403
+ }
404
+
405
+ /**
406
+ * 从 .md 内容解析为 wire format JSON
407
+ * 返回值可直接 KnowledgeEntry.fromJSON(data) 构造实体
408
+ *
409
+ * @param {string} content .md 文件全文
410
+ * @param {string} [relPath] 相对路径(用于溯源)
411
+ * @returns {Object} wire format JSON
412
+ */
413
+ export function parseKnowledgeMarkdown(content, relPath) {
414
+ const fmMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
415
+ const data = {};
416
+
417
+ if (fmMatch) {
418
+ const fmLines = fmMatch[1].split('\n');
419
+
420
+ for (let i = 0; i < fmLines.length; i++) {
421
+ const line = fmLines[i];
422
+ const colonIdx = line.indexOf(':');
423
+ if (colonIdx <= 0) continue;
424
+
425
+ const key = line.slice(0, colonIdx).trim();
426
+ // 跳过带空格的非正常 key
427
+ if (/\s/.test(key)) continue;
428
+
429
+ let value = line.slice(colonIdx + 1).trim();
430
+
431
+ // ── _ 前缀字段:统一去掉 _ 前缀存入 data ──
432
+ if (key.startsWith('_')) {
433
+ const dataKey = key.slice(1); // _content → content, _ai_insight → ai_insight
434
+
435
+ // JSON 对象/数组值
436
+ if (value.startsWith('{') || value.startsWith('[')) {
437
+ try {
438
+ data[dataKey] = JSON.parse(value);
439
+ continue;
440
+ } catch {
441
+ // 可能是跨多行的 JSON — 尝试拼接后续行
442
+ let jsonStr = value;
443
+ while (i + 1 < fmLines.length) {
444
+ i++;
445
+ jsonStr += fmLines[i];
446
+ try {
447
+ data[dataKey] = JSON.parse(jsonStr);
448
+ break;
449
+ } catch { /* continue concatenating */ }
450
+ }
451
+ continue;
452
+ }
453
+ }
454
+
455
+ // JSON 字符串值(如 _ai_insight: "text")
456
+ if (value.startsWith('"')) {
457
+ try {
458
+ data[dataKey] = JSON.parse(value);
459
+ continue;
460
+ } catch { /* fall through to plain string */ }
461
+ }
462
+
463
+ // 纯标量值(如 _content_hash: abc123)
464
+ if (/^\d+$/.test(value)) { data[dataKey] = parseInt(value, 10); continue; }
465
+ if (/^\d+\.\d+$/.test(value)) { data[dataKey] = parseFloat(value); continue; }
466
+ if (value === 'true') { data[dataKey] = true; continue; }
467
+ if (value === 'false') { data[dataKey] = false; continue; }
468
+ data[dataKey] = value;
469
+ continue;
470
+ }
471
+
472
+ // ── 布尔 ──
473
+ if (value === 'true') { data[key] = true; continue; }
474
+ if (value === 'false') { data[key] = false; continue; }
475
+
476
+ // ── 数值(整数或浮点) ──
477
+ if (/^\d+$/.test(value)) { data[key] = parseInt(value, 10); continue; }
478
+ if (/^\d+\.\d+$/.test(value)) { data[key] = parseFloat(value); continue; }
479
+
480
+ // ── JSON 数组(非 _ 前缀) ──
481
+ if (value.startsWith('[')) {
482
+ try { data[key] = JSON.parse(value); continue; } catch { /* fallthrough */ }
483
+ }
484
+
485
+ // ── 去引号 ──
486
+ if (/^".*"$/.test(value)) {
487
+ value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, '\n');
488
+ }
489
+
490
+ data[key] = value;
491
+ }
492
+ }
493
+
494
+ // ── 从 body 提取信息 ──
495
+ const bodyMatch = content.match(/^---[\s\S]*?---\s*\r?\n([\s\S]*)$/);
496
+ if (bodyMatch) {
497
+ const body = bodyMatch[1].trim();
498
+
499
+ // 如果 content 中没有 pattern,从 body 代码块提取
500
+ if (!data.content?.pattern) {
501
+ const codeMatch = body.match(/```\w*\n([\s\S]*?)```/);
502
+ if (codeMatch) {
503
+ data.content = data.content || {};
504
+ data.content.pattern = codeMatch[1].trimEnd();
505
+ }
506
+ }
507
+
508
+ // 如果 content 中没有 markdown 且 body 看起来是 Markdown 文章
509
+ if (!data.content?.markdown && !data.content?.pattern) {
510
+ const isMarkdownArticle = body.includes('— 项目特写') ||
511
+ (body.startsWith('#') && body.length > 200);
512
+ if (isMarkdownArticle) {
513
+ data.content = data.content || {};
514
+ data.content.markdown = body;
515
+ }
516
+ }
517
+ }
518
+
519
+ // ── 元数据补充 ──
520
+ if (relPath) {
521
+ data.source_file = relPath;
522
+ }
523
+
524
+ // ── fallback: title 从 body heading 提取 ──
525
+ if (!data.title) {
526
+ const headingMatch = content.match(/^##?\s+(.+)$/m);
527
+ if (headingMatch) data.title = headingMatch[1].trim();
528
+ }
529
+
530
+ return data;
531
+ }
532
+
533
+ /* ═══ 私有辅助 ═══════════════════════════════════════════ */
534
+
535
+ /**
536
+ * 生成文件名 slug
537
+ * @param {string} trigger
538
+ * @param {string} title
539
+ * @param {string} id
540
+ * @returns {string} 文件名(含 .md 后缀)
541
+ */
542
+ function _slugFilename(trigger, title, id) {
543
+ // 优先用 trigger
544
+ if (trigger) {
545
+ const clean = trigger
546
+ .replace(/^@/, '')
547
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
548
+ .slice(0, 60);
549
+ if (clean.length >= 2) return `${clean}.md`;
550
+ }
551
+
552
+ // 其次用 title
553
+ if (title) {
554
+ const slug = title
555
+ .toLowerCase()
556
+ .replace(/[^\p{L}\p{N}\s-]/gu, '')
557
+ .replace(/\s+/g, '-')
558
+ .replace(/-{2,}/g, '-')
559
+ .replace(/^-|-$/g, '')
560
+ .slice(0, 60);
561
+ if (slug.length >= 3) return `${slug}.md`;
562
+ }
563
+
564
+ // 最后用 id 前 8 位
565
+ return `${(id || 'unknown').slice(0, 8)}.md`;
566
+ }
567
+
568
+ /**
569
+ * 将 YAML 值安全序列化
570
+ */
571
+ function _yamlValue(key, val) {
572
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val);
573
+ const str = String(val);
574
+ // 含特殊字符时加引号
575
+ if (/[:#\[\]{}&*!|>'"`,@\n]/.test(str) || str.trim() !== str) {
576
+ return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
577
+ }
578
+ return str;
579
+ }
580
+
581
+ /**
582
+ * 递归扫描目录,删除包含指定 id 的 .md 文件
583
+ * @returns {boolean}
584
+ */
585
+ function _walkAndRemoveById(dir, id) {
586
+ if (!fs.existsSync(dir)) return false;
587
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
588
+ const full = path.join(dir, entry.name);
589
+ if (entry.isDirectory()) {
590
+ if (_walkAndRemoveById(full, id)) return true;
591
+ } else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
592
+ const head = fs.readFileSync(full, 'utf8').slice(0, 500);
593
+ if (head.includes(`id: ${id}`)) {
594
+ fs.unlinkSync(full);
595
+ return true;
596
+ }
597
+ }
598
+ }
599
+ return false;
600
+ }
601
+
602
+ export default KnowledgeFileWriter;