autosnippet 2.8.3 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +16 -4
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -973
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* │ 17. generate_guard_rule AI 生成 Guard 规则 │
|
|
34
34
|
* └─────────────────────────────────────────────────────┘
|
|
35
35
|
* ┌─── 生命周期操作类 (7) ─────────────────────────────┐
|
|
36
|
-
* │ 18.
|
|
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: '匹配行前后的上下文行数,默认
|
|
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
|
|
|
@@ -756,19 +853,19 @@ const searchRecipes = {
|
|
|
756
853
|
},
|
|
757
854
|
},
|
|
758
855
|
handler: async (params, ctx) => {
|
|
759
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
898
|
+
if (status) filters.lifecycle = status;
|
|
801
899
|
if (language) filters.language = language;
|
|
802
900
|
if (category) filters.category = category;
|
|
803
901
|
|
|
804
|
-
return
|
|
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
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
|
838
|
-
const
|
|
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
|
-
|
|
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
|
|
907
|
-
const
|
|
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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
|
1141
|
-
const found = await
|
|
1229
|
+
const knowledgeService = ctx.container.get('knowledgeService');
|
|
1230
|
+
const found = await knowledgeService.get(params.candidateId);
|
|
1142
1231
|
if (found) {
|
|
1143
|
-
const
|
|
1232
|
+
const json = typeof found.toJSON === 'function' ? found.toJSON() : found;
|
|
1144
1233
|
cand = {
|
|
1145
|
-
title:
|
|
1146
|
-
summary:
|
|
1147
|
-
code:
|
|
1148
|
-
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
|
|
1349
|
-
|
|
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 维度类型校验 —
|
|
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 -
|
|
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(`[
|
|
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,不允许调用
|
|
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(`[
|
|
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.
|
|
1662
|
+
// 16. submit_knowledge
|
|
1573
1663
|
// ────────────────────────────────────────────────────────────
|
|
1574
1664
|
const submitCandidate = {
|
|
1575
|
-
name: '
|
|
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
|
|
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(`[
|
|
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
|
-
//
|
|
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 = []
|
|
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
|
|
1658
|
-
|
|
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
|
|
1677
|
-
return
|
|
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
|
|
1697
|
-
return
|
|
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
|
|
1716
|
-
return
|
|
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
|
|
1736
|
-
return
|
|
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
|
|
1756
|
-
return
|
|
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
|
|
1876
|
+
const knowledgeService = ctx.container.get('knowledgeService');
|
|
1776
1877
|
const type = params.type || 'adoption';
|
|
1777
|
-
await
|
|
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
|
|
1801
|
-
|
|
1802
|
-
|
|
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
|
|
2117
|
-
|
|
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.
|
|
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 +
|
|
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 维度类型校验 (与
|
|
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
|
|
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
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === '
|
|
2467
|
+
tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check',
|
|
2364
2468
|
);
|
|
2365
2469
|
|
|
2366
2470
|
if (submitted.length === 0) {
|