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
|
@@ -15,6 +15,8 @@ const BM25_B = 0.75;
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* 分词: 中英文混合分词
|
|
18
|
+
* 英文: camelCase / PascalCase 拆分 + 小写化
|
|
19
|
+
* 中文: 单字 + 二元组(bigram)— 无需分词词典即可支持子串匹配
|
|
18
20
|
*/
|
|
19
21
|
export function tokenize(text) {
|
|
20
22
|
if (!text) return [];
|
|
@@ -23,7 +25,35 @@ export function tokenize(text) {
|
|
|
23
25
|
// 拆全大写前缀:URLSession → URL Session, UITableView → UI Table View
|
|
24
26
|
expanded = expanded.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
25
27
|
const normalized = expanded.toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, ' ');
|
|
26
|
-
const
|
|
28
|
+
const rawTokens = normalized.split(/[\s_-]+/).filter(t => t.length >= 1);
|
|
29
|
+
|
|
30
|
+
const tokens = [];
|
|
31
|
+
// CJK 正则(中日韩统一表意文字 + 扩展区)
|
|
32
|
+
const cjkRe = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/;
|
|
33
|
+
|
|
34
|
+
for (const raw of rawTokens) {
|
|
35
|
+
if (cjkRe.test(raw)) {
|
|
36
|
+
// 中文片段:提取所有 CJK 连续子串,生成单字 + bigram 覆盖
|
|
37
|
+
const cjkChars = raw.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]+/g) || [];
|
|
38
|
+
for (const seg of cjkChars) {
|
|
39
|
+
// 单字
|
|
40
|
+
for (const ch of seg) tokens.push(ch);
|
|
41
|
+
// bigram
|
|
42
|
+
for (let i = 0; i < seg.length - 1; i++) tokens.push(seg[i] + seg[i + 1]);
|
|
43
|
+
// 完整片段(≥3 字时额外保留,提升精确匹配权重)
|
|
44
|
+
if (seg.length >= 3) tokens.push(seg);
|
|
45
|
+
}
|
|
46
|
+
// 非 CJK 部分(英文/数字)也保留
|
|
47
|
+
const nonCjk = raw.replace(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]+/g, ' ').trim();
|
|
48
|
+
if (nonCjk) {
|
|
49
|
+
for (const t of nonCjk.split(/\s+/)) {
|
|
50
|
+
if (t.length >= 2) tokens.push(t);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else if (raw.length >= 2) {
|
|
54
|
+
tokens.push(raw);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
return [...new Set(tokens)];
|
|
28
58
|
}
|
|
29
59
|
|
|
@@ -126,13 +156,33 @@ export class SearchEngine {
|
|
|
126
156
|
this._cache.clear();
|
|
127
157
|
|
|
128
158
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
let entries = [];
|
|
160
|
+
|
|
161
|
+
// V3: 优先从 knowledge_entries 表索引
|
|
162
|
+
try {
|
|
163
|
+
entries = this.db.prepare(
|
|
164
|
+
`SELECT id, title, description, language, category, knowledge_type, kind,
|
|
165
|
+
content AS content_json, lifecycle, tags AS tags_json, trigger_key
|
|
166
|
+
FROM knowledge_entries WHERE lifecycle != 'deprecated'`
|
|
167
|
+
).all();
|
|
168
|
+
// 标准化字段名以便后续处理
|
|
169
|
+
entries = entries.map(e => ({
|
|
170
|
+
...e,
|
|
171
|
+
trigger: e.trigger_key,
|
|
172
|
+
status: e.lifecycle,
|
|
173
|
+
}));
|
|
174
|
+
} catch {
|
|
175
|
+
// V3 表不存在,回退到 V2 recipes 表
|
|
176
|
+
try {
|
|
177
|
+
entries = this.db.prepare(
|
|
178
|
+
`SELECT id, title, description, language, category, knowledge_type, kind,
|
|
179
|
+
content_json, status, tags_json, "trigger"
|
|
180
|
+
FROM recipes WHERE status != 'deprecated'`
|
|
181
|
+
).all();
|
|
182
|
+
} catch { /* neither table exists */ }
|
|
183
|
+
}
|
|
134
184
|
|
|
135
|
-
for (const r of
|
|
185
|
+
for (const r of entries) {
|
|
136
186
|
let contentText = '';
|
|
137
187
|
try {
|
|
138
188
|
const content = JSON.parse(r.content_json || '{}');
|
|
@@ -143,12 +193,12 @@ export class SearchEngine {
|
|
|
143
193
|
try { tagText = JSON.parse(r.tags_json || '[]').join(' '); } catch { /* ignore */ }
|
|
144
194
|
const text = [r.title, r.description, r.trigger, r.language, r.category, r.knowledge_type, tagText, contentText]
|
|
145
195
|
.filter(Boolean).join(' ');
|
|
146
|
-
this.scorer.addDocument(r.id, text, { type: '
|
|
196
|
+
this.scorer.addDocument(r.id, text, { type: 'knowledge', title: r.title, trigger: r.trigger || '', status: r.status, knowledgeType: r.knowledge_type, kind: r.kind || 'pattern', language: r.language || '', category: r.category || '' });
|
|
147
197
|
}
|
|
148
198
|
|
|
149
199
|
this._indexed = true;
|
|
150
200
|
this.logger.info('Search index built', {
|
|
151
|
-
|
|
201
|
+
entries: entries.length,
|
|
152
202
|
total: this.scorer.totalDocs,
|
|
153
203
|
});
|
|
154
204
|
} catch (err) {
|
|
@@ -229,14 +279,26 @@ export class SearchEngine {
|
|
|
229
279
|
const escaped = query.replace(/[%_\\]/g, ch => `\\${ch}`);
|
|
230
280
|
const pattern = `%${escaped}%`;
|
|
231
281
|
|
|
232
|
-
if (type === 'all' || type === 'recipe' || type === 'rule' || type === 'solution') {
|
|
282
|
+
if (type === 'all' || type === 'recipe' || type === 'knowledge' || type === 'rule' || type === 'solution') {
|
|
233
283
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
284
|
+
let rows = [];
|
|
285
|
+
// V3: 优先 knowledge_entries
|
|
286
|
+
try {
|
|
287
|
+
rows = this.db.prepare(
|
|
288
|
+
`SELECT id, title, description, language, category, knowledge_type, kind, lifecycle as status, content AS content_json, trigger_key as "trigger", headers, module_name, 'knowledge' as type
|
|
289
|
+
FROM knowledge_entries
|
|
290
|
+
WHERE lifecycle != 'deprecated' AND (title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR trigger_key LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\')
|
|
291
|
+
LIMIT ?`
|
|
292
|
+
).all(pattern, pattern, pattern, pattern, limit);
|
|
293
|
+
} catch {
|
|
294
|
+
// V3 表不存在,回退到 V2
|
|
295
|
+
rows = this.db.prepare(
|
|
296
|
+
`SELECT id, title, description, language, category, knowledge_type, kind, status, content_json, dimensions_json, "trigger", 'recipe' as type
|
|
297
|
+
FROM recipes
|
|
298
|
+
WHERE status != 'deprecated' AND (title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR "trigger" LIKE ? ESCAPE '\\' OR content_json LIKE ? ESCAPE '\\')
|
|
299
|
+
LIMIT ?`
|
|
300
|
+
).all(pattern, pattern, pattern, pattern, limit);
|
|
301
|
+
}
|
|
240
302
|
// 基础相关性排序:trigger 精确 > 标题匹配 > 描述匹配 > 内容匹配
|
|
241
303
|
const lowerQ = query.toLowerCase();
|
|
242
304
|
results.push(...rows.map(r => {
|
|
@@ -349,9 +411,17 @@ export class SearchEngine {
|
|
|
349
411
|
try {
|
|
350
412
|
const ids = items.map(it => it.id);
|
|
351
413
|
const placeholders = ids.map(() => '?').join(',');
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
414
|
+
let rows = [];
|
|
415
|
+
// V3: 优先 knowledge_entries
|
|
416
|
+
try {
|
|
417
|
+
rows = this.db.prepare(
|
|
418
|
+
`SELECT id, content AS content_json, description, trigger_key as "trigger", headers, module_name FROM knowledge_entries WHERE id IN (${placeholders})`
|
|
419
|
+
).all(...ids);
|
|
420
|
+
} catch {
|
|
421
|
+
rows = this.db.prepare(
|
|
422
|
+
`SELECT id, content_json, dimensions_json, description, "trigger" FROM recipes WHERE id IN (${placeholders})`
|
|
423
|
+
).all(...ids);
|
|
424
|
+
}
|
|
355
425
|
const rowMap = new Map(rows.map(r => [r.id, r]));
|
|
356
426
|
for (const item of items) {
|
|
357
427
|
const row = rowMap.get(item.id);
|
|
@@ -360,6 +430,9 @@ export class SearchEngine {
|
|
|
360
430
|
item.dimensions_json = row.dimensions_json || null;
|
|
361
431
|
item.description = item.description || row.description || '';
|
|
362
432
|
item.trigger = item.trigger || row.trigger || '';
|
|
433
|
+
// V3: headers & module_name 是独立列
|
|
434
|
+
if (row.headers) item.headers = row.headers;
|
|
435
|
+
if (row.module_name) item.module_name = row.module_name;
|
|
363
436
|
}
|
|
364
437
|
}
|
|
365
438
|
} catch { /* DB may not be available */ }
|
|
@@ -128,8 +128,9 @@ export class SignalCollector {
|
|
|
128
128
|
`[SignalCollector] started — mode=${this.#mode}, initialInterval=${this.#intervalMs}ms, AI-driven`
|
|
129
129
|
);
|
|
130
130
|
|
|
131
|
-
//
|
|
132
|
-
|
|
131
|
+
// 首次按正常间隔执行(不立即触发,避免启动时消耗 AI token)
|
|
132
|
+
// 如果有事件推送(EventAggregator batch),会提前触发
|
|
133
|
+
this.#timer = setTimeout(() => this.#tick(), this.#intervalMs);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
stop() {
|
|
@@ -335,11 +336,16 @@ export class SignalCollector {
|
|
|
335
336
|
#collectRecipeSignals() {
|
|
336
337
|
try {
|
|
337
338
|
if (!this.#db) return [];
|
|
339
|
+
// V3: knowledge_entries 统一表,统计字段在 stats/quality JSON 中
|
|
338
340
|
const rows = this.#db.prepare(
|
|
339
341
|
`SELECT id, title, knowledge_type, category, language,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
342
|
+
json_extract(stats, '$.adoptions') as adoption_count,
|
|
343
|
+
json_extract(stats, '$.applications') as application_count,
|
|
344
|
+
json_extract(quality, '$.overall') as quality_overall,
|
|
345
|
+
updated_at
|
|
346
|
+
FROM knowledge_entries
|
|
347
|
+
WHERE lifecycle = 'active'
|
|
348
|
+
ORDER BY updated_at DESC LIMIT 30`
|
|
343
349
|
).all();
|
|
344
350
|
return rows;
|
|
345
351
|
} catch { return []; }
|
|
@@ -348,11 +354,11 @@ export class SignalCollector {
|
|
|
348
354
|
#collectCandidateSignals() {
|
|
349
355
|
try {
|
|
350
356
|
if (!this.#db) return [];
|
|
357
|
+
// V3: candidates 已合并到 knowledge_entries,lifecycle='pending' 即为候选
|
|
351
358
|
const rows = this.#db.prepare(
|
|
352
|
-
`SELECT id, source, status, language, category,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
FROM candidates WHERE status = 'pending'
|
|
359
|
+
`SELECT id, source, lifecycle as status, language, category,
|
|
360
|
+
title, created_at
|
|
361
|
+
FROM knowledge_entries WHERE lifecycle = 'pending'
|
|
356
362
|
ORDER BY created_at DESC LIMIT 30`
|
|
357
363
|
).all();
|
|
358
364
|
return rows;
|
|
@@ -202,8 +202,8 @@ export class SkillAdvisor {
|
|
|
202
202
|
// 按 category 分布
|
|
203
203
|
const categories = this.#db.prepare(`
|
|
204
204
|
SELECT category, COUNT(*) as cnt
|
|
205
|
-
FROM
|
|
206
|
-
WHERE category IS NOT NULL AND category != ''
|
|
205
|
+
FROM knowledge_entries
|
|
206
|
+
WHERE lifecycle = 'active' AND category IS NOT NULL AND category != ''
|
|
207
207
|
GROUP BY category
|
|
208
208
|
ORDER BY cnt DESC
|
|
209
209
|
`).all();
|
|
@@ -211,8 +211,8 @@ export class SkillAdvisor {
|
|
|
211
211
|
// 按 language 分布
|
|
212
212
|
const languages = this.#db.prepare(`
|
|
213
213
|
SELECT language, COUNT(*) as cnt
|
|
214
|
-
FROM
|
|
215
|
-
WHERE language IS NOT NULL AND language != ''
|
|
214
|
+
FROM knowledge_entries
|
|
215
|
+
WHERE lifecycle = 'active' AND language IS NOT NULL AND language != ''
|
|
216
216
|
GROUP BY language
|
|
217
217
|
ORDER BY cnt DESC
|
|
218
218
|
`).all();
|
|
@@ -231,14 +231,15 @@ export class SkillAdvisor {
|
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// 高使用量 Recipe 统计(
|
|
234
|
+
// 高使用量 Recipe 统计(V3: stats JSON 中的 adoptions + applications)
|
|
235
235
|
let hotRecipes = [];
|
|
236
236
|
try {
|
|
237
237
|
hotRecipes = this.#db.prepare(`
|
|
238
238
|
SELECT title, category,
|
|
239
|
-
(
|
|
240
|
-
FROM
|
|
241
|
-
WHERE
|
|
239
|
+
(COALESCE(json_extract(stats, '$.adoptions'), 0) + COALESCE(json_extract(stats, '$.applications'), 0)) as total_usage
|
|
240
|
+
FROM knowledge_entries
|
|
241
|
+
WHERE lifecycle = 'active'
|
|
242
|
+
AND (COALESCE(json_extract(stats, '$.adoptions'), 0) + COALESCE(json_extract(stats, '$.applications'), 0)) >= 5
|
|
242
243
|
ORDER BY total_usage DESC
|
|
243
244
|
LIMIT 10
|
|
244
245
|
`).all();
|
|
@@ -262,12 +263,13 @@ export class SkillAdvisor {
|
|
|
262
263
|
if (!this.#db) return { summary: 'DB 不可用', suggestions };
|
|
263
264
|
|
|
264
265
|
try {
|
|
266
|
+
// V3: candidates 已合并到 knowledge_entries,ifecycle='pending' 即为候选
|
|
265
267
|
const stats = this.#db.prepare(`
|
|
266
268
|
SELECT
|
|
267
269
|
COUNT(*) as total,
|
|
268
|
-
SUM(CASE WHEN
|
|
269
|
-
SUM(CASE WHEN
|
|
270
|
-
FROM
|
|
270
|
+
SUM(CASE WHEN lifecycle='pending' THEN 1 ELSE 0 END) as pending,
|
|
271
|
+
SUM(CASE WHEN lifecycle='deprecated' THEN 1 ELSE 0 END) as rejected
|
|
272
|
+
FROM knowledge_entries
|
|
271
273
|
`).get();
|
|
272
274
|
|
|
273
275
|
// 大量被拒绝 → 提示候选质量 Skill
|
|
@@ -45,16 +45,16 @@ const SNIPPET_TEMPLATE = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
45
45
|
|
|
46
46
|
export class SnippetFactory {
|
|
47
47
|
/**
|
|
48
|
-
* @param {object} [
|
|
48
|
+
* @param {object} [knowledgeRepository] — KnowledgeRepositoryImpl(可选,用于列表查询)
|
|
49
49
|
*/
|
|
50
|
-
constructor(
|
|
51
|
-
this._recipeRepo =
|
|
50
|
+
constructor(knowledgeRepository) {
|
|
51
|
+
this._recipeRepo = knowledgeRepository || null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* 运行时注入
|
|
55
|
+
* 运行时注入 knowledgeRepository(用于延迟绑定场景)
|
|
56
56
|
*/
|
|
57
|
-
|
|
57
|
+
setKnowledgeRepository(repo) {
|
|
58
58
|
this._recipeRepo = repo;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -7,8 +7,9 @@ import Logger from '../../infrastructure/logging/Logger.js';
|
|
|
7
7
|
import { PackageSwiftParser } from './PackageSwiftParser.js';
|
|
8
8
|
import { DependencyGraph } from './DependencyGraph.js';
|
|
9
9
|
import { PolicyEngine } from './PolicyEngine.js';
|
|
10
|
+
import { GraphCache } from '../../infrastructure/cache/GraphCache.js';
|
|
10
11
|
import { dirname, relative, sep, resolve as pathResolve } from 'node:path';
|
|
11
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
12
13
|
|
|
13
14
|
export class SpmService {
|
|
14
15
|
#parser;
|
|
@@ -35,6 +36,12 @@ export class SpmService {
|
|
|
35
36
|
*/
|
|
36
37
|
#packageDepGraph;
|
|
37
38
|
|
|
39
|
+
/** @type {GraphCache} 磁盘缓存层 */
|
|
40
|
+
#graphCache;
|
|
41
|
+
|
|
42
|
+
/** @type {Map<string, object[]>} target 文件列表缓存 */
|
|
43
|
+
#targetFilesCache;
|
|
44
|
+
|
|
38
45
|
constructor(projectRoot, options = {}) {
|
|
39
46
|
this.#projectRoot = projectRoot;
|
|
40
47
|
this.#aiFactory = options.aiFactory || null;
|
|
@@ -49,60 +56,92 @@ export class SpmService {
|
|
|
49
56
|
this.#logger = Logger.getInstance();
|
|
50
57
|
this.#targetPackageMap = new Map();
|
|
51
58
|
this.#packageDepGraph = new Map();
|
|
59
|
+
this.#graphCache = new GraphCache(projectRoot);
|
|
60
|
+
this.#targetFilesCache = new Map();
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/**
|
|
55
64
|
* 加载并解析 Package.swift,构建依赖图
|
|
56
65
|
* 支持多 Package 项目(如 BiliDemo 有多个子 Package)
|
|
66
|
+
* 优先从磁盘缓存加载(Package.swift contentHash 匹配即命中)
|
|
57
67
|
*/
|
|
58
68
|
async load() {
|
|
59
69
|
this.#targetPackageMap.clear();
|
|
60
70
|
this.#packageDepGraph.clear();
|
|
71
|
+
this.#targetFilesCache.clear();
|
|
61
72
|
|
|
62
|
-
//
|
|
73
|
+
// ── 收集所有 Package.swift 路径 + 联合 hash ──
|
|
63
74
|
const packagePath = this.#parser.findPackageSwift(this.#projectRoot);
|
|
75
|
+
const allPaths = packagePath ? [packagePath] : this.#parser.findAllPackageSwifts(this.#projectRoot);
|
|
76
|
+
if (allPaths.length === 0) {
|
|
77
|
+
this.#logger.warn('[SpmService] Package.swift 未找到');
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const combinedHash = allPaths
|
|
82
|
+
.map(p => this.#graphCache.computeFileHash(p))
|
|
83
|
+
.join(':');
|
|
84
|
+
|
|
85
|
+
// ── 尝试命中缓存 ──
|
|
86
|
+
const cached = this.#graphCache.load('spm-graph');
|
|
87
|
+
if (cached && cached.contentHash === combinedHash) {
|
|
88
|
+
this.#restoreFromCache(cached.data);
|
|
89
|
+
this.#logger.info(`[SpmService] ⚡ 缓存命中 (${this.#graph.getNodes().length} targets, hash=${combinedHash.substring(0, 8)})`);
|
|
90
|
+
return cached.data.parsedResult;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── 缓存未命中,走完整解析 ──
|
|
94
|
+
const startTime = Date.now();
|
|
95
|
+
let parsedResult;
|
|
96
|
+
|
|
64
97
|
if (packagePath) {
|
|
98
|
+
// 单包模式
|
|
65
99
|
const parsed = this.#parser.parse(packagePath);
|
|
66
100
|
this.#graph.buildFromParsed(parsed);
|
|
67
|
-
// 构建 target→package 映射
|
|
68
101
|
for (const t of parsed.targets || []) {
|
|
69
102
|
this.#targetPackageMap.set(t.name, { packageName: parsed.name, packagePath });
|
|
70
103
|
}
|
|
71
|
-
// 构建包级依赖图(解析 .package(path: "...") 引用)
|
|
72
104
|
this.#buildPackageDepGraph([{ path: packagePath, parsed }]);
|
|
73
105
|
this.#logger.info(`[SpmService] 加载完成: ${parsed.name} (${parsed.targets.length} targets)`);
|
|
74
|
-
|
|
106
|
+
parsedResult = parsed;
|
|
107
|
+
} else {
|
|
108
|
+
// 多包模式
|
|
109
|
+
parsedResult = this.#loadMultiPackage(allPaths);
|
|
75
110
|
}
|
|
76
111
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.#logger.
|
|
81
|
-
return null;
|
|
112
|
+
// ── 写入缓存 ──
|
|
113
|
+
if (parsedResult) {
|
|
114
|
+
this.#saveToCache(combinedHash, parsedResult);
|
|
115
|
+
this.#logger.info(`[SpmService] 缓存已写入 (${Date.now() - startTime}ms 解析)`);
|
|
82
116
|
}
|
|
83
117
|
|
|
118
|
+
return parsedResult;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 多包加载(从 load() 拆出)
|
|
123
|
+
* @param {string[]} allPaths Package.swift 路径数组
|
|
124
|
+
* @returns {object|null}
|
|
125
|
+
*/
|
|
126
|
+
#loadMultiPackage(allPaths) {
|
|
84
127
|
this.#logger.info(`[SpmService] 发现 ${allPaths.length} 个 Package.swift,逐一解析...`);
|
|
85
128
|
let mergedTargets = [];
|
|
86
129
|
let lastName = 'multi-package';
|
|
87
130
|
const allParsed = [];
|
|
88
131
|
|
|
89
|
-
// 先清空图,然后逐个添加节点和边(不用 buildFromParsed 避免重复 clear)
|
|
90
132
|
this.#graph.clear();
|
|
91
133
|
for (const pkgPath of allPaths) {
|
|
92
134
|
try {
|
|
93
135
|
const parsed = this.#parser.parse(pkgPath);
|
|
94
136
|
if (parsed) {
|
|
95
137
|
allParsed.push({ path: pkgPath, parsed });
|
|
96
|
-
// 逐个添加 target 到图中
|
|
97
138
|
for (const t of parsed.targets || []) {
|
|
98
139
|
this.#graph.addNode(t.name);
|
|
99
140
|
for (const dep of t.dependencies || []) {
|
|
100
141
|
this.#graph.addEdge(t.name, dep);
|
|
101
142
|
}
|
|
102
|
-
// 构建 target→package 映射
|
|
103
143
|
this.#targetPackageMap.set(t.name, { packageName: parsed.name, packagePath: pkgPath });
|
|
104
144
|
}
|
|
105
|
-
// 合并 target 列表(带 packageName 标记)
|
|
106
145
|
for (const t of parsed.targets) {
|
|
107
146
|
mergedTargets.push({
|
|
108
147
|
...t,
|
|
@@ -117,7 +156,6 @@ export class SpmService {
|
|
|
117
156
|
}
|
|
118
157
|
}
|
|
119
158
|
|
|
120
|
-
// 构建包级依赖图
|
|
121
159
|
this.#buildPackageDepGraph(allParsed);
|
|
122
160
|
|
|
123
161
|
this.#logger.info(`[SpmService] 多包加载完成: ${mergedTargets.length} targets from ${allPaths.length} packages`);
|
|
@@ -128,6 +166,51 @@ export class SpmService {
|
|
|
128
166
|
};
|
|
129
167
|
}
|
|
130
168
|
|
|
169
|
+
/**
|
|
170
|
+
* 将当前内存状态序列化到缓存
|
|
171
|
+
*/
|
|
172
|
+
#saveToCache(contentHash, parsedResult) {
|
|
173
|
+
const graphJSON = this.#graph.toJSON();
|
|
174
|
+
const targetPackageEntries = [...this.#targetPackageMap.entries()];
|
|
175
|
+
const packageDepEntries = [...this.#packageDepGraph.entries()].map(
|
|
176
|
+
([k, v]) => [k, [...v]]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
this.#graphCache.save('spm-graph', {
|
|
180
|
+
parsedResult,
|
|
181
|
+
graphNodes: graphJSON.nodes,
|
|
182
|
+
graphEdges: graphJSON.edges,
|
|
183
|
+
targetPackageMap: targetPackageEntries,
|
|
184
|
+
packageDepGraph: packageDepEntries,
|
|
185
|
+
}, { contentHash });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 从缓存数据恢复内存状态
|
|
190
|
+
*/
|
|
191
|
+
#restoreFromCache(data) {
|
|
192
|
+
// 恢复 DependencyGraph
|
|
193
|
+
this.#graph.clear();
|
|
194
|
+
for (const node of data.graphNodes || []) {
|
|
195
|
+
this.#graph.addNode(node);
|
|
196
|
+
}
|
|
197
|
+
for (const edge of data.graphEdges || []) {
|
|
198
|
+
this.#graph.addEdge(edge.from, edge.to);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 恢复 targetPackageMap
|
|
202
|
+
this.#targetPackageMap.clear();
|
|
203
|
+
for (const [name, info] of data.targetPackageMap || []) {
|
|
204
|
+
this.#targetPackageMap.set(name, info);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 恢复 packageDepGraph
|
|
208
|
+
this.#packageDepGraph.clear();
|
|
209
|
+
for (const [pkgPath, deps] of data.packageDepGraph || []) {
|
|
210
|
+
this.#packageDepGraph.set(pkgPath, new Set(deps));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
131
214
|
// ─────────────── 包级依赖图构建 ───────────────
|
|
132
215
|
|
|
133
216
|
/**
|
|
@@ -585,14 +668,29 @@ export class SpmService {
|
|
|
585
668
|
* 获取 Target 源文件列表(路由: POST /spm/target-files)
|
|
586
669
|
* 支持多 Package 项目:先在 target 所属 package 目录查找
|
|
587
670
|
* 支持非 SPM 项目:虚拟 directory target 直接扫描对应目录
|
|
671
|
+
* 使用内存缓存避免重复 fs walk
|
|
588
672
|
*/
|
|
589
673
|
async getTargetFiles(target) {
|
|
590
|
-
const { existsSync, readdirSync, statSync } = await import('fs');
|
|
591
|
-
const { join, dirname, relative, extname } = await import('path');
|
|
592
|
-
|
|
593
674
|
const targetName = typeof target === 'string' ? target : target?.name;
|
|
594
675
|
if (!targetName) return [];
|
|
595
676
|
|
|
677
|
+
// 内存缓存命中
|
|
678
|
+
if (this.#targetFilesCache.has(targetName)) {
|
|
679
|
+
return this.#targetFilesCache.get(targetName);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const result = await this.#walkTargetFiles(target, targetName);
|
|
683
|
+
this.#targetFilesCache.set(targetName, result);
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* 实际的文件遍历逻辑(拆出以供缓存用)
|
|
689
|
+
*/
|
|
690
|
+
async #walkTargetFiles(target, targetName) {
|
|
691
|
+
const { existsSync, readdirSync, statSync } = await import('fs');
|
|
692
|
+
const { join, dirname, relative, extname } = await import('path');
|
|
693
|
+
|
|
596
694
|
// 判断是否为虚拟目录 target(非 SPM fallback)
|
|
597
695
|
const isDirectoryTarget = typeof target === 'object' && target?.type === 'directory';
|
|
598
696
|
|
|
@@ -1103,8 +1201,11 @@ export class SpmService {
|
|
|
1103
1201
|
|
|
1104
1202
|
/**
|
|
1105
1203
|
* 刷新依赖映射(路由: POST /commands/spm-map)
|
|
1204
|
+
* 会主动使缓存失效并重新解析
|
|
1106
1205
|
*/
|
|
1107
1206
|
async updateDependencyMap(options = {}) {
|
|
1207
|
+
this.#graphCache.invalidate('spm-graph');
|
|
1208
|
+
this.#targetFilesCache.clear();
|
|
1108
1209
|
this.#graph.clear();
|
|
1109
1210
|
const parsed = await this.load();
|
|
1110
1211
|
if (!parsed) {
|
package/package.json
CHANGED
|
@@ -22,7 +22,6 @@ const require = createRequire(import.meta.url);
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import * as defaults from '../lib/infrastructure/config/Defaults.js';
|
|
25
|
-
import { RecipeStatsTracker } from '../lib/service/recipe/RecipeStatsTracker.js';
|
|
26
25
|
|
|
27
26
|
const autoSnippetRoot = path.join(__dirname, '..');
|
|
28
27
|
const skillsSource = path.join(autoSnippetRoot, 'skills');
|
|
@@ -156,11 +155,6 @@ function buildProjectRecipesContext(projectRoot) {
|
|
|
156
155
|
if (!fs.existsSync(recipesDir)) return null;
|
|
157
156
|
const mdFiles = collectMdFiles(recipesDir, recipesDir).sort();
|
|
158
157
|
if (mdFiles.length === 0) return null;
|
|
159
|
-
let stats = { byFile: {} };
|
|
160
|
-
try {
|
|
161
|
-
const tracker = new RecipeStatsTracker(projectRoot);
|
|
162
|
-
stats = tracker.getStats();
|
|
163
|
-
} catch (_) {}
|
|
164
158
|
|
|
165
159
|
const lines = [
|
|
166
160
|
'# Project Recipes Index\n\n',
|