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.
- package/README.md +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +16 -4
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -973
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CandidateFileWriter — 将 Candidate 领域对象序列化为标准 .md 文件
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* - Candidate domain → YAML frontmatter + Markdown body
|
|
6
|
-
* - 落盘到 AutoSnippet/candidates/{category}/ 目录
|
|
7
|
-
* - .md 文件可 Git 合并,使团队共享 Candidate 数据
|
|
8
|
-
*
|
|
9
|
-
* Frontmatter 分层:
|
|
10
|
-
* - 基础字段(人类可读):id, status, language, category, source, createdBy, ...
|
|
11
|
-
* - 审核字段:approvedBy, rejectedBy, rejectionReason, appliedRecipeId
|
|
12
|
-
* - 机器字段(_ 前缀):_reasoning, _metadata, _statusHistory, _contentHash
|
|
13
|
-
*
|
|
14
|
-
* 文件名策略:metadata.title slug > id 前 8 位
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import { createHash } from 'node:crypto';
|
|
20
|
-
import { CANDIDATES_DIR } from '../../infrastructure/config/Defaults.js';
|
|
21
|
-
import Logger from '../../infrastructure/logging/Logger.js';
|
|
22
|
-
import pathGuard from '../../shared/PathGuard.js';
|
|
23
|
-
|
|
24
|
-
export { CANDIDATES_DIR };
|
|
25
|
-
|
|
26
|
-
export class CandidateFileWriter {
|
|
27
|
-
/**
|
|
28
|
-
* @param {string} projectRoot 项目根目录
|
|
29
|
-
*/
|
|
30
|
-
constructor(projectRoot) {
|
|
31
|
-
this.projectRoot = projectRoot;
|
|
32
|
-
this.candidatesDir = path.join(projectRoot, CANDIDATES_DIR);
|
|
33
|
-
this.logger = Logger.getInstance();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/* ═══ 序列化 ═══════════════════════════════════════════ */
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 将 Candidate 领域对象序列化为完整 .md(YAML frontmatter + body)
|
|
40
|
-
* @param {object} candidate
|
|
41
|
-
* @returns {string}
|
|
42
|
-
*/
|
|
43
|
-
serializeToMarkdown(candidate) {
|
|
44
|
-
const lines = ['---'];
|
|
45
|
-
|
|
46
|
-
// ── 基础字段(人类可读)──
|
|
47
|
-
lines.push(`id: ${candidate.id}`);
|
|
48
|
-
lines.push(`status: ${candidate.status || 'pending'}`);
|
|
49
|
-
lines.push(`language: ${candidate.language || 'swift'}`);
|
|
50
|
-
lines.push(`category: ${candidate.category || 'general'}`);
|
|
51
|
-
lines.push(`source: ${candidate.source || 'manual'}`);
|
|
52
|
-
|
|
53
|
-
// ── 审核信息 ──
|
|
54
|
-
if (candidate.approvedBy) lines.push(`approvedBy: ${candidate.approvedBy}`);
|
|
55
|
-
if (candidate.approvedAt) lines.push(`approvedAt: ${candidate.approvedAt}`);
|
|
56
|
-
if (candidate.rejectedBy) lines.push(`rejectedBy: ${candidate.rejectedBy}`);
|
|
57
|
-
if (candidate.rejectionReason) lines.push(`rejectionReason: ${this.#yamlStr(candidate.rejectionReason)}`);
|
|
58
|
-
if (candidate.appliedRecipeId) lines.push(`appliedRecipeId: ${candidate.appliedRecipeId}`);
|
|
59
|
-
|
|
60
|
-
// ── 时间 ──
|
|
61
|
-
lines.push(`createdBy: ${candidate.createdBy || 'system'}`);
|
|
62
|
-
lines.push(`createdAt: ${candidate.createdAt || Math.floor(Date.now() / 1000)}`);
|
|
63
|
-
lines.push(`updatedAt: ${candidate.updatedAt || Math.floor(Date.now() / 1000)}`);
|
|
64
|
-
|
|
65
|
-
// ── 机器管理字段(_ 前缀,单行 JSON)──
|
|
66
|
-
const reasoning = candidate.reasoning || candidate.reasoning_json;
|
|
67
|
-
if (reasoning) {
|
|
68
|
-
const r = typeof reasoning === 'string' ? reasoning : JSON.stringify(reasoning);
|
|
69
|
-
lines.push(`_reasoning: ${r}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const metadata = candidate.metadata || {};
|
|
73
|
-
if (Object.keys(metadata).length > 0) {
|
|
74
|
-
lines.push(`_metadata: ${JSON.stringify(metadata)}`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const history = candidate.statusHistory || candidate.status_history_json;
|
|
78
|
-
if (history) {
|
|
79
|
-
const h = typeof history === 'string' ? history : JSON.stringify(history);
|
|
80
|
-
if (h !== '[]') lines.push(`_statusHistory: ${h}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ── 判断 code 是否为 Markdown 内容(项目特写)──
|
|
84
|
-
// 注意: 只匹配 Markdown heading (# + 空格),避免 ObjC 的 #import/#define 误判
|
|
85
|
-
const isSnapshot = candidate.code &&
|
|
86
|
-
(candidate.code.includes('— 项目特写') || /^#{1,3}\s/.test(candidate.code.trimStart()));
|
|
87
|
-
|
|
88
|
-
if (isSnapshot) {
|
|
89
|
-
lines.push('_format: snapshot');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// _contentHash 占位索引(后续替换为真实 hash)
|
|
93
|
-
const hashIdx = lines.length;
|
|
94
|
-
lines.push(''); // 占位行
|
|
95
|
-
|
|
96
|
-
lines.push('---');
|
|
97
|
-
lines.push('');
|
|
98
|
-
|
|
99
|
-
if (isSnapshot) {
|
|
100
|
-
// ── 项目特写:Markdown 内容直接输出,不包裹 code fence ──
|
|
101
|
-
// 修复: AI 有时在 code 字段中输出字面 \n 而非真实换行 — 统一转换
|
|
102
|
-
let snapshotCode = candidate.code;
|
|
103
|
-
if (snapshotCode.includes('\\n') && snapshotCode.split('\n').length < 5) {
|
|
104
|
-
snapshotCode = snapshotCode.replace(/\\n/g, '\n');
|
|
105
|
-
}
|
|
106
|
-
lines.push(snapshotCode);
|
|
107
|
-
lines.push('');
|
|
108
|
-
|
|
109
|
-
// ── Reasoning → blockquote ──
|
|
110
|
-
if (reasoning) {
|
|
111
|
-
const r = typeof reasoning === 'string' ? JSON.parse(reasoning) : reasoning;
|
|
112
|
-
lines.push('<!-- reasoning -->');
|
|
113
|
-
if (r.whyStandard) {
|
|
114
|
-
lines.push(`> **审核理由**: ${r.whyStandard}`);
|
|
115
|
-
}
|
|
116
|
-
if (r.sources?.length > 0) {
|
|
117
|
-
lines.push(`> **来源**: ${r.sources.join(', ')}`);
|
|
118
|
-
}
|
|
119
|
-
lines.push('');
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
// ── 传统代码:保持原有 fence + heading 格式 ──
|
|
123
|
-
const title = metadata.title || metadata.description || `Candidate ${candidate.id.slice(0, 8)}`;
|
|
124
|
-
lines.push(`## ${title}`);
|
|
125
|
-
lines.push('');
|
|
126
|
-
|
|
127
|
-
if (candidate.code) {
|
|
128
|
-
lines.push(`\`\`\`${candidate.language || 'swift'}`);
|
|
129
|
-
lines.push(candidate.code);
|
|
130
|
-
lines.push('```');
|
|
131
|
-
lines.push('');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (reasoning) {
|
|
135
|
-
const r = typeof reasoning === 'string' ? JSON.parse(reasoning) : reasoning;
|
|
136
|
-
if (r.whyStandard) {
|
|
137
|
-
lines.push('## Why Standard');
|
|
138
|
-
lines.push('');
|
|
139
|
-
lines.push(r.whyStandard);
|
|
140
|
-
lines.push('');
|
|
141
|
-
}
|
|
142
|
-
if (r.sources?.length > 0) {
|
|
143
|
-
lines.push('## Sources');
|
|
144
|
-
lines.push('');
|
|
145
|
-
for (const src of r.sources) {
|
|
146
|
-
lines.push(`- ${src}`);
|
|
147
|
-
}
|
|
148
|
-
lines.push('');
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ── 计算 content hash ──
|
|
154
|
-
const linesForHash = [...lines];
|
|
155
|
-
linesForHash.splice(hashIdx, 1);
|
|
156
|
-
const hash = computeCandidateHash(linesForHash.join('\n'));
|
|
157
|
-
lines[hashIdx] = `_contentHash: ${hash}`;
|
|
158
|
-
return lines.join('\n');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/* ═══ 文件操作 ═══════════════════════════════════════════ */
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* 将 Candidate 落盘到 AutoSnippet/candidates/{category}/ 目录
|
|
165
|
-
* @param {object} candidate
|
|
166
|
-
* @returns {string|null} 写入的文件路径,失败返回 null
|
|
167
|
-
*/
|
|
168
|
-
persistCandidate(candidate) {
|
|
169
|
-
try {
|
|
170
|
-
if (!candidate?.id) {
|
|
171
|
-
this.logger.warn('Cannot persist candidate: missing id');
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const filename = this.#getFilename(candidate);
|
|
176
|
-
const category = (candidate.category || 'general').toLowerCase();
|
|
177
|
-
const categoryDir = path.join(this.candidatesDir, category);
|
|
178
|
-
|
|
179
|
-
// 路径安全检查 — 阻止 category 含 ../ 导致路径逃逸
|
|
180
|
-
pathGuard.assertProjectWriteSafe(categoryDir);
|
|
181
|
-
|
|
182
|
-
if (!fs.existsSync(categoryDir)) {
|
|
183
|
-
fs.mkdirSync(categoryDir, { recursive: true });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 检查是否需要清理旧文件(category 变更场景)
|
|
187
|
-
this.#cleanupOldFile(candidate, path.join(categoryDir, filename));
|
|
188
|
-
|
|
189
|
-
const filePath = path.join(categoryDir, filename);
|
|
190
|
-
const markdown = this.serializeToMarkdown(candidate);
|
|
191
|
-
fs.writeFileSync(filePath, markdown, 'utf8');
|
|
192
|
-
|
|
193
|
-
this.logger.info('Candidate persisted to file', {
|
|
194
|
-
candidateId: candidate.id,
|
|
195
|
-
path: path.relative(this.projectRoot, filePath),
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
return filePath;
|
|
199
|
-
} catch (error) {
|
|
200
|
-
this.logger.warn('Failed to persist candidate to file', {
|
|
201
|
-
candidateId: candidate?.id,
|
|
202
|
-
error: error.message,
|
|
203
|
-
});
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* 删除 Candidate 对应的 .md 文件
|
|
210
|
-
* @param {object} candidate
|
|
211
|
-
* @returns {boolean}
|
|
212
|
-
*/
|
|
213
|
-
removeCandidate(candidate) {
|
|
214
|
-
try {
|
|
215
|
-
if (!candidate?.id) return false;
|
|
216
|
-
|
|
217
|
-
const filename = this.#getFilename(candidate);
|
|
218
|
-
const category = (candidate.category || 'general').toLowerCase();
|
|
219
|
-
const filePath = path.join(this.candidatesDir, category, filename);
|
|
220
|
-
|
|
221
|
-
if (fs.existsSync(filePath)) {
|
|
222
|
-
fs.unlinkSync(filePath);
|
|
223
|
-
this.logger.info('Candidate file removed', {
|
|
224
|
-
candidateId: candidate.id,
|
|
225
|
-
path: path.relative(this.projectRoot, filePath),
|
|
226
|
-
});
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// fallback: scan by id
|
|
231
|
-
return this.#removeById(candidate.id);
|
|
232
|
-
} catch (error) {
|
|
233
|
-
this.logger.warn('Failed to remove candidate file', {
|
|
234
|
-
candidateId: candidate?.id,
|
|
235
|
-
error: error.message,
|
|
236
|
-
});
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/* ═══ Private helpers ═══════════════════════════════════ */
|
|
242
|
-
|
|
243
|
-
#getFilename(candidate) {
|
|
244
|
-
const meta = candidate.metadata || {};
|
|
245
|
-
const title = meta.title || meta.description || '';
|
|
246
|
-
if (title) {
|
|
247
|
-
const slug = title
|
|
248
|
-
.toLowerCase()
|
|
249
|
-
.replace(/[^\p{L}\p{N}\s-]/gu, '') // 保留 Unicode 字母(含CJK)、数字、空格、连字符
|
|
250
|
-
.replace(/\s+/g, '-')
|
|
251
|
-
.replace(/-{2,}/g, '-') // 合并连续连字符
|
|
252
|
-
.replace(/^-|-$/g, '') // 去除首尾连字符
|
|
253
|
-
.slice(0, 60);
|
|
254
|
-
if (slug.length >= 3) return `${slug}.md`;
|
|
255
|
-
}
|
|
256
|
-
return `${candidate.id.slice(0, 8)}.md`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
#cleanupOldFile(candidate, newPath) {
|
|
260
|
-
// scan all category dirs for a file with matching id in frontmatter
|
|
261
|
-
if (!fs.existsSync(this.candidatesDir)) return;
|
|
262
|
-
try {
|
|
263
|
-
const categories = fs.readdirSync(this.candidatesDir, { withFileTypes: true })
|
|
264
|
-
.filter(d => d.isDirectory())
|
|
265
|
-
.map(d => d.name);
|
|
266
|
-
|
|
267
|
-
for (const cat of categories) {
|
|
268
|
-
const catDir = path.join(this.candidatesDir, cat);
|
|
269
|
-
const files = fs.readdirSync(catDir).filter(f => f.endsWith('.md'));
|
|
270
|
-
for (const file of files) {
|
|
271
|
-
const fp = path.join(catDir, file);
|
|
272
|
-
if (fp === newPath) continue;
|
|
273
|
-
const head = fs.readFileSync(fp, 'utf8').slice(0, 500);
|
|
274
|
-
if (head.includes(`id: ${candidate.id}`)) {
|
|
275
|
-
fs.unlinkSync(fp);
|
|
276
|
-
this.logger.info('Cleaned up old candidate file', { old: fp });
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
} catch { /* ignore scan errors */ }
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
#removeById(id) {
|
|
284
|
-
if (!fs.existsSync(this.candidatesDir)) return false;
|
|
285
|
-
try {
|
|
286
|
-
const categories = fs.readdirSync(this.candidatesDir, { withFileTypes: true })
|
|
287
|
-
.filter(d => d.isDirectory())
|
|
288
|
-
.map(d => d.name);
|
|
289
|
-
|
|
290
|
-
for (const cat of categories) {
|
|
291
|
-
const catDir = path.join(this.candidatesDir, cat);
|
|
292
|
-
const files = fs.readdirSync(catDir).filter(f => f.endsWith('.md'));
|
|
293
|
-
for (const file of files) {
|
|
294
|
-
const fp = path.join(catDir, file);
|
|
295
|
-
const head = fs.readFileSync(fp, 'utf8').slice(0, 500);
|
|
296
|
-
if (head.includes(`id: ${id}`)) {
|
|
297
|
-
fs.unlinkSync(fp);
|
|
298
|
-
return true;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} catch { /* ignore */ }
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
#yamlStr(str) {
|
|
307
|
-
if (!str) return '""';
|
|
308
|
-
if (/[:"'{}\[\]#&*!|>%@`]/.test(str) || str.trim() !== str) {
|
|
309
|
-
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
310
|
-
}
|
|
311
|
-
return str;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/* ═══ Standalone hash utility ═════════════════════════════ */
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* 计算 Candidate .md 内容的 SHA-256 hash(去除 _contentHash 行后)
|
|
319
|
-
* @param {string} content
|
|
320
|
-
* @returns {string} 16 字符 hex
|
|
321
|
-
*/
|
|
322
|
-
export function computeCandidateHash(content) {
|
|
323
|
-
const cleaned = content.replace(/^_contentHash:.*\n?/m, '').trim();
|
|
324
|
-
return createHash('sha256').update(cleaned, 'utf8').digest('hex').slice(0, 16);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* 从 Candidate .md 内容解析 frontmatter
|
|
329
|
-
* @param {string} content .md 文件全文
|
|
330
|
-
* @param {string} relPath 相对路径(用于日志)
|
|
331
|
-
* @returns {object} 解析后的 candidate 数据
|
|
332
|
-
*/
|
|
333
|
-
export function parseCandidateMarkdown(content, relPath) {
|
|
334
|
-
const fmMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
335
|
-
const data = {};
|
|
336
|
-
|
|
337
|
-
if (fmMatch) {
|
|
338
|
-
for (const line of fmMatch[1].split('\n')) {
|
|
339
|
-
const colonIdx = line.indexOf(':');
|
|
340
|
-
if (colonIdx <= 0) continue;
|
|
341
|
-
const key = line.slice(0, colonIdx).trim();
|
|
342
|
-
let value = line.slice(colonIdx + 1).trim();
|
|
343
|
-
|
|
344
|
-
// 单行 JSON(_ 前缀机器字段)
|
|
345
|
-
if (key.startsWith('_') && (value.startsWith('{') || value.startsWith('['))) {
|
|
346
|
-
try { value = JSON.parse(value); } catch { /* keep as string */ }
|
|
347
|
-
}
|
|
348
|
-
// 数值
|
|
349
|
-
else if (/^\d+$/.test(value)) {
|
|
350
|
-
value = parseInt(value, 10);
|
|
351
|
-
}
|
|
352
|
-
// 去引号
|
|
353
|
-
else if (/^".*"$/.test(value)) {
|
|
354
|
-
value = value.slice(1, -1);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
data[key] = value;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// 提取 body 中的 code block
|
|
362
|
-
const bodyMatch = content.match(/^---[\s\S]*?---\s*\r?\n([\s\S]*)$/);
|
|
363
|
-
if (bodyMatch) {
|
|
364
|
-
const body = bodyMatch[1];
|
|
365
|
-
|
|
366
|
-
if (data._format === 'snapshot') {
|
|
367
|
-
// 项目特写格式:Markdown 内容直接输出,reasoning 在 <!-- reasoning --> 之后
|
|
368
|
-
const parts = body.split('<!-- reasoning -->');
|
|
369
|
-
data._bodyCode = parts[0].trim();
|
|
370
|
-
} else {
|
|
371
|
-
// 传统格式:从 code fence 提取
|
|
372
|
-
const codeMatch = body.match(/```\w*\n([\s\S]*?)```/);
|
|
373
|
-
if (codeMatch) {
|
|
374
|
-
data._bodyCode = codeMatch[1].trimEnd();
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
data._sourceFile = relPath;
|
|
380
|
-
return data;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export default CandidateFileWriter;
|