autosnippet 2.8.3 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +5 -5
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +9 -27
  11. package/lib/core/ast/ProjectGraph.js +160 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +109 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +125 -0
  19. package/lib/domain/knowledge/values/Content.js +86 -0
  20. package/lib/domain/knowledge/values/Quality.js +93 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  22. package/lib/domain/knowledge/values/Relations.js +168 -0
  23. package/lib/domain/knowledge/values/Stats.js +87 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +48 -0
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
  27. package/lib/external/mcp/McpServer.js +7 -5
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
  29. package/lib/external/mcp/handlers/bootstrap.js +121 -12
  30. package/lib/external/mcp/handlers/browse.js +77 -73
  31. package/lib/external/mcp/handlers/candidate.js +29 -276
  32. package/lib/external/mcp/handlers/guard.js +2 -0
  33. package/lib/external/mcp/handlers/knowledge.js +205 -0
  34. package/lib/external/mcp/handlers/skill.js +4 -2
  35. package/lib/external/mcp/handlers/structure.js +25 -23
  36. package/lib/external/mcp/handlers/system.js +10 -12
  37. package/lib/external/mcp/tools.js +125 -138
  38. package/lib/http/HttpServer.js +4 -8
  39. package/lib/http/middleware/requestLogger.js +3 -3
  40. package/lib/http/routes/ai.js +17 -1
  41. package/lib/http/routes/extract.js +48 -4
  42. package/lib/http/routes/knowledge.js +246 -0
  43. package/lib/http/routes/search.js +12 -17
  44. package/lib/http/routes/skills.js +44 -1
  45. package/lib/infrastructure/cache/GraphCache.js +143 -0
  46. package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
  47. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  48. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  49. package/lib/infrastructure/realtime/RealtimeService.js +14 -2
  50. package/lib/injection/ServiceContainer.js +164 -63
  51. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  52. package/lib/repository/token/TokenUsageStore.js +162 -0
  53. package/lib/service/automation/DirectiveDetector.js +2 -3
  54. package/lib/service/automation/FileWatcher.js +67 -28
  55. package/lib/service/automation/XcodeIntegration.js +931 -156
  56. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  57. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  58. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  59. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  60. package/lib/service/chat/AnalystAgent.js +25 -14
  61. package/lib/service/chat/CandidateGuardrail.js +1 -1
  62. package/lib/service/chat/ChatAgent.js +280 -48
  63. package/lib/service/chat/ContextWindow.js +92 -8
  64. package/lib/service/chat/HandoffProtocol.js +26 -1
  65. package/lib/service/chat/ProducerAgent.js +11 -9
  66. package/lib/service/chat/tools.js +298 -194
  67. package/lib/service/guard/GuardCheckEngine.js +114 -10
  68. package/lib/service/guard/GuardService.js +59 -48
  69. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  70. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  71. package/lib/service/knowledge/KnowledgeService.js +725 -0
  72. package/lib/service/search/SearchEngine.js +92 -19
  73. package/lib/service/skills/SignalCollector.js +15 -9
  74. package/lib/service/skills/SkillAdvisor.js +13 -11
  75. package/lib/service/snippet/SnippetFactory.js +5 -5
  76. package/lib/service/spm/SpmService.js +119 -18
  77. package/package.json +1 -1
  78. package/scripts/install-cursor-skill.js +0 -6
  79. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  80. package/skills/autosnippet-analysis/SKILL.md +15 -7
  81. package/skills/autosnippet-candidates/SKILL.md +6 -6
  82. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  83. package/skills/autosnippet-concepts/SKILL.md +7 -6
  84. package/skills/autosnippet-create/SKILL.md +13 -13
  85. package/skills/autosnippet-intent/SKILL.md +3 -2
  86. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  87. package/skills/autosnippet-recipes/SKILL.md +16 -4
  88. package/templates/constitution.yaml +1 -1
  89. package/templates/copilot-instructions.md +6 -6
  90. package/templates/recipes-setup/README.md +3 -3
  91. package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
  92. package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
  93. package/lib/cli/CandidateSyncService.js +0 -261
  94. package/lib/cli/SyncService.js +0 -356
  95. package/lib/domain/candidate/Candidate.js +0 -196
  96. package/lib/domain/candidate/CandidateRepository.js +0 -107
  97. package/lib/domain/candidate/Reasoning.js +0 -52
  98. package/lib/domain/recipe/Recipe.js +0 -421
  99. package/lib/domain/recipe/RecipeRepository.js +0 -54
  100. package/lib/domain/types/CandidateStatus.js +0 -52
  101. package/lib/http/routes/candidates.js +0 -559
  102. package/lib/http/routes/recipes.js +0 -397
  103. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  104. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  105. package/lib/service/candidate/CandidateAggregator.js +0 -52
  106. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  107. package/lib/service/candidate/CandidateService.js +0 -973
  108. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  109. package/lib/service/recipe/RecipeService.js +0 -786
  110. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -33,7 +33,7 @@
33
33
  * │ 17. generate_guard_rule AI 生成 Guard 规则 │
34
34
  * └─────────────────────────────────────────────────────┘
35
35
  * ┌─── 生命周期操作类 (7) ─────────────────────────────┐
36
- * │ 18. submit_candidate 提交候选 │
36
+ * │ 18. submit_knowledge 提交候选 │
37
37
  * │ 19. approve_candidate 批准候选 │
38
38
  * │ 20. reject_candidate 驳回候选 │
39
39
  * │ 21. publish_recipe 发布 Recipe │
@@ -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: '匹配行前后的上下文行数,默认 5' },
138
- maxResults: { type: 'number', description: '最大返回结果数,默认 8' },
200
+ contextLines: { type: 'number', description: '匹配行前后的上下文行数,默认 3' },
201
+ maxResults: { type: 'number', description: '每个 pattern 的最大返回结果数,默认 5' },
139
202
  },
140
- required: ['pattern'],
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 = 5, maxResults = 8 } = params;
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(搜索关键词或正则表达式)', matches: [], total: 0 };
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
- return {
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 > 8 && ctx.source === 'system') {
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: ['filePath'],
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
- return {
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
 
@@ -756,19 +853,19 @@ const searchRecipes = {
756
853
  },
757
854
  },
758
855
  handler: async (params, ctx) => {
759
- const recipeService = ctx.container.get('recipeService');
856
+ const knowledgeService = ctx.container.get('knowledgeService');
760
857
  const { keyword, category, language, knowledgeType, limit = 10 } = params;
761
858
 
762
859
  if (keyword) {
763
- return recipeService.searchRecipes(keyword, { page: 1, pageSize: limit });
860
+ return knowledgeService.search(keyword, { page: 1, pageSize: limit });
764
861
  }
765
862
 
766
- const filters = {};
863
+ const filters = { lifecycle: 'active' };
767
864
  if (category) filters.category = category;
768
865
  if (language) filters.language = language;
769
866
  if (knowledgeType) filters.knowledgeType = knowledgeType;
770
867
 
771
- return recipeService.listRecipes(filters, { page: 1, pageSize: limit });
868
+ return knowledgeService.list(filters, { page: 1, pageSize: limit });
772
869
  },
773
870
  };
774
871
 
@@ -789,19 +886,20 @@ const searchCandidates = {
789
886
  },
790
887
  },
791
888
  handler: async (params, ctx) => {
792
- const candidateService = ctx.container.get('candidateService');
889
+ const knowledgeService = ctx.container.get('knowledgeService');
793
890
  const { keyword, status, language, category, limit = 10 } = params;
794
891
 
795
892
  if (keyword) {
796
- return candidateService.searchCandidates(keyword, { page: 1, pageSize: limit });
893
+ return knowledgeService.search(keyword, { page: 1, pageSize: limit });
797
894
  }
798
895
 
896
+ // V3: status 映射为 lifecycle
799
897
  const filters = {};
800
- if (status) filters.status = status;
898
+ if (status) filters.lifecycle = status;
801
899
  if (language) filters.language = language;
802
900
  if (category) filters.category = category;
803
901
 
804
- return candidateService.listCandidates(filters, { page: 1, pageSize: limit });
902
+ return knowledgeService.list(filters, { page: 1, pageSize: limit });
805
903
  },
806
904
  };
807
905
 
@@ -819,10 +917,13 @@ const getRecipeDetail = {
819
917
  required: ['recipeId'],
820
918
  },
821
919
  handler: async (params, ctx) => {
822
- const recipeRepo = ctx.container.get('recipeRepository');
823
- const recipe = await recipeRepo.findById(params.recipeId);
824
- if (!recipe) return { error: `Recipe '${params.recipeId}' not found` };
825
- return recipe;
920
+ const knowledgeService = ctx.container.get('knowledgeService');
921
+ try {
922
+ const entry = await knowledgeService.get(params.recipeId);
923
+ return typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
924
+ } catch {
925
+ return { error: `Knowledge entry '${params.recipeId}' not found` };
926
+ }
826
927
  },
827
928
  };
828
929
 
@@ -834,13 +935,8 @@ const getProjectStats = {
834
935
  description: '获取项目知识库的整体统计:Recipe 数量/分类分布、候选项数量/状态分布、知识图谱节点/边数。',
835
936
  parameters: { type: 'object', properties: {} },
836
937
  handler: async (_params, ctx) => {
837
- const recipeService = ctx.container.get('recipeService');
838
- const candidateService = ctx.container.get('candidateService');
839
-
840
- const [recipeStats, candidateStats] = await Promise.all([
841
- recipeService.getRecipeStats(),
842
- candidateService.getCandidateStats(),
843
- ]);
938
+ const knowledgeService = ctx.container.get('knowledgeService');
939
+ const stats = await knowledgeService.getStats();
844
940
 
845
941
  // 尝试获取知识图谱统计
846
942
  let graphStats = null;
@@ -850,8 +946,7 @@ const getProjectStats = {
850
946
  } catch { /* KG not available */ }
851
947
 
852
948
  return {
853
- recipes: recipeStats,
854
- candidates: candidateStats,
949
+ knowledge: stats,
855
950
  knowledgeGraph: graphStats,
856
951
  };
857
952
  },
@@ -903,8 +998,9 @@ const searchKnowledge = {
903
998
  // 降级: RetrievalFunnel + 全量候选
904
999
  try {
905
1000
  const funnel = ctx.container.get('retrievalFunnel');
906
- const recipeRepo = ctx.container.get('recipeRepository');
907
- const allRecipes = await recipeRepo.findAll?.() || [];
1001
+ const knowledgeRepo = ctx.container.get('knowledgeRepository');
1002
+ const allResult = await knowledgeRepo.findWithPagination({}, { page: 1, pageSize: 500 });
1003
+ const allRecipes = allResult?.items || [];
908
1004
 
909
1005
  // 规范化为 funnel 输入格式
910
1006
  const candidates = allRecipes.map(r => ({
@@ -991,7 +1087,7 @@ const extractRecipes = {
991
1087
  },
992
1088
  handler: async (params, ctx) => {
993
1089
  if (!ctx.aiProvider) return { error: 'AI provider not available' };
994
- const { targetName, files } = params;
1090
+ const { targetName, files, comprehensive } = params;
995
1091
 
996
1092
  // 加载语言参考 Skill(如有),注入到 AI 提取 prompt
997
1093
  let skillReference = null;
@@ -1027,6 +1123,7 @@ const extractRecipes = {
1027
1123
  const extractOpts = {};
1028
1124
  if (skillReference) extractOpts.skillReference = skillReference;
1029
1125
  if (astContext) extractOpts.astContext = astContext;
1126
+ if (comprehensive) extractOpts.comprehensive = true;
1030
1127
 
1031
1128
  // 首选:使用当前 aiProvider
1032
1129
  try {
@@ -1072,12 +1169,10 @@ const enrichCandidate = {
1072
1169
  },
1073
1170
  handler: async (params, ctx) => {
1074
1171
  if (!ctx.aiProvider) return { error: 'AI provider not available' };
1075
- const candidateService = ctx.container.get('candidateService');
1076
- return candidateService.enrichCandidates(
1077
- params.candidateIds,
1078
- ctx.aiProvider,
1079
- { userId: 'agent' },
1080
- );
1172
+ // V3: 使用 MCP handler enrichCandidates 的逻辑
1173
+ const { enrichCandidates: enrichFn } = await import('../../external/mcp/handlers/candidate.js');
1174
+ const result = await enrichFn(ctx, { candidateIds: params.candidateIds });
1175
+ return result?.data || result;
1081
1176
  },
1082
1177
  };
1083
1178
 
@@ -1097,20 +1192,14 @@ const refineBootstrapCandidates = {
1097
1192
  },
1098
1193
  handler: async (params, ctx) => {
1099
1194
  if (!ctx.aiProvider) return { error: 'AI provider not available' };
1100
- const candidateService = ctx.container.get('candidateService');
1101
-
1102
- // 接入 BootstrapTaskManager 双通道推送 refine:* 事件到前端
1103
- let onProgress = null;
1104
- try {
1105
- const taskManager = ctx.container.get('bootstrapTaskManager');
1106
- onProgress = (eventName, data) => taskManager.emitProgress(eventName, data);
1107
- } catch { /* optional: no realtime push */ }
1108
-
1109
- return candidateService.refineBootstrapCandidates(
1110
- ctx.aiProvider,
1111
- { candidateIds: params.candidateIds, userPrompt: params.userPrompt, dryRun: params.dryRun, onProgress },
1112
- { userId: 'agent' },
1113
- );
1195
+ // V3: 委托给 bootstrap handler 的 refine 逻辑
1196
+ const { bootstrapRefine } = await import('../../external/mcp/handlers/bootstrap.js');
1197
+ const result = await bootstrapRefine(ctx, {
1198
+ candidateIds: params.candidateIds,
1199
+ userPrompt: params.userPrompt,
1200
+ dryRun: params.dryRun,
1201
+ });
1202
+ return result?.data || result;
1114
1203
  },
1115
1204
  };
1116
1205
 
@@ -1134,18 +1223,18 @@ const checkDuplicate = {
1134
1223
  const projectRoot = params.projectRoot || ctx.projectRoot;
1135
1224
  const threshold = params.threshold ?? 0.5;
1136
1225
 
1137
- // 如果提供 candidateId,从数据库读取候选信息
1226
+ // 如果提供 candidateId,从数据库读取条目信息
1138
1227
  if (!cand && params.candidateId) {
1139
1228
  try {
1140
- const candidateRepo = ctx.container.get('candidateRepository');
1141
- const found = await candidateRepo.findById(params.candidateId);
1229
+ const knowledgeService = ctx.container.get('knowledgeService');
1230
+ const found = await knowledgeService.get(params.candidateId);
1142
1231
  if (found) {
1143
- const meta = found.metadata || {};
1232
+ const json = typeof found.toJSON === 'function' ? found.toJSON() : found;
1144
1233
  cand = {
1145
- title: meta.title || '',
1146
- summary: meta.summary_cn || meta.summary || '',
1147
- code: found.code || '',
1148
- usageGuide: meta.usageGuide_cn || meta.usageGuide || '',
1234
+ title: json.title || '',
1235
+ summary: json.summary_cn || json.description || '',
1236
+ code: json.content?.pattern || '',
1237
+ usageGuide: json.usage_guide_cn || '',
1149
1238
  };
1150
1239
  }
1151
1240
  } catch { /* ignore */ }
@@ -1345,8 +1434,9 @@ const getRecommendations = {
1345
1434
  },
1346
1435
  },
1347
1436
  handler: async (params, ctx) => {
1348
- const recipeService = ctx.container.get('recipeService');
1349
- return recipeService.getRecommendations(params.limit || 10);
1437
+ const knowledgeService = ctx.container.get('knowledgeService');
1438
+ // V3: 推荐 = 活跃条目按使用量排序
1439
+ return knowledgeService.list({ lifecycle: 'active' }, { page: 1, pageSize: params.limit || 10 });
1350
1440
  },
1351
1441
  };
1352
1442
 
@@ -1535,24 +1625,24 @@ const DIMENSION_DISPLAY_GROUP = {
1535
1625
  };
1536
1626
 
1537
1627
  // ────────────────────────────────────────────────────────────
1538
- // Bootstrap 维度类型校验 — submit_candidate / submit_with_check 共用
1628
+ // Bootstrap 维度类型校验 — submit_knowledge / submit_with_check 共用
1539
1629
  // 基于 dimensionMeta 类型标注系统,而非关键词模糊匹配
1540
1630
  // ────────────────────────────────────────────────────────────
1541
1631
 
1542
1632
  /**
1543
1633
  * 基于维度元数据 (dimensionMeta) 检查提交是否合法
1544
1634
  * @param {{ id: string, outputType: 'candidate'|'skill'|'dual', allowedKnowledgeTypes: string[] }} dimensionMeta
1545
- * @param {object} params - submit_candidate 的参数
1635
+ * @param {object} params - submit_knowledge 的参数
1546
1636
  * @param {object} [logger]
1547
1637
  * @returns {{ status: string, reason: string } | null} 不合法返回 rejected,合法返回 null
1548
1638
  */
1549
1639
  function _checkDimensionType(dimensionMeta, params, logger) {
1550
1640
  // 1. Skill-only 维度不允许提交 Candidate
1551
1641
  if (dimensionMeta.outputType === 'skill') {
1552
- logger?.info(`[submit_candidate] ✗ rejected — dimension "${dimensionMeta.id}" is skill-only, cannot submit candidates`);
1642
+ logger?.info(`[submit_knowledge] ✗ rejected — dimension "${dimensionMeta.id}" is skill-only, cannot submit candidates`);
1553
1643
  return {
1554
1644
  status: 'rejected',
1555
- reason: `当前维度 "${dimensionMeta.id}" 的输出类型为 skill-only,不允许调用 submit_candidate。请只在最终回复中提供 dimensionDigest JSON。`,
1645
+ reason: `当前维度 "${dimensionMeta.id}" 的输出类型为 skill-only,不允许调用 submit_knowledge。请只在最终回复中提供 dimensionDigest JSON。`,
1556
1646
  };
1557
1647
  }
1558
1648
 
@@ -1561,7 +1651,7 @@ function _checkDimensionType(dimensionMeta, params, logger) {
1561
1651
  if (allowed.length > 0 && params.knowledgeType) {
1562
1652
  if (!allowed.includes(params.knowledgeType)) {
1563
1653
  const corrected = allowed[0];
1564
- logger?.warn(`[submit_candidate] knowledgeType "${params.knowledgeType}" → "${corrected}" (auto-corrected for dimension "${dimensionMeta.id}")`);
1654
+ logger?.warn(`[submit_knowledge] knowledgeType "${params.knowledgeType}" → "${corrected}" (auto-corrected for dimension "${dimensionMeta.id}")`);
1565
1655
  params.knowledgeType = corrected;
1566
1656
  }
1567
1657
  }
@@ -1569,10 +1659,10 @@ function _checkDimensionType(dimensionMeta, params, logger) {
1569
1659
  return null;
1570
1660
  }
1571
1661
 
1572
- // 16. submit_candidate
1662
+ // 16. submit_knowledge
1573
1663
  // ────────────────────────────────────────────────────────────
1574
1664
  const submitCandidate = {
1575
- name: 'submit_candidate',
1665
+ name: 'submit_knowledge',
1576
1666
  description: '提交新的代码候选项到知识库审核队列。',
1577
1667
  parameters: {
1578
1668
  type: 'object',
@@ -1591,7 +1681,7 @@ const submitCandidate = {
1591
1681
  required: ['code', 'language', 'category', 'title'],
1592
1682
  },
1593
1683
  handler: async (params, ctx) => {
1594
- const candidateService = ctx.container.get('candidateService');
1684
+ const knowledgeService = ctx.container.get('knowledgeService');
1595
1685
 
1596
1686
  // ── Bootstrap 维度类型校验 (基于 dimensionMeta 类型标注) ──
1597
1687
  const dimMeta = ctx._dimensionMeta;
@@ -1609,8 +1699,7 @@ const submitCandidate = {
1609
1699
  params.knowledgeType = dimMeta.allowedKnowledgeTypes[0];
1610
1700
  }
1611
1701
 
1612
- // Bootstrap 模式: 将 category 覆盖为展示分组 ID,确保前端按合并后的分组展示
1613
- // AI 可能填 "UI"/"Core" 等功能分类,但前端通过 BOOTSTRAP_DIM_LABELS 按展示分组分组
1702
+ // Bootstrap 模式: 将 category 覆盖为展示分组 ID
1614
1703
  params.category = DIMENSION_DISPLAY_GROUP[dimMeta.id] || dimMeta.id;
1615
1704
 
1616
1705
  // ── CandidateGuardrail 质量验证 (Bootstrap 模式) ──
@@ -1620,7 +1709,7 @@ const submitCandidate = {
1620
1709
  );
1621
1710
  const guardResult = guardrail.validate(params);
1622
1711
  if (!guardResult.valid) {
1623
- ctx.logger?.info(`[submit_candidate] ✗ guardrail rejected: ${guardResult.error}`);
1712
+ ctx.logger?.info(`[submit_knowledge] ✗ guardrail rejected: ${guardResult.error}`);
1624
1713
  return {
1625
1714
  status: 'rejected',
1626
1715
  error: guardResult.error,
@@ -1629,11 +1718,10 @@ const submitCandidate = {
1629
1718
  }
1630
1719
  }
1631
1720
 
1632
- // 将所有顶层字段展开到 item LLM 可能把 title/summary/tags 等
1633
- // 放在顶层而非 metadata 中(production prompt 指引)
1721
+ // V3: 将顶层字段映射为 wire format
1634
1722
  const { code, language, category, source, reasoning, metadata, ...rest } = params;
1635
1723
 
1636
- // 防御性修复: AI 可能提交 reasoning.sources = [] 空数组,自动从 filePaths 补充
1724
+ // 防御性修复: AI 可能提交 reasoning.sources = [] 空数组
1637
1725
  const finalReasoning = reasoning || { whyStandard: 'Submitted via ChatAgent', sources: ['agent'], confidence: 0.7 };
1638
1726
  if (Array.isArray(finalReasoning.sources) && finalReasoning.sources.length === 0) {
1639
1727
  const fallbackSources = params.filePaths || rest.filePaths;
@@ -1644,18 +1732,31 @@ const submitCandidate = {
1644
1732
  }
1645
1733
  }
1646
1734
 
1647
- const item = {
1648
- code,
1649
- language,
1650
- category,
1651
- ...rest, // 顶层扩展字段 (title, summary, knowledgeType, tags 等)
1652
- ...metadata, // metadata 对象 (如有)
1653
- reasoning: finalReasoning,
1654
- };
1655
- // Bootstrap 模式额外注入 targetName — 前端优先按 meta.targetName 分组
1656
1735
  const displayGroup = dimMeta ? (DIMENSION_DISPLAY_GROUP[dimMeta.id] || dimMeta.id) : null;
1657
- const extraMeta = (dimMeta && ctx.source === 'system') ? { targetName: displayGroup } : {};
1658
- return candidateService.createFromToolParams(item, source || 'agent', extraMeta, { userId: 'agent' });
1736
+ const data = {
1737
+ title: rest.title || '',
1738
+ description: rest.summary || rest.description || '',
1739
+ language: language || '',
1740
+ category: category || 'general',
1741
+ knowledge_type: rest.knowledgeType || 'code-pattern',
1742
+ tags: rest.tags || [],
1743
+ summary_cn: rest.summary || '',
1744
+ source: source || 'agent',
1745
+ content: {
1746
+ pattern: code || '',
1747
+ rationale: rest.rationale || '',
1748
+ },
1749
+ reasoning: {
1750
+ why_standard: finalReasoning.whyStandard || finalReasoning.why_standard || 'Submitted via ChatAgent',
1751
+ sources: finalReasoning.sources || ['agent'],
1752
+ confidence: finalReasoning.confidence || 0.7,
1753
+ },
1754
+ ...(metadata || {}),
1755
+ };
1756
+ if (dimMeta && ctx.source === 'system' && displayGroup) {
1757
+ data.tags = [...new Set([...(data.tags || []), displayGroup])];
1758
+ }
1759
+ return knowledgeService.create(data, { userId: 'agent' });
1659
1760
  },
1660
1761
  };
1661
1762
 
@@ -1673,8 +1774,8 @@ const approveCandidate = {
1673
1774
  required: ['candidateId'],
1674
1775
  },
1675
1776
  handler: async (params, ctx) => {
1676
- const candidateService = ctx.container.get('candidateService');
1677
- return candidateService.approveCandidate(params.candidateId, { userId: 'agent' });
1777
+ const knowledgeService = ctx.container.get('knowledgeService');
1778
+ return knowledgeService.approve(params.candidateId, { userId: 'agent' });
1678
1779
  },
1679
1780
  };
1680
1781
 
@@ -1693,8 +1794,8 @@ const rejectCandidate = {
1693
1794
  required: ['candidateId', 'reason'],
1694
1795
  },
1695
1796
  handler: async (params, ctx) => {
1696
- const candidateService = ctx.container.get('candidateService');
1697
- return candidateService.rejectCandidate(params.candidateId, params.reason, { userId: 'agent' });
1797
+ const knowledgeService = ctx.container.get('knowledgeService');
1798
+ return knowledgeService.reject(params.candidateId, params.reason, { userId: 'agent' });
1698
1799
  },
1699
1800
  };
1700
1801
 
@@ -1712,8 +1813,8 @@ const publishRecipe = {
1712
1813
  required: ['recipeId'],
1713
1814
  },
1714
1815
  handler: async (params, ctx) => {
1715
- const recipeService = ctx.container.get('recipeService');
1716
- return recipeService.publishRecipe(params.recipeId, { userId: 'agent' });
1816
+ const knowledgeService = ctx.container.get('knowledgeService');
1817
+ return knowledgeService.publish(params.recipeId, { userId: 'agent' });
1717
1818
  },
1718
1819
  };
1719
1820
 
@@ -1732,8 +1833,8 @@ const deprecateRecipe = {
1732
1833
  required: ['recipeId', 'reason'],
1733
1834
  },
1734
1835
  handler: async (params, ctx) => {
1735
- const recipeService = ctx.container.get('recipeService');
1736
- return recipeService.deprecateRecipe(params.recipeId, params.reason, { userId: 'agent' });
1836
+ const knowledgeService = ctx.container.get('knowledgeService');
1837
+ return knowledgeService.deprecate(params.recipeId, params.reason, { userId: 'agent' });
1737
1838
  },
1738
1839
  };
1739
1840
 
@@ -1752,8 +1853,8 @@ const updateRecipe = {
1752
1853
  required: ['recipeId', 'updates'],
1753
1854
  },
1754
1855
  handler: async (params, ctx) => {
1755
- const recipeService = ctx.container.get('recipeService');
1756
- return recipeService.updateRecipe(params.recipeId, params.updates, { userId: 'agent' });
1856
+ const knowledgeService = ctx.container.get('knowledgeService');
1857
+ return knowledgeService.update(params.recipeId, params.updates, { userId: 'agent' });
1757
1858
  },
1758
1859
  };
1759
1860
 
@@ -1772,9 +1873,9 @@ const recordUsage = {
1772
1873
  required: ['recipeId'],
1773
1874
  },
1774
1875
  handler: async (params, ctx) => {
1775
- const recipeService = ctx.container.get('recipeService');
1876
+ const knowledgeService = ctx.container.get('knowledgeService');
1776
1877
  const type = params.type || 'adoption';
1777
- await recipeService.incrementUsage(params.recipeId, type);
1878
+ await knowledgeService.incrementUsage(params.recipeId, type);
1778
1879
  return { success: true, recipeId: params.recipeId, type };
1779
1880
  },
1780
1881
  };
@@ -1797,9 +1898,13 @@ const qualityScore = {
1797
1898
  let recipe = params.recipe;
1798
1899
 
1799
1900
  if (!recipe && params.recipeId) {
1800
- const recipeRepo = ctx.container.get('recipeRepository');
1801
- recipe = await recipeRepo.findById(params.recipeId);
1802
- if (!recipe) return { error: `Recipe '${params.recipeId}' not found` };
1901
+ const knowledgeService = ctx.container.get('knowledgeService');
1902
+ try {
1903
+ const entry = await knowledgeService.get(params.recipeId);
1904
+ recipe = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
1905
+ } catch {
1906
+ return { error: `Knowledge entry '${params.recipeId}' not found` };
1907
+ }
1803
1908
  }
1804
1909
  if (!recipe) return { error: 'Provide recipeId or recipe object' };
1805
1910
 
@@ -2113,13 +2218,8 @@ const knowledgeOverview = {
2113
2218
  const [statsResult, feedbackResult] = await Promise.all([
2114
2219
  (async () => {
2115
2220
  try {
2116
- const recipeService = ctx.container.get('recipeService');
2117
- const candidateService = ctx.container.get('candidateService');
2118
- const [rs, cs] = await Promise.all([
2119
- recipeService.getRecipeStats(),
2120
- candidateService.getCandidateStats(),
2121
- ]);
2122
- return { recipes: rs, candidates: cs };
2221
+ const knowledgeService = ctx.container.get('knowledgeService');
2222
+ return knowledgeService.getStats();
2123
2223
  } catch { return null; }
2124
2224
  })(),
2125
2225
  (async () => {
@@ -2132,8 +2232,7 @@ const knowledgeOverview = {
2132
2232
  ]);
2133
2233
 
2134
2234
  if (statsResult) {
2135
- result.recipes = statsResult.recipes;
2136
- result.candidates = statsResult.candidates;
2235
+ result.knowledge = statsResult;
2137
2236
  }
2138
2237
 
2139
2238
  // 知识图谱统计
@@ -2159,7 +2258,7 @@ const knowledgeOverview = {
2159
2258
  // ────────────────────────────────────────────────────────────
2160
2259
  const submitWithCheck = {
2161
2260
  name: 'submit_with_check',
2162
- description: '安全提交候选:先执行查重检测,无重复则自动提交。如果发现高度相似 Recipe 则阻止并返回相似列表。一次调用完成 check_duplicate + submit_candidate。',
2261
+ description: '安全提交候选:先执行查重检测,无重复则自动提交。如果发现高度相似 Recipe 则阻止并返回相似列表。一次调用完成 check_duplicate + submit_knowledge。',
2163
2262
  parameters: {
2164
2263
  type: 'object',
2165
2264
  properties: {
@@ -2176,7 +2275,7 @@ const submitWithCheck = {
2176
2275
  const { code, language, category, title, summary, threshold = 0.7 } = params;
2177
2276
  const projectRoot = ctx.projectRoot;
2178
2277
 
2179
- // ── Bootstrap 维度类型校验 (与 submit_candidate 共用逻辑) ──
2278
+ // ── Bootstrap 维度类型校验 (与 submit_knowledge 共用逻辑) ──
2180
2279
  const dimMeta = ctx._dimensionMeta;
2181
2280
  if (dimMeta && ctx.source === 'system') {
2182
2281
  const rejected = _checkDimensionType(dimMeta, params, ctx.logger);
@@ -2214,28 +2313,33 @@ const submitWithCheck = {
2214
2313
  };
2215
2314
  }
2216
2315
 
2217
- // Step 2: 提交
2316
+ // Step 2: 提交 (V3: 使用 knowledgeService.create)
2218
2317
  try {
2219
- const candidateService = ctx.container.get('candidateService');
2318
+ const knowledgeService = ctx.container.get('knowledgeService');
2220
2319
  const { code: _c, language: _l, category: _cat, title: _t, summary: _s, threshold: _th, source: paramSource, reasoning: userReasoning, metadata, ...rest } = params;
2221
- const item = {
2222
- code,
2223
- language,
2224
- category,
2225
- ...rest, // 顶层扩展字段 (title, summary, knowledgeType, tags 等)
2226
- ...metadata, // metadata 对象 (如有)
2227
- title: title || '',
2228
- summary: summary || '',
2229
- reasoning: userReasoning || { whyStandard: 'Submitted via submit_with_check', sources: ['agent'], confidence: 0.7 },
2320
+ const swcReasoning = userReasoning || { whyStandard: 'Submitted via submit_with_check', sources: ['agent'], confidence: 0.7 };
2321
+ const data = {
2322
+ title: title || '',
2323
+ description: summary || '',
2324
+ language: language || '',
2325
+ category: category || 'general',
2326
+ knowledge_type: rest.knowledgeType || 'code-pattern',
2327
+ tags: rest.tags || [],
2328
+ summary_cn: summary || '',
2329
+ source: paramSource || 'agent',
2330
+ content: { pattern: code || '' },
2331
+ reasoning: {
2332
+ why_standard: swcReasoning.whyStandard || swcReasoning.why_standard || 'Submitted via submit_with_check',
2333
+ sources: swcReasoning.sources || ['agent'],
2334
+ confidence: swcReasoning.confidence || 0.7,
2335
+ },
2336
+ ...(metadata || {}),
2230
2337
  };
2231
- // Bootstrap 模式注入展示分组 targetName
2232
- const swcDisplayGroup = dimMeta ? (DIMENSION_DISPLAY_GROUP[dimMeta.id] || dimMeta.id) : null;
2233
- const swcExtraMeta = (dimMeta && ctx.source === 'system') ? { targetName: swcDisplayGroup } : {};
2234
- const created = await candidateService.createFromToolParams(item, paramSource || 'agent', swcExtraMeta, { userId: 'agent' });
2338
+ const created = await knowledgeService.create(data, { userId: 'agent' });
2235
2339
 
2236
2340
  return {
2237
2341
  submitted: true,
2238
- candidate: created,
2342
+ entry: typeof created.toJSON === 'function' ? created.toJSON() : created,
2239
2343
  similar: similar.length > 0 ? similar : [],
2240
2344
  _meta: {
2241
2345
  confidence: 'high',
@@ -2360,7 +2464,7 @@ const reviewMyOutput = {
2360
2464
  },
2361
2465
  handler: async (params, context) => {
2362
2466
  const submitted = (context._sessionToolCalls || []).filter(
2363
- tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check',
2467
+ tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check',
2364
2468
  );
2365
2469
 
2366
2470
  if (submitted.length === 0) {