autosnippet 2.8.2 → 2.9.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 +1 -1
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-CH-H9x0E.js} +1 -1
- package/dashboard/dist/assets/index-CqJRvYRL.js +197 -0
- package/dashboard/dist/assets/index-DICm9PNa.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/SetupService.js +1 -1
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +1 -0
- package/lib/external/mcp/handlers/bootstrap.js +33 -36
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/system.js +3 -3
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- 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/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +114 -2
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/candidate/CandidateService.js +28 -0
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/ChatAgent.js +237 -6
- package/lib/service/chat/ContextWindow.js +87 -3
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +4 -2
- package/lib/service/chat/tools.js +168 -71
- package/lib/service/skills/SignalCollector.js +3 -2
- package/lib/service/spm/SpmService.js +119 -18
- package/package.json +1 -1
- package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
|
@@ -123,30 +123,133 @@ function _scoreSearchLine(line) {
|
|
|
123
123
|
return 0;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* 收集项目文件列表 — 抽取为公用函数,供单次和批量搜索复用。
|
|
128
|
+
* 优先使用内存缓存(bootstrap 场景),否则从磁盘递归读取。
|
|
129
|
+
*/
|
|
130
|
+
async function _getProjectFiles(params, ctx) {
|
|
131
|
+
const { fileFilter } = params;
|
|
132
|
+
const projectRoot = ctx.projectRoot || process.cwd();
|
|
133
|
+
|
|
134
|
+
let extFilter = null;
|
|
135
|
+
if (fileFilter) {
|
|
136
|
+
const exts = fileFilter.split(',').map(e => e.trim().replace(/^\./, ''));
|
|
137
|
+
extFilter = new RegExp(`\\.(${exts.join('|')})$`, 'i');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fileCache = ctx.fileCache || null;
|
|
141
|
+
let files;
|
|
142
|
+
let skippedThirdParty = 0;
|
|
143
|
+
|
|
144
|
+
if (fileCache && Array.isArray(fileCache)) {
|
|
145
|
+
files = fileCache.filter(f => {
|
|
146
|
+
const p = f.relativePath || f.path || '';
|
|
147
|
+
if (THIRD_PARTY_RE.test(p)) { skippedThirdParty++; return false; }
|
|
148
|
+
if (extFilter && !extFilter.test(p)) return false;
|
|
149
|
+
if (!SOURCE_EXT_RE.test(p)) return false;
|
|
150
|
+
return true;
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
files = [];
|
|
154
|
+
const MAX_FILE_SIZE = 512 * 1024;
|
|
155
|
+
const walk = (dir, relBase = '') => {
|
|
156
|
+
try {
|
|
157
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
160
|
+
const fullPath = path.join(dir, entry.name);
|
|
161
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && (() => { try { return fs.statSync(fullPath).isDirectory(); } catch { return false; } })());
|
|
162
|
+
const isFile = entry.isFile() || (entry.isSymbolicLink() && (() => { try { return fs.statSync(fullPath).isFile(); } catch { return false; } })());
|
|
163
|
+
if (isDir) {
|
|
164
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'build') continue;
|
|
165
|
+
if (THIRD_PARTY_RE.test(relPath + '/')) { skippedThirdParty++; continue; }
|
|
166
|
+
walk(fullPath, relPath);
|
|
167
|
+
} else if (isFile) {
|
|
168
|
+
if (THIRD_PARTY_RE.test(relPath)) { skippedThirdParty++; continue; }
|
|
169
|
+
if (!SOURCE_EXT_RE.test(entry.name)) continue;
|
|
170
|
+
if (extFilter && !extFilter.test(entry.name)) continue;
|
|
171
|
+
try {
|
|
172
|
+
const stat = fs.statSync(fullPath);
|
|
173
|
+
if (stat.size > MAX_FILE_SIZE) continue;
|
|
174
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
175
|
+
files.push({ relativePath: relPath, content, name: entry.name });
|
|
176
|
+
} catch { /* skip unreadable files */ }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch { /* skip inaccessible dirs */ }
|
|
180
|
+
};
|
|
181
|
+
walk(projectRoot);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { files, skippedThirdParty };
|
|
185
|
+
}
|
|
186
|
+
|
|
126
187
|
const searchProjectCode = {
|
|
127
188
|
name: 'search_project_code',
|
|
128
189
|
description: '在用户项目源码中搜索指定模式。返回匹配的代码片段及上下文。' +
|
|
129
190
|
'自动过滤三方库代码(Pods/Carthage/node_modules),优先返回实际使用行而非声明行。' +
|
|
130
|
-
'适用场景:验证代码模式存在性、查找更多项目示例、理解项目中某个 API 的用法。'
|
|
191
|
+
'适用场景:验证代码模式存在性、查找更多项目示例、理解项目中某个 API 的用法。' +
|
|
192
|
+
'批量搜索:传入 patterns 数组可一次搜索多个关键词(每个关键词独立返回结果),减少工具调用次数。',
|
|
131
193
|
parameters: {
|
|
132
194
|
type: 'object',
|
|
133
195
|
properties: {
|
|
134
|
-
pattern: { type: 'string', description: '
|
|
196
|
+
pattern: { type: 'string', description: '搜索词或正则表达式(单个搜索时使用)' },
|
|
197
|
+
patterns: { type: 'array', items: { type: 'string' }, description: '批量搜索:多个搜索词数组,如 ["methodA", "methodB", "classC"]。与 pattern 互斥,优先使用 patterns。' },
|
|
135
198
|
isRegex: { type: 'boolean', description: '是否为正则表达式,默认 false' },
|
|
136
199
|
fileFilter: { type: 'string', description: '文件扩展名过滤,如 ".m,.swift"' },
|
|
137
|
-
contextLines: { type: 'number', description: '匹配行前后的上下文行数,默认
|
|
138
|
-
maxResults: { type: 'number', description: '
|
|
200
|
+
contextLines: { type: 'number', description: '匹配行前后的上下文行数,默认 3' },
|
|
201
|
+
maxResults: { type: 'number', description: '每个 pattern 的最大返回结果数,默认 5' },
|
|
139
202
|
},
|
|
140
|
-
required: [
|
|
203
|
+
required: [],
|
|
141
204
|
},
|
|
142
205
|
handler: async (params, ctx) => {
|
|
206
|
+
// ── 去重缓存初始化 ──
|
|
207
|
+
const state = ctx._sharedState || ctx;
|
|
208
|
+
if (!state._searchCache) state._searchCache = new Map();
|
|
209
|
+
|
|
210
|
+
// ── 批量模式:patterns 数组 ──
|
|
211
|
+
if (Array.isArray(params.patterns) && params.patterns.length > 0) {
|
|
212
|
+
const batchPatterns = params.patterns.slice(0, 10); // 最多 10 个
|
|
213
|
+
const batchResults = {};
|
|
214
|
+
let dedupCount = 0;
|
|
215
|
+
for (const p of batchPatterns) {
|
|
216
|
+
// 去重:已搜索过的 pattern 直接返回缓存
|
|
217
|
+
const cacheKey = `${p}|${params.isRegex || false}|${params.fileFilter || ''}`;
|
|
218
|
+
if (state._searchCache.has(cacheKey)) {
|
|
219
|
+
batchResults[p] = { ...state._searchCache.get(cacheKey), _cached: true };
|
|
220
|
+
dedupCount++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const sub = await searchProjectCode.handler(
|
|
224
|
+
{ ...params, pattern: p, patterns: undefined },
|
|
225
|
+
ctx,
|
|
226
|
+
);
|
|
227
|
+
const entry = { matches: sub.matches || [], total: sub.total || 0 };
|
|
228
|
+
state._searchCache.set(cacheKey, entry);
|
|
229
|
+
batchResults[p] = entry;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
batchResults,
|
|
233
|
+
patternsSearched: batchPatterns.length,
|
|
234
|
+
searchedFiles: (await _getProjectFiles(params, ctx)).files.length,
|
|
235
|
+
...(dedupCount > 0 ? { _deduped: dedupCount, hint: `${dedupCount} 个 pattern 命中缓存,请避免重复搜索相同关键词。` } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
143
239
|
// 兼容 AI 传 "query" / "search" / "keyword" 替代 "pattern"
|
|
144
240
|
const pattern = params.pattern || params.query || params.search || params.keyword || params.search_query;
|
|
145
|
-
const { isRegex = false, fileFilter, contextLines =
|
|
241
|
+
const { isRegex = false, fileFilter, contextLines = 3, maxResults = 5 } = params;
|
|
146
242
|
const projectRoot = ctx.projectRoot || process.cwd();
|
|
147
243
|
|
|
148
244
|
if (!pattern || typeof pattern !== 'string') {
|
|
149
|
-
return { error: '参数错误: 请提供 pattern
|
|
245
|
+
return { error: '参数错误: 请提供 pattern(搜索关键词或正则表达式)或 patterns 数组', matches: [], total: 0 };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── 单 pattern 去重检查 ──
|
|
249
|
+
const cacheKey = `${pattern}|${params.isRegex || false}|${params.fileFilter || ''}`;
|
|
250
|
+
if (state._searchCache.has(cacheKey)) {
|
|
251
|
+
const cached = state._searchCache.get(cacheKey);
|
|
252
|
+
return { ...cached, _cached: true, hint: `⚠ 已搜索过 "${pattern}",返回缓存结果。请搜索不同的关键词以获取新信息。` };
|
|
150
253
|
}
|
|
151
254
|
|
|
152
255
|
// 构建搜索正则
|
|
@@ -157,61 +260,7 @@ const searchProjectCode = {
|
|
|
157
260
|
return { error: `Invalid pattern: ${err.message}`, matches: [], total: 0 };
|
|
158
261
|
}
|
|
159
262
|
|
|
160
|
-
|
|
161
|
-
let extFilter = null;
|
|
162
|
-
if (fileFilter) {
|
|
163
|
-
const exts = fileFilter.split(',').map(e => e.trim().replace(/^\./, ''));
|
|
164
|
-
extFilter = new RegExp(`\\.(${exts.join('|')})$`, 'i');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 收集文件列表 — 优先使用内存缓存(bootstrap 场景),否则从磁盘递归读取
|
|
168
|
-
const fileCache = ctx.fileCache || null;
|
|
169
|
-
let files;
|
|
170
|
-
let skippedThirdParty = 0;
|
|
171
|
-
|
|
172
|
-
if (fileCache && Array.isArray(fileCache)) {
|
|
173
|
-
// Bootstrap 场景: allFiles 已在内存
|
|
174
|
-
files = fileCache.filter(f => {
|
|
175
|
-
const p = f.relativePath || f.path || '';
|
|
176
|
-
if (THIRD_PARTY_RE.test(p)) { skippedThirdParty++; return false; }
|
|
177
|
-
if (extFilter && !extFilter.test(p)) return false;
|
|
178
|
-
if (!SOURCE_EXT_RE.test(p)) return false;
|
|
179
|
-
return true;
|
|
180
|
-
});
|
|
181
|
-
} else {
|
|
182
|
-
// Dashboard / SignalCollector 场景: 从磁盘递归读取
|
|
183
|
-
files = [];
|
|
184
|
-
const MAX_FILE_SIZE = 512 * 1024; // 512KB — 跳过超大文件
|
|
185
|
-
const walk = (dir, relBase = '') => {
|
|
186
|
-
try {
|
|
187
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
188
|
-
for (const entry of entries) {
|
|
189
|
-
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
190
|
-
const fullPath = path.join(dir, entry.name);
|
|
191
|
-
// 支持 symlink: 解析为目录或文件
|
|
192
|
-
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && (() => { try { return fs.statSync(fullPath).isDirectory(); } catch { return false; } })());
|
|
193
|
-
const isFile = entry.isFile() || (entry.isSymbolicLink() && (() => { try { return fs.statSync(fullPath).isFile(); } catch { return false; } })());
|
|
194
|
-
if (isDir) {
|
|
195
|
-
// 跳过隐藏目录和常见无关目录
|
|
196
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'build') continue;
|
|
197
|
-
if (THIRD_PARTY_RE.test(relPath + '/')) { skippedThirdParty++; continue; }
|
|
198
|
-
walk(fullPath, relPath);
|
|
199
|
-
} else if (isFile) {
|
|
200
|
-
if (THIRD_PARTY_RE.test(relPath)) { skippedThirdParty++; continue; }
|
|
201
|
-
if (!SOURCE_EXT_RE.test(entry.name)) continue;
|
|
202
|
-
if (extFilter && !extFilter.test(entry.name)) continue;
|
|
203
|
-
try {
|
|
204
|
-
const stat = fs.statSync(fullPath);
|
|
205
|
-
if (stat.size > MAX_FILE_SIZE) continue; // 跳过超大文件
|
|
206
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
207
|
-
files.push({ relativePath: relPath, content, name: entry.name });
|
|
208
|
-
} catch { /* skip unreadable files */ }
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} catch { /* skip inaccessible dirs */ }
|
|
212
|
-
};
|
|
213
|
-
walk(projectRoot);
|
|
214
|
-
}
|
|
263
|
+
const { files, skippedThirdParty } = await _getProjectFiles(params, ctx);
|
|
215
264
|
|
|
216
265
|
// 搜索匹配
|
|
217
266
|
const matches = [];
|
|
@@ -253,21 +302,25 @@ const searchProjectCode = {
|
|
|
253
302
|
// 按 score 降序排列(实际使用行优先)
|
|
254
303
|
matches.sort((a, b) => b.score - a.score);
|
|
255
304
|
|
|
256
|
-
|
|
305
|
+
const result = {
|
|
257
306
|
matches,
|
|
258
307
|
total,
|
|
259
308
|
searchedFiles: files.length,
|
|
260
309
|
skippedThirdParty,
|
|
261
310
|
...((() => {
|
|
262
311
|
// P2.2: 搜索超限提示 — 引导使用 AST 工具
|
|
263
|
-
const state = ctx._sharedState || ctx;
|
|
264
312
|
state._searchCallCount = (state._searchCallCount || 0) + 1;
|
|
265
|
-
if (state._searchCallCount >
|
|
313
|
+
if (state._searchCallCount > 12 && ctx.source === 'system') {
|
|
266
314
|
return { hint: `💡 你已搜索 ${state._searchCallCount} 次。考虑使用 get_class_info / get_class_hierarchy / get_project_overview 获取结构化信息,效率更高。` };
|
|
267
315
|
}
|
|
268
316
|
return {};
|
|
269
317
|
})()),
|
|
270
318
|
};
|
|
319
|
+
|
|
320
|
+
// 缓存搜索结果
|
|
321
|
+
state._searchCache.set(cacheKey, { matches: result.matches, total: result.total });
|
|
322
|
+
|
|
323
|
+
return result;
|
|
271
324
|
},
|
|
272
325
|
};
|
|
273
326
|
|
|
@@ -277,18 +330,51 @@ const searchProjectCode = {
|
|
|
277
330
|
const readProjectFile = {
|
|
278
331
|
name: 'read_project_file',
|
|
279
332
|
description: '读取项目中指定文件的内容(部分或全部)。' +
|
|
280
|
-
'通常在 search_project_code 找到匹配后使用,获取更完整的上下文。'
|
|
333
|
+
'通常在 search_project_code 找到匹配后使用,获取更完整的上下文。' +
|
|
334
|
+
'批量读取:传入 filePaths 数组可一次读取多个文件,减少工具调用次数。',
|
|
281
335
|
parameters: {
|
|
282
336
|
type: 'object',
|
|
283
337
|
properties: {
|
|
284
|
-
filePath: { type: 'string', description: '
|
|
338
|
+
filePath: { type: 'string', description: '相对于项目根目录的文件路径(单个文件时使用)' },
|
|
339
|
+
filePaths: { type: 'array', items: { type: 'string' }, description: '批量读取:多个文件路径数组。与 filePath 互斥,优先使用 filePaths。' },
|
|
285
340
|
startLine: { type: 'number', description: '起始行号(1-based),默认 1' },
|
|
286
341
|
endLine: { type: 'number', description: '结束行号(1-based),默认文件末尾' },
|
|
287
|
-
maxLines: { type: 'number', description: '最大返回行数,默认 200' },
|
|
342
|
+
maxLines: { type: 'number', description: '最大返回行数,默认 200(批量模式下每个文件最多 100 行)' },
|
|
288
343
|
},
|
|
289
|
-
required: [
|
|
344
|
+
required: [],
|
|
290
345
|
},
|
|
291
346
|
handler: async (params, ctx) => {
|
|
347
|
+
// ── 去重缓存初始化 ──
|
|
348
|
+
const state = ctx._sharedState || ctx;
|
|
349
|
+
if (!state._readCache) state._readCache = new Map();
|
|
350
|
+
|
|
351
|
+
// ── 批量模式:filePaths 数组 ──
|
|
352
|
+
if (Array.isArray(params.filePaths) && params.filePaths.length > 0) {
|
|
353
|
+
const batchPaths = params.filePaths.slice(0, 8); // 最多 8 个文件
|
|
354
|
+
const batchResults = {};
|
|
355
|
+
let dedupCount = 0;
|
|
356
|
+
for (const fp of batchPaths) {
|
|
357
|
+
const cacheKey = `${fp}|${params.startLine || 1}|${params.endLine || ''}|${params.maxLines || 100}`;
|
|
358
|
+
if (state._readCache.has(cacheKey)) {
|
|
359
|
+
batchResults[fp] = { ...state._readCache.get(cacheKey), _cached: true };
|
|
360
|
+
dedupCount++;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const sub = await readProjectFile.handler(
|
|
364
|
+
{ ...params, filePath: fp, filePaths: undefined, maxLines: Math.min(params.maxLines || 100, 100) },
|
|
365
|
+
ctx,
|
|
366
|
+
);
|
|
367
|
+
const entry = sub.error ? { error: sub.error } : { content: sub.content, totalLines: sub.totalLines, language: sub.language };
|
|
368
|
+
state._readCache.set(cacheKey, entry);
|
|
369
|
+
batchResults[fp] = entry;
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
batchResults,
|
|
373
|
+
filesRead: batchPaths.length,
|
|
374
|
+
...(dedupCount > 0 ? { _deduped: dedupCount, hint: `${dedupCount} 个文件命中缓存,请避免重复读取相同文件。` } : {}),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
292
378
|
// 兼容各种参数名变体 (ToolRegistry 层已做 snake→camel 归一化,
|
|
293
379
|
// 这里兜底处理漏网之鱼)
|
|
294
380
|
const filePath = params.filePath || params.path || params.file_path || params.filepath || params.file || params.filename;
|
|
@@ -296,7 +382,13 @@ const readProjectFile = {
|
|
|
296
382
|
const projectRoot = ctx.projectRoot || process.cwd();
|
|
297
383
|
|
|
298
384
|
if (!filePath || typeof filePath !== 'string') {
|
|
299
|
-
return { error: '参数错误: 请提供 filePath
|
|
385
|
+
return { error: '参数错误: 请提供 filePath(相对于项目根目录的文件路径)或 filePaths 数组' };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── 单文件去重检查 ──
|
|
389
|
+
const readCacheKey = `${filePath}|${startLine}|${params.endLine || ''}|${maxLines}`;
|
|
390
|
+
if (state._readCache.has(readCacheKey)) {
|
|
391
|
+
return { ...state._readCache.get(readCacheKey), _cached: true, hint: `⚠ 已读取过该文件相同行范围,返回缓存结果。如需其他行范围请指定不同的 startLine/endLine。` };
|
|
300
392
|
}
|
|
301
393
|
|
|
302
394
|
// 安全检查: 禁止路径遍历
|
|
@@ -349,7 +441,7 @@ const readProjectFile = {
|
|
|
349
441
|
const langMap = { '.m': 'objectivec', '.mm': 'objectivec', '.h': 'objectivec', '.swift': 'swift', '.js': 'javascript', '.ts': 'typescript', '.py': 'python', '.java': 'java', '.kt': 'kotlin', '.go': 'go', '.rs': 'rust', '.rb': 'ruby' };
|
|
350
442
|
const language = langMap[ext] || 'unknown';
|
|
351
443
|
|
|
352
|
-
|
|
444
|
+
const readResult = {
|
|
353
445
|
filePath,
|
|
354
446
|
totalLines,
|
|
355
447
|
startLine: start,
|
|
@@ -357,6 +449,11 @@ const readProjectFile = {
|
|
|
357
449
|
content: selectedLines.join('\n'),
|
|
358
450
|
language,
|
|
359
451
|
};
|
|
452
|
+
|
|
453
|
+
// 缓存读取结果
|
|
454
|
+
state._readCache.set(readCacheKey, { content: readResult.content, totalLines, language });
|
|
455
|
+
|
|
456
|
+
return readResult;
|
|
360
457
|
},
|
|
361
458
|
};
|
|
362
459
|
|
|
@@ -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() {
|
|
@@ -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) {
|