autosnippet 3.0.11 → 3.0.13

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 (56) hide show
  1. package/bin/cli.js +64 -1
  2. package/config/default.json +9 -0
  3. package/dashboard/dist/assets/{index-I2ySoCmF.js → index-Bnm26ulL.js} +47 -47
  4. package/dashboard/dist/index.html +1 -1
  5. package/lib/cli/SetupService.js +92 -5
  6. package/lib/cli/UpgradeService.js +14 -5
  7. package/lib/core/discovery/GenericDiscoverer.js +4 -28
  8. package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +246 -0
  9. package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +80 -0
  10. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +275 -0
  11. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +600 -0
  12. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +125 -342
  13. package/lib/external/mcp/handlers/bootstrap/refine.js +362 -0
  14. package/lib/external/mcp/handlers/bootstrap.js +6 -590
  15. package/lib/external/mcp/handlers/browse.js +119 -9
  16. package/lib/external/mcp/handlers/guard.js +25 -6
  17. package/lib/external/mcp/handlers/search.js +56 -24
  18. package/lib/http/routes/guardRules.js +9 -17
  19. package/lib/injection/ServiceContainer.js +12 -3
  20. package/lib/platform/ios/xcode/XcodeImportResolver.js +434 -0
  21. package/lib/platform/ios/xcode/XcodeIntegration.js +40 -659
  22. package/lib/platform/ios/xcode/XcodeWriteUtils.js +220 -0
  23. package/lib/service/chat/ChatAgent.js +39 -418
  24. package/lib/service/chat/ChatAgentPrompts.js +149 -0
  25. package/lib/service/chat/ChatAgentTasks.js +297 -0
  26. package/lib/service/chat/tools/_shared.js +61 -0
  27. package/lib/service/chat/tools/ai-analysis.js +284 -0
  28. package/lib/service/chat/tools/ast-graph.js +681 -0
  29. package/lib/service/chat/tools/composite.js +496 -0
  30. package/lib/service/chat/tools/guard.js +265 -0
  31. package/lib/service/chat/tools/index.js +250 -0
  32. package/lib/service/chat/tools/infrastructure.js +222 -0
  33. package/lib/service/chat/tools/knowledge-graph.js +234 -0
  34. package/lib/service/chat/tools/lifecycle.js +469 -0
  35. package/lib/service/chat/tools/project-access.js +923 -0
  36. package/lib/service/chat/tools/query.js +264 -0
  37. package/lib/service/chat/tools.js +14 -3994
  38. package/lib/service/cursor/AgentInstructionsGenerator.js +395 -0
  39. package/lib/service/cursor/CursorDeliveryPipeline.js +70 -11
  40. package/lib/service/cursor/FileProtection.js +116 -0
  41. package/lib/service/cursor/KnowledgeCompressor.js +61 -11
  42. package/lib/service/cursor/SkillsSyncer.js +5 -3
  43. package/lib/service/cursor/TopicClassifier.js +19 -3
  44. package/lib/service/guard/ExclusionManager.js +26 -2
  45. package/lib/service/guard/GuardCheckEngine.js +38 -370
  46. package/lib/service/guard/GuardCodeChecks.js +362 -0
  47. package/lib/service/guard/GuardCrossFileChecks.js +307 -0
  48. package/lib/service/guard/GuardPatternUtils.js +180 -0
  49. package/lib/service/guard/GuardService.js +80 -38
  50. package/lib/service/module/ModuleService.js +1 -0
  51. package/lib/service/search/SearchEngine.js +10 -2
  52. package/lib/service/wiki/WikiGenerator.js +226 -1532
  53. package/lib/service/wiki/WikiRenderers.js +1878 -0
  54. package/lib/service/wiki/WikiUtils.js +907 -0
  55. package/lib/shared/LanguageService.js +299 -0
  56. package/package.json +1 -1
@@ -0,0 +1,923 @@
1
+ /**
2
+ * project-access.js — 项目数据访问工具 (5)
3
+ *
4
+ * 1. search_project_code 搜索项目源码
5
+ * 2. read_project_file 读取项目文件
6
+ * 2b. list_project_structure 列出项目目录结构
7
+ * 2c. get_file_summary 文件结构摘要
8
+ * 2d. semantic_search_code 语义知识搜索
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { LanguageService } from '../../../shared/LanguageService.js';
14
+
15
+ // ─── 共享常量 ──────────────────────────────────────────────
16
+
17
+ /** 三方库路径识别(与 bootstrap/shared/third-party-filter.js 对齐) */
18
+ export const THIRD_PARTY_RE =
19
+ /(?:^|\/)(?:Pods|Carthage|\.build\/checkouts|vendor|ThirdParty|External|Submodules|DerivedData|include|node_modules|build)\/|(?:^|\/)(?:Masonry|AFNetworking|SDWebImage|MJRefresh|MJExtension|YYKit|YYModel|Lottie|FLEX|IQKeyboardManager|MBProgressHUD|SVProgressHUD|SnapKit|Kingfisher|Alamofire|Moya|ReactiveObjC|ReactiveCocoa|RxSwift|RxCocoa|FMDB|Realm|Mantle|JSONModel|CocoaLumberjack|CocoaAsyncSocket|SocketRocket|GPUImage|FBSDKCore|FBSDKLogin|FlatBuffers|Protobuf|PromiseKit|Charts|Hero)\//i;
20
+
21
+ /** 源码文件扩展名 */
22
+ export const SOURCE_EXT_RE = /\.(m|mm|swift|h|c|cpp|js|ts|jsx|tsx|py|rb|java|kt|go|rs)$/i;
23
+
24
+ /** 声明行识别 — 用于对匹配行打分(与 bootstrap/shared/scanner.js 对齐) */
25
+ const DECL_RE =
26
+ /^\s*(@property\b|@interface\b|@protocol\b|@class\b|@synthesize\b|@dynamic\b|@end\b|NS_ASSUME_NONNULL|#import\b|#include\b|#define\b)/;
27
+ const TYPE_DECL_RE = /^\s*\w[\w<>*\s]+[\s*]+_?\w+\s*;$/;
28
+
29
+ function _scoreSearchLine(line) {
30
+ const t = line.trim();
31
+ if (DECL_RE.test(t)) {
32
+ return -2;
33
+ }
34
+ if (TYPE_DECL_RE.test(t)) {
35
+ return -1;
36
+ }
37
+ if (/^[-+]\s*\([^)]+\)\s*\w+[^{]*;\s*$/.test(t)) {
38
+ return -1;
39
+ }
40
+ if (/\[.*\w+.*\]/.test(t)) {
41
+ return 2; // ObjC message send
42
+ }
43
+ if (/\w+\s*\(/.test(t)) {
44
+ return 2; // function call
45
+ }
46
+ if (/\^\s*[{(]/.test(t)) {
47
+ return 1; // block literal
48
+ }
49
+ return 0;
50
+ }
51
+
52
+ /**
53
+ * 收集项目文件列表 — 抽取为公用函数,供单次和批量搜索复用。
54
+ * 优先使用内存缓存(bootstrap 场景),否则从磁盘递归读取。
55
+ */
56
+ async function _getProjectFiles(params, ctx) {
57
+ const { fileFilter } = params;
58
+ const projectRoot = ctx.projectRoot || process.cwd();
59
+
60
+ let extFilter = null;
61
+ if (fileFilter) {
62
+ const exts = fileFilter.split(',').map((e) => e.trim().replace(/^\./, ''));
63
+ extFilter = new RegExp(`\\.(${exts.join('|')})$`, 'i');
64
+ }
65
+
66
+ const fileCache = ctx.fileCache || null;
67
+ let files;
68
+ let skippedThirdParty = 0;
69
+
70
+ if (fileCache && Array.isArray(fileCache)) {
71
+ files = fileCache.filter((f) => {
72
+ const p = f.relativePath || f.path || '';
73
+ if (THIRD_PARTY_RE.test(p)) {
74
+ skippedThirdParty++;
75
+ return false;
76
+ }
77
+ if (extFilter && !extFilter.test(p)) {
78
+ return false;
79
+ }
80
+ if (!SOURCE_EXT_RE.test(p)) {
81
+ return false;
82
+ }
83
+ return true;
84
+ });
85
+ } else {
86
+ files = [];
87
+ const MAX_FILE_SIZE = 512 * 1024;
88
+ const walk = (dir, relBase = '') => {
89
+ try {
90
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
93
+ const fullPath = path.join(dir, entry.name);
94
+ const isDir =
95
+ entry.isDirectory() ||
96
+ (entry.isSymbolicLink() &&
97
+ (() => {
98
+ try {
99
+ return fs.statSync(fullPath).isDirectory();
100
+ } catch {
101
+ return false;
102
+ }
103
+ })());
104
+ const isFile =
105
+ entry.isFile() ||
106
+ (entry.isSymbolicLink() &&
107
+ (() => {
108
+ try {
109
+ return fs.statSync(fullPath).isFile();
110
+ } catch {
111
+ return false;
112
+ }
113
+ })());
114
+ if (isDir) {
115
+ if (
116
+ entry.name.startsWith('.') ||
117
+ entry.name === 'node_modules' ||
118
+ entry.name === 'build'
119
+ ) {
120
+ continue;
121
+ }
122
+ if (THIRD_PARTY_RE.test(`${relPath}/`)) {
123
+ skippedThirdParty++;
124
+ continue;
125
+ }
126
+ walk(fullPath, relPath);
127
+ } else if (isFile) {
128
+ if (THIRD_PARTY_RE.test(relPath)) {
129
+ skippedThirdParty++;
130
+ continue;
131
+ }
132
+ if (!SOURCE_EXT_RE.test(entry.name)) {
133
+ continue;
134
+ }
135
+ if (extFilter && !extFilter.test(entry.name)) {
136
+ continue;
137
+ }
138
+ try {
139
+ const stat = fs.statSync(fullPath);
140
+ if (stat.size > MAX_FILE_SIZE) {
141
+ continue;
142
+ }
143
+ const content = fs.readFileSync(fullPath, 'utf-8');
144
+ files.push({ relativePath: relPath, content, name: entry.name });
145
+ } catch {
146
+ /* skip unreadable files */
147
+ }
148
+ }
149
+ }
150
+ } catch {
151
+ /* skip inaccessible dirs */
152
+ }
153
+ };
154
+ walk(projectRoot);
155
+ }
156
+
157
+ return { files, skippedThirdParty };
158
+ }
159
+
160
+ // ─── 1. search_project_code ────────────────────────────────
161
+
162
+ export const searchProjectCode = {
163
+ name: 'search_project_code',
164
+ description:
165
+ '在用户项目源码中搜索指定模式。返回匹配的代码片段及上下文。' +
166
+ '自动过滤三方库代码(Pods/Carthage/node_modules),优先返回实际使用行而非声明行。' +
167
+ '适用场景:验证代码模式存在性、查找更多项目示例、理解项目中某个 API 的用法。' +
168
+ '批量搜索:传入 patterns 数组可一次搜索多个关键词(每个关键词独立返回结果),减少工具调用次数。',
169
+ parameters: {
170
+ type: 'object',
171
+ properties: {
172
+ pattern: { type: 'string', description: '搜索词或正则表达式(单个搜索时使用)' },
173
+ patterns: {
174
+ type: 'array',
175
+ items: { type: 'string' },
176
+ description:
177
+ '批量搜索:多个搜索词数组,如 ["methodA", "methodB", "classC"]。与 pattern 互斥,优先使用 patterns。',
178
+ },
179
+ isRegex: { type: 'boolean', description: '是否为正则表达式,默认 false' },
180
+ fileFilter: { type: 'string', description: '文件扩展名过滤,如 ".m,.swift"' },
181
+ contextLines: { type: 'number', description: '匹配行前后的上下文行数,默认 3' },
182
+ maxResults: { type: 'number', description: '每个 pattern 的最大返回结果数,默认 5' },
183
+ },
184
+ required: [],
185
+ },
186
+ handler: async (params, ctx) => {
187
+ // ── 去重缓存初始化 ──
188
+ const state = ctx._sharedState || ctx;
189
+ if (!state._searchCache) {
190
+ state._searchCache = new Map();
191
+ }
192
+
193
+ // ── 批量模式:patterns 数组 ──
194
+ if (Array.isArray(params.patterns) && params.patterns.length > 0) {
195
+ const batchPatterns = params.patterns.slice(0, 10);
196
+ const batchResults = {};
197
+ let dedupCount = 0;
198
+ for (const p of batchPatterns) {
199
+ const cacheKey = `${p}|${params.isRegex || false}|${params.fileFilter || ''}`;
200
+ if (state._searchCache.has(cacheKey)) {
201
+ batchResults[p] = { ...state._searchCache.get(cacheKey), _cached: true };
202
+ dedupCount++;
203
+ continue;
204
+ }
205
+ const sub = await searchProjectCode.handler(
206
+ { ...params, pattern: p, patterns: undefined },
207
+ ctx
208
+ );
209
+ const entry = { matches: sub.matches || [], total: sub.total || 0 };
210
+ state._searchCache.set(cacheKey, entry);
211
+ batchResults[p] = entry;
212
+ }
213
+ return {
214
+ batchResults,
215
+ patternsSearched: batchPatterns.length,
216
+ searchedFiles: (await _getProjectFiles(params, ctx)).files.length,
217
+ ...(dedupCount > 0
218
+ ? {
219
+ _deduped: dedupCount,
220
+ hint: `${dedupCount} 个 pattern 命中缓存,请避免重复搜索相同关键词。`,
221
+ }
222
+ : {}),
223
+ };
224
+ }
225
+
226
+ // 兼容 AI 传 "query" / "search" / "keyword" 替代 "pattern"
227
+ const pattern =
228
+ params.pattern || params.query || params.search || params.keyword || params.search_query;
229
+ const { isRegex = false, contextLines = 3, maxResults = 5 } = params;
230
+
231
+ if (!pattern || typeof pattern !== 'string') {
232
+ return {
233
+ error: '参数错误: 请提供 pattern(搜索关键词或正则表达式)或 patterns 数组',
234
+ matches: [],
235
+ total: 0,
236
+ };
237
+ }
238
+
239
+ // ── 单 pattern 去重检查 ──
240
+ const cacheKey = `${pattern}|${params.isRegex || false}|${params.fileFilter || ''}`;
241
+ if (state._searchCache.has(cacheKey)) {
242
+ const cached = state._searchCache.get(cacheKey);
243
+ return {
244
+ ...cached,
245
+ _cached: true,
246
+ hint: `⚠ 已搜索过 "${pattern}",返回缓存结果。请搜索不同的关键词以获取新信息。`,
247
+ };
248
+ }
249
+
250
+ // 构建搜索正则
251
+ let searchRe;
252
+ try {
253
+ searchRe = isRegex
254
+ ? new RegExp(pattern, 'gi')
255
+ : new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
256
+ } catch (err) {
257
+ return { error: `Invalid pattern: ${err.message}`, matches: [], total: 0 };
258
+ }
259
+
260
+ const { files, skippedThirdParty } = await _getProjectFiles(params, ctx);
261
+
262
+ // 搜索匹配
263
+ const matches = [];
264
+ let total = 0;
265
+
266
+ for (const f of files) {
267
+ if (!f.content) {
268
+ continue;
269
+ }
270
+ searchRe.lastIndex = 0;
271
+ if (!searchRe.test(f.content)) {
272
+ continue;
273
+ }
274
+
275
+ const lines = f.content.split('\n');
276
+ searchRe.lastIndex = 0;
277
+
278
+ for (let i = 0; i < lines.length; i++) {
279
+ searchRe.lastIndex = 0;
280
+ if (!searchRe.test(lines[i])) {
281
+ continue;
282
+ }
283
+ total++;
284
+
285
+ if (matches.length < maxResults) {
286
+ const start = Math.max(0, i - contextLines);
287
+ const end = Math.min(lines.length - 1, i + contextLines);
288
+ const contextArr = [];
289
+ for (let j = start; j <= end; j++) {
290
+ contextArr.push(lines[j]);
291
+ }
292
+
293
+ matches.push({
294
+ file: f.relativePath || f.path || f.name,
295
+ line: i + 1,
296
+ code: lines[i],
297
+ context: contextArr.join('\n'),
298
+ score: _scoreSearchLine(lines[i]),
299
+ });
300
+ }
301
+ }
302
+ }
303
+
304
+ // 按 score 降序排列(实际使用行优先)
305
+ matches.sort((a, b) => b.score - a.score);
306
+
307
+ const result = {
308
+ matches,
309
+ total,
310
+ searchedFiles: files.length,
311
+ skippedThirdParty,
312
+ ...(() => {
313
+ state._searchCallCount = (state._searchCallCount || 0) + 1;
314
+ if (state._searchCallCount > 12 && ctx.source === 'system') {
315
+ return {
316
+ hint: `💡 你已搜索 ${state._searchCallCount} 次。考虑使用 get_class_info / get_class_hierarchy / get_project_overview 获取结构化信息,效率更高。`,
317
+ };
318
+ }
319
+ return {};
320
+ })(),
321
+ };
322
+
323
+ state._searchCache.set(cacheKey, { matches: result.matches, total: result.total });
324
+
325
+ return result;
326
+ },
327
+ };
328
+
329
+ // ─── 2. read_project_file ──────────────────────────────────
330
+
331
+ export const readProjectFile = {
332
+ name: 'read_project_file',
333
+ description:
334
+ '读取项目中指定文件的内容(部分或全部)。' +
335
+ '通常在 search_project_code 找到匹配后使用,获取更完整的上下文。' +
336
+ '批量读取:传入 filePaths 数组可一次读取多个文件,减少工具调用次数。',
337
+ parameters: {
338
+ type: 'object',
339
+ properties: {
340
+ filePath: { type: 'string', description: '相对于项目根目录的文件路径(单个文件时使用)' },
341
+ filePaths: {
342
+ type: 'array',
343
+ items: { type: 'string' },
344
+ description: '批量读取:多个文件路径数组。与 filePath 互斥,优先使用 filePaths。',
345
+ },
346
+ startLine: { type: 'number', description: '起始行号(1-based),默认 1' },
347
+ endLine: { type: 'number', description: '结束行号(1-based),默认文件末尾' },
348
+ maxLines: {
349
+ type: 'number',
350
+ description: '最大返回行数,默认 200(批量模式下每个文件最多 100 行)',
351
+ },
352
+ },
353
+ required: [],
354
+ },
355
+ handler: async (params, ctx) => {
356
+ // ── 去重缓存初始化 ──
357
+ const state = ctx._sharedState || ctx;
358
+ if (!state._readCache) {
359
+ state._readCache = new Map();
360
+ }
361
+
362
+ // ── 批量模式:filePaths 数组 ──
363
+ if (Array.isArray(params.filePaths) && params.filePaths.length > 0) {
364
+ const batchPaths = params.filePaths.slice(0, 8);
365
+ const batchResults = {};
366
+ let dedupCount = 0;
367
+ for (const fp of batchPaths) {
368
+ const cacheKey = `${fp}|${params.startLine || 1}|${params.endLine || ''}|${params.maxLines || 100}`;
369
+ if (state._readCache.has(cacheKey)) {
370
+ batchResults[fp] = { ...state._readCache.get(cacheKey), _cached: true };
371
+ dedupCount++;
372
+ continue;
373
+ }
374
+ const sub = await readProjectFile.handler(
375
+ {
376
+ ...params,
377
+ filePath: fp,
378
+ filePaths: undefined,
379
+ maxLines: Math.min(params.maxLines || 100, 100),
380
+ },
381
+ ctx
382
+ );
383
+ const entry = sub.error
384
+ ? { error: sub.error }
385
+ : { content: sub.content, totalLines: sub.totalLines, language: sub.language };
386
+ state._readCache.set(cacheKey, entry);
387
+ batchResults[fp] = entry;
388
+ }
389
+ return {
390
+ batchResults,
391
+ filesRead: batchPaths.length,
392
+ ...(dedupCount > 0
393
+ ? { _deduped: dedupCount, hint: `${dedupCount} 个文件命中缓存,请避免重复读取相同文件。` }
394
+ : {}),
395
+ };
396
+ }
397
+
398
+ const filePath =
399
+ params.filePath ||
400
+ params.path ||
401
+ params.file_path ||
402
+ params.filepath ||
403
+ params.file ||
404
+ params.filename;
405
+ const { startLine = 1, maxLines = 200 } = params;
406
+ const projectRoot = ctx.projectRoot || process.cwd();
407
+
408
+ if (!filePath || typeof filePath !== 'string') {
409
+ return { error: '参数错误: 请提供 filePath(相对于项目根目录的文件路径)或 filePaths 数组' };
410
+ }
411
+
412
+ // ── 单文件去重检查 ──
413
+ const readCacheKey = `${filePath}|${startLine}|${params.endLine || ''}|${maxLines}`;
414
+ if (state._readCache.has(readCacheKey)) {
415
+ return {
416
+ ...state._readCache.get(readCacheKey),
417
+ _cached: true,
418
+ hint: `⚠ 已读取过该文件相同行范围,返回缓存结果。如需其他行范围请指定不同的 startLine/endLine。`,
419
+ };
420
+ }
421
+
422
+ // 安全检查: 禁止路径遍历
423
+ const normalized = path.normalize(filePath);
424
+ if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
425
+ return { error: 'Path traversal not allowed. Use relative paths within the project.' };
426
+ }
427
+
428
+ // 优先从内存缓存读取(bootstrap 场景)
429
+ const fileCache = ctx.fileCache || null;
430
+ let content = null;
431
+
432
+ if (fileCache && Array.isArray(fileCache)) {
433
+ const cached = fileCache.find(
434
+ (f) =>
435
+ (f.relativePath || f.path || '') === filePath ||
436
+ (f.relativePath || f.path || '') === normalized
437
+ );
438
+ if (cached) {
439
+ content = cached.content;
440
+ }
441
+ }
442
+
443
+ // 降级: 从磁盘读取
444
+ if (content === null) {
445
+ const fullPath = path.resolve(projectRoot, normalized);
446
+ if (!fullPath.startsWith(projectRoot)) {
447
+ return { error: 'Path traversal not allowed.' };
448
+ }
449
+ try {
450
+ content = fs.readFileSync(fullPath, 'utf-8');
451
+ } catch (err) {
452
+ return { error: `File not found or unreadable: ${err.message}` };
453
+ }
454
+ }
455
+
456
+ const allLines = content.split('\n');
457
+ const totalLines = allLines.length;
458
+ const start = Math.max(1, startLine);
459
+ let end = params.endLine || totalLines;
460
+ end = Math.min(end, totalLines);
461
+
462
+ if (end - start + 1 > maxLines) {
463
+ end = start + maxLines - 1;
464
+ }
465
+
466
+ const selectedLines = allLines.slice(start - 1, end);
467
+
468
+ const ext = path.extname(filePath).toLowerCase();
469
+ const language = LanguageService.langFromExt(ext);
470
+
471
+ const readResult = {
472
+ filePath,
473
+ totalLines,
474
+ startLine: start,
475
+ endLine: end,
476
+ content: selectedLines.join('\n'),
477
+ language,
478
+ };
479
+
480
+ state._readCache.set(readCacheKey, { content: readResult.content, totalLines, language });
481
+
482
+ return readResult;
483
+ },
484
+ };
485
+
486
+ // ─── 2b. list_project_structure ────────────────────────────
487
+
488
+ export const listProjectStructure = {
489
+ name: 'list_project_structure',
490
+ description:
491
+ '列出项目目录结构和文件统计信息。不读取文件内容,只返回目录树和元数据。' +
492
+ '适用场景:了解项目整体布局、识别关键目录、规划探索路径。',
493
+ parameters: {
494
+ type: 'object',
495
+ properties: {
496
+ directory: { type: 'string', description: '相对于项目根目录的子目录路径,默认根目录' },
497
+ depth: { type: 'number', description: '目录展开深度,默认 3' },
498
+ includeStats: {
499
+ type: 'boolean',
500
+ description: '是否包含文件统计(语言分布、行数),默认 true',
501
+ },
502
+ },
503
+ },
504
+ handler: async (params, ctx) => {
505
+ const directory = params.directory || '';
506
+ const depth = Math.min(params.depth ?? 3, 5);
507
+ const includeStats = params.includeStats !== false;
508
+ const projectRoot = ctx.projectRoot || process.cwd();
509
+
510
+ const normalized = path.normalize(directory);
511
+ if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
512
+ return { error: 'Path traversal not allowed. Use relative paths within the project.' };
513
+ }
514
+ const targetDir = directory ? path.resolve(projectRoot, normalized) : projectRoot;
515
+ if (!targetDir.startsWith(projectRoot)) {
516
+ return { error: 'Path traversal not allowed.' };
517
+ }
518
+
519
+ const treeLines = [];
520
+ const stats = { totalFiles: 0, totalDirs: 0, byLanguage: {}, totalLines: 0 };
521
+
522
+ const walk = (dir, relBase, currentDepth, prefix) => {
523
+ if (currentDepth > depth) {
524
+ return;
525
+ }
526
+ let entries;
527
+ try {
528
+ entries = fs.readdirSync(dir, { withFileTypes: true });
529
+ } catch {
530
+ return;
531
+ }
532
+
533
+ entries.sort((a, b) => {
534
+ const aIsDir = a.isDirectory();
535
+ const bIsDir = b.isDirectory();
536
+ if (aIsDir !== bIsDir) {
537
+ return aIsDir ? -1 : 1;
538
+ }
539
+ return a.name.localeCompare(b.name);
540
+ });
541
+
542
+ entries = entries.filter((e) => {
543
+ if (e.name.startsWith('.')) {
544
+ return false;
545
+ }
546
+ const rel = relBase ? `${relBase}/${e.name}` : e.name;
547
+ if (THIRD_PARTY_RE.test(`${rel}/`)) {
548
+ return false;
549
+ }
550
+ return true;
551
+ });
552
+
553
+ for (let i = 0; i < entries.length; i++) {
554
+ const entry = entries[i];
555
+ const isLast = i === entries.length - 1;
556
+ const connector = isLast ? '└── ' : '├── ';
557
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
558
+ const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
559
+ const fullPath = path.join(dir, entry.name);
560
+
561
+ if (entry.isDirectory()) {
562
+ let childCount = 0;
563
+ try {
564
+ childCount = fs.readdirSync(fullPath).length;
565
+ } catch {
566
+ /* skip */
567
+ }
568
+ treeLines.push(`${prefix}${connector}${entry.name}/ (${childCount})`);
569
+ stats.totalDirs++;
570
+ walk(fullPath, rel, currentDepth + 1, childPrefix);
571
+ } else if (entry.isFile()) {
572
+ const ext = path.extname(entry.name).toLowerCase();
573
+ let lineCount = 0;
574
+ let size = 0;
575
+ if (includeStats) {
576
+ try {
577
+ const st = fs.statSync(fullPath);
578
+ size = st.size;
579
+ if (SOURCE_EXT_RE.test(entry.name) && size < 512 * 1024) {
580
+ const content = fs.readFileSync(fullPath, 'utf-8');
581
+ lineCount = content.split('\n').length;
582
+ stats.totalLines += lineCount;
583
+ }
584
+ } catch {
585
+ /* skip */
586
+ }
587
+ }
588
+ const lang = LanguageService.displayNameFromExt(ext);
589
+ if (lang !== ext) {
590
+ stats.byLanguage[lang] = (stats.byLanguage[lang] || 0) + 1;
591
+ }
592
+ const sizeLabel = size > 1024 ? `${(size / 1024).toFixed(0)}KB` : `${size}B`;
593
+ const lineLabel = lineCount > 0 ? `, ${lineCount}L` : '';
594
+ treeLines.push(`${prefix}${connector}${entry.name} (${sizeLabel}${lineLabel})`);
595
+ stats.totalFiles++;
596
+ }
597
+ }
598
+ };
599
+
600
+ walk(targetDir, directory, 1, '');
601
+
602
+ return {
603
+ directory: directory || '.',
604
+ tree: treeLines.join('\n'),
605
+ stats: includeStats ? stats : undefined,
606
+ };
607
+ },
608
+ };
609
+
610
+ // ─── 2c. get_file_summary ──────────────────────────────────
611
+
612
+ /** 语言相关的声明提取正则 */
613
+ const SUMMARY_EXTRACTORS = {
614
+ objectivec: {
615
+ imports: /^\s*(#import\s+.+|#include\s+.+|@import\s+\w+;)/gm,
616
+ declarations:
617
+ /^\s*(@interface\s+\w+[\s:(].*|@protocol\s+\w+[\s<(].*|@implementation\s+\w+|typedef\s+(?:NS_ENUM|NS_OPTIONS)\s*\([^)]+\)\s*\{?)/gm,
618
+ methods: /^\s*[-+]\s*\([^)]+\)\s*[^;{]+/gm,
619
+ properties: /^\s*@property\s*\([^)]*\)\s*[^;]+;/gm,
620
+ },
621
+ swift: {
622
+ imports: /^\s*import\s+\w+/gm,
623
+ declarations:
624
+ /^\s*(?:open|public|internal|fileprivate|private|final)?\s*(?:class|struct|enum|protocol|actor|extension)\s+\w+[^{]*/gm,
625
+ methods:
626
+ /^\s*(?:open|public|internal|fileprivate|private|override|static|class)?\s*func\s+\w+[^{]*/gm,
627
+ properties:
628
+ /^\s*(?:open|public|internal|fileprivate|private|static|class|lazy)?\s*(?:var|let)\s+\w+\s*:\s*[^={\n]+/gm,
629
+ },
630
+ javascript: {
631
+ imports: /^\s*(?:import\s+.+from\s+['"].+['"]|const\s+\{?\s*\w+.*\}?\s*=\s*require\s*\(.+\))/gm,
632
+ declarations: /^\s*(?:export\s+)?(?:default\s+)?(?:class|function|const|let|var)\s+\w+/gm,
633
+ methods: /^\s*(?:async\s+)?(?:static\s+)?(?:get\s+|set\s+)?(?:#?\w+)\s*\([^)]*\)\s*\{/gm,
634
+ },
635
+ typescript: {
636
+ imports: /^\s*import\s+.+from\s+['"].+['"]/gm,
637
+ declarations:
638
+ /^\s*(?:export\s+)?(?:default\s+)?(?:class|interface|type|enum|function|const|let|var|abstract\s+class)\s+\w+/gm,
639
+ methods:
640
+ /^\s*(?:async\s+)?(?:static\s+)?(?:public|private|protected)?\s*(?:get\s+|set\s+)?(?:#?\w+)\s*\([^)]*\)\s*[:{]/gm,
641
+ },
642
+ python: {
643
+ imports: /^\s*(?:import\s+\w+|from\s+\w+\s+import\s+.+)/gm,
644
+ declarations: /^\s*class\s+\w+[^:]*:/gm,
645
+ methods: /^\s*(?:async\s+)?def\s+\w+\s*\([^)]*\)/gm,
646
+ },
647
+ go: {
648
+ imports: /^\s*(?:import\s+"[^"]+"|import\s+\w+\s+"[^"]+")/gm,
649
+ declarations:
650
+ /^\s*(?:type\s+\w+\s+(?:struct|interface|func)\b.*)/gm,
651
+ methods:
652
+ /^\s*func\s+(?:\(\s*\w+\s+\*?\w+\s*\)\s+)?\w+\s*\([^)]*\)[^{]*/gm,
653
+ },
654
+ java: {
655
+ imports: /^\s*import\s+(?:static\s+)?[\w.]+\*?;/gm,
656
+ declarations:
657
+ /^\s*(?:public|private|protected)?\s*(?:abstract|final|static)?\s*(?:class|interface|enum|record|@interface)\s+\w+/gm,
658
+ methods:
659
+ /^\s*(?:public|private|protected)?\s*(?:abstract|static|final|synchronized|default)?\s*(?:<[^>]+>\s+)?\w[\w<>\[\],\s]*\s+\w+\s*\([^)]*\)/gm,
660
+ },
661
+ kotlin: {
662
+ imports: /^\s*import\s+[\w.]+/gm,
663
+ declarations:
664
+ /^\s*(?:open|abstract|data|sealed|inner|value|inline)?\s*(?:class|interface|object|enum\s+class|fun\s+interface)\s+\w+/gm,
665
+ methods:
666
+ /^\s*(?:override\s+)?(?:suspend\s+)?(?:fun|val|var)\s+(?:<[^>]+>\s+)?\w+/gm,
667
+ },
668
+ dart: {
669
+ imports: /^\s*import\s+['"][^'"]+['"];?/gm,
670
+ declarations:
671
+ /^\s*(?:abstract\s+|sealed\s+)?(?:class|mixin|extension|enum|typedef)\s+\w+[^{]*/gm,
672
+ methods:
673
+ /^\s*(?:@override\s+)?(?:static\s+)?(?:Future|Stream|void|\w[\w<>?]*)?\s+\w+\s*\([^)]*\)/gm,
674
+ properties:
675
+ /^\s*(?:static\s+)?(?:final\s+|late\s+|const\s+)?(?:\w[\w<>?]*)\s+\w+\s*[;=]/gm,
676
+ },
677
+ };
678
+ SUMMARY_EXTRACTORS['objectivec++'] = SUMMARY_EXTRACTORS.objectivec;
679
+ SUMMARY_EXTRACTORS.jsx = SUMMARY_EXTRACTORS.javascript;
680
+ SUMMARY_EXTRACTORS.tsx = SUMMARY_EXTRACTORS.typescript;
681
+
682
+ export const getFileSummary = {
683
+ name: 'get_file_summary',
684
+ description:
685
+ '获取文件的结构摘要(导入、声明、方法签名),不包含实现代码。' +
686
+ '比 read_project_file 更轻量,适合快速了解文件角色和 API。',
687
+ parameters: {
688
+ type: 'object',
689
+ properties: {
690
+ filePath: { type: 'string', description: '相对于项目根目录的文件路径' },
691
+ },
692
+ required: ['filePath'],
693
+ },
694
+ handler: async (params, ctx) => {
695
+ const filePath = params.filePath || params.file_path || params.path || params.file;
696
+ const projectRoot = ctx.projectRoot || process.cwd();
697
+
698
+ if (!filePath || typeof filePath !== 'string') {
699
+ return { error: '参数错误: 请提供 filePath' };
700
+ }
701
+
702
+ const normalized = path.normalize(filePath);
703
+ if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
704
+ return { error: 'Path traversal not allowed.' };
705
+ }
706
+
707
+ const fileCache = ctx.fileCache || null;
708
+ let content = null;
709
+
710
+ if (fileCache && Array.isArray(fileCache)) {
711
+ const cached = fileCache.find(
712
+ (f) =>
713
+ (f.relativePath || f.path || '') === filePath ||
714
+ (f.relativePath || f.path || '') === normalized
715
+ );
716
+ if (cached) {
717
+ content = cached.content;
718
+ }
719
+ }
720
+
721
+ if (content === null) {
722
+ const fullPath = path.resolve(projectRoot, normalized);
723
+ if (!fullPath.startsWith(projectRoot)) {
724
+ return { error: 'Path traversal not allowed.' };
725
+ }
726
+ try {
727
+ content = fs.readFileSync(fullPath, 'utf-8');
728
+ } catch (err) {
729
+ return { error: `File not found or unreadable: ${err.message}` };
730
+ }
731
+ }
732
+
733
+ const ext = path.extname(filePath).toLowerCase();
734
+ const language = LanguageService.langFromExt(ext);
735
+ const extractor = SUMMARY_EXTRACTORS[language];
736
+
737
+ const result = {
738
+ filePath,
739
+ language,
740
+ lineCount: content.split('\n').length,
741
+ imports: [],
742
+ declarations: [],
743
+ methods: [],
744
+ properties: [],
745
+ };
746
+
747
+ if (!extractor) {
748
+ result.preview = content.split('\n').slice(0, 30).join('\n');
749
+ return result;
750
+ }
751
+
752
+ const extract = (regex) => {
753
+ const matches = [];
754
+ let m;
755
+ regex.lastIndex = 0;
756
+ while ((m = regex.exec(content)) !== null) {
757
+ matches.push(m[0].trim());
758
+ }
759
+ return matches;
760
+ };
761
+
762
+ if (extractor.imports) {
763
+ result.imports = extract(extractor.imports);
764
+ }
765
+ if (extractor.declarations) {
766
+ result.declarations = extract(extractor.declarations);
767
+ }
768
+ if (extractor.methods) {
769
+ result.methods = extract(extractor.methods).slice(0, 50);
770
+ }
771
+ if (extractor.properties) {
772
+ result.properties = extract(extractor.properties).slice(0, 30);
773
+ }
774
+
775
+ return result;
776
+ },
777
+ };
778
+
779
+ // ─── 2d. semantic_search_code ──────────────────────────────
780
+
781
+ export const semanticSearchCode = {
782
+ name: 'semantic_search_code',
783
+ description:
784
+ '在知识库中进行语义搜索。使用自然语言描述你要查找的代码模式或概念,' +
785
+ '返回语义最相关的知识条目。比关键词搜索更适合模糊/概念性查询。' +
786
+ '示例: "网络请求的错误处理策略"、"线程安全的单例实现"',
787
+ parameters: {
788
+ type: 'object',
789
+ properties: {
790
+ query: { type: 'string', description: '自然语言搜索查询' },
791
+ topK: { type: 'number', description: '返回结果数量,默认 5' },
792
+ category: { type: 'string', description: '按分类过滤 (View/Service/Network/Model 等)' },
793
+ language: { type: 'string', description: '按语言过滤 (swift/objectivec 等)' },
794
+ },
795
+ required: ['query'],
796
+ },
797
+ handler: async (params, ctx) => {
798
+ const query = params.query || params.search || params.keyword;
799
+ const topK = Math.min(params.topK ?? 5, 20);
800
+ const { category, language } = params;
801
+
802
+ if (!query || typeof query !== 'string') {
803
+ return { error: '参数错误: 请提供 query (自然语言搜索查询)' };
804
+ }
805
+
806
+ let searchEngine = null;
807
+ try {
808
+ searchEngine = ctx.container?.get('searchEngine');
809
+ } catch {
810
+ /* not available */
811
+ }
812
+
813
+ if (!searchEngine) {
814
+ let vectorStore = null;
815
+ try {
816
+ vectorStore = ctx.container?.get('vectorStore');
817
+ } catch {
818
+ /* not available */
819
+ }
820
+
821
+ if (!vectorStore) {
822
+ return {
823
+ error:
824
+ '语义搜索不可用: SearchEngine 和 VectorStore 均未初始化。可使用 search_project_code 进行关键词搜索替代。',
825
+ fallbackTool: 'search_project_code',
826
+ };
827
+ }
828
+
829
+ let aiProvider = null;
830
+ try {
831
+ aiProvider = ctx.container?.get('aiProvider');
832
+ } catch {
833
+ /* not available */
834
+ }
835
+
836
+ if (!aiProvider || typeof aiProvider.generateEmbedding !== 'function') {
837
+ const filter = {};
838
+ if (category) {
839
+ filter.category = category;
840
+ }
841
+ if (language) {
842
+ filter.language = language;
843
+ }
844
+
845
+ const results = await vectorStore.hybridSearch([], query, { topK, filter });
846
+ return {
847
+ mode: 'keyword-fallback',
848
+ query,
849
+ message: 'AI Provider 不支持 embedding,已降级到关键词匹配',
850
+ results: results.map((r) => ({
851
+ id: r.item.id,
852
+ content: (r.item.content || '').slice(0, 500),
853
+ score: Math.round(r.score * 100) / 100,
854
+ metadata: r.item.metadata || {},
855
+ })),
856
+ };
857
+ }
858
+
859
+ try {
860
+ const embedding = await aiProvider.generateEmbedding(query);
861
+ const filter = {};
862
+ if (category) {
863
+ filter.category = category;
864
+ }
865
+ if (language) {
866
+ filter.language = language;
867
+ }
868
+
869
+ const results = await vectorStore.hybridSearch(embedding, query, { topK, filter });
870
+ return {
871
+ mode: 'vector',
872
+ query,
873
+ results: results.map((r) => ({
874
+ id: r.item.id,
875
+ content: (r.item.content || '').slice(0, 500),
876
+ score: Math.round(r.score * 100) / 100,
877
+ metadata: r.item.metadata || {},
878
+ })),
879
+ };
880
+ } catch (err) {
881
+ return { error: `向量搜索失败: ${err.message}`, fallbackTool: 'search_project_code' };
882
+ }
883
+ }
884
+
885
+ // 使用 SearchEngine (BM25 + 可选向量)
886
+ try {
887
+ const result = await searchEngine.search(query, {
888
+ mode: 'semantic',
889
+ limit: topK * 2,
890
+ groupByKind: true,
891
+ });
892
+
893
+ let items = result?.items || [];
894
+ const actualMode = result?.mode || 'bm25';
895
+
896
+ if (category) {
897
+ items = items.filter((i) => (i.category || '').toLowerCase() === category.toLowerCase());
898
+ }
899
+ if (language) {
900
+ items = items.filter((i) => (i.language || '').toLowerCase() === language.toLowerCase());
901
+ }
902
+ items = items.slice(0, topK);
903
+
904
+ return {
905
+ mode: actualMode,
906
+ query,
907
+ degraded: actualMode !== 'semantic',
908
+ totalResults: items.length,
909
+ results: items.map((item) => ({
910
+ id: item.id,
911
+ title: item.title || '',
912
+ content: (item.content || item.description || '').slice(0, 500),
913
+ score: Math.round((item.score || 0) * 100) / 100,
914
+ knowledgeType: item.knowledgeType || item.kind || '',
915
+ category: item.category || '',
916
+ language: item.language || '',
917
+ })),
918
+ };
919
+ } catch (err) {
920
+ return { error: `搜索失败: ${err.message}`, fallbackTool: 'search_project_code' };
921
+ }
922
+ },
923
+ };