autosnippet 2.9.0 → 2.11.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 +12 -12
- package/bin/cli.js +53 -40
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-D4IWpDIk.js} +105 -100
- package/dashboard/dist/assets/index-CWBNcF9z.css +1 -0
- package/dashboard/dist/assets/index-DHtzhbuG.js +120 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +35 -36
- package/lib/cli/KnowledgeSyncService.js +345 -0
- package/lib/cli/SetupService.js +8 -26
- package/lib/cli/UpgradeService.js +28 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +289 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +99 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +128 -0
- package/lib/domain/knowledge/values/Content.js +69 -0
- package/lib/domain/knowledge/values/Quality.js +81 -0
- package/lib/domain/knowledge/values/Reasoning.js +70 -0
- package/lib/domain/knowledge/values/Relations.js +142 -0
- package/lib/domain/knowledge/values/Stats.js +72 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +85 -11
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +18 -2
- package/lib/external/mcp/handlers/bootstrap.js +116 -11
- package/lib/external/mcp/handlers/browse.js +76 -73
- package/lib/external/mcp/handlers/candidate.js +26 -275
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +267 -0
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +134 -140
- package/lib/http/HttpServer.js +14 -8
- package/lib/http/routes/ai.js +4 -3
- 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/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/database/migrations/017_camelcase_knowledge_entries.js +107 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/injection/ServiceContainer.js +69 -60
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +338 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +59 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +5 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -19
- package/lib/service/automation/handlers/DraftHandler.js +1 -1
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +25 -22
- package/lib/service/candidate/SimilarityService.js +2 -2
- package/lib/service/chat/AnalystAgent.js +9 -0
- package/lib/service/chat/CandidateGuardrail.js +22 -11
- package/lib/service/chat/ChatAgent.js +132 -54
- package/lib/service/chat/ContextWindow.js +5 -5
- package/lib/service/chat/HandoffProtocol.js +1 -0
- package/lib/service/chat/ProducerAgent.js +40 -13
- package/lib/service/chat/ReasoningLayer.js +854 -0
- package/lib/service/chat/ReasoningTrace.js +329 -0
- package/lib/service/chat/tools.js +308 -205
- package/lib/service/cursor/CursorDeliveryPipeline.js +279 -0
- package/lib/service/cursor/KnowledgeCompressor.js +87 -0
- package/lib/service/cursor/RulesGenerator.js +168 -0
- package/lib/service/cursor/SkillsSyncer.js +268 -0
- package/lib/service/cursor/TokenBudget.js +58 -0
- package/lib/service/cursor/TopicClassifier.js +141 -0
- package/lib/service/guard/GuardCheckEngine.js +99 -10
- package/lib/service/guard/GuardService.js +57 -46
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +595 -0
- package/lib/service/knowledge/KnowledgeService.js +802 -0
- package/lib/service/recipe/RecipeParser.js +3 -12
- package/lib/service/search/SearchEngine.js +67 -22
- package/lib/service/skills/SignalCollector.js +14 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +15 -48
- package/lib/shared/RecipeReadinessChecker.js +6 -11
- 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 +8 -8
- package/skills/autosnippet-coldstart/SKILL.md +8 -4
- 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 +18 -6
- 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-CqJRvYRL.js +0 -197
- package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
- 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 -1001
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -1,60 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP Handlers —
|
|
3
|
-
* validateCandidate, checkDuplicate,
|
|
4
|
-
*
|
|
2
|
+
* MCP Handlers — 候选校验 & 字段诊断 (V3: 使用 knowledgeService)
|
|
3
|
+
* validateCandidate, checkDuplicate, enrichCandidates
|
|
4
|
+
*
|
|
5
|
+
* 注意: submitSingle, submitBatch, submitDrafts 已移至 V3 knowledge handlers
|
|
6
|
+
* (autosnippet_submit_knowledge / submit_knowledge_batch / knowledge_lifecycle)
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
9
|
import { envelope } from '../envelope.js';
|
|
10
|
-
import * as Paths from '../../../infrastructure/config/Paths.js';
|
|
11
|
-
import { checkRecipeReadiness } from '../../../shared/RecipeReadinessChecker.js';
|
|
12
|
-
|
|
13
|
-
// ─── 辅助方法 ──────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 从工具参数构建 Reasoning 值对象数据。
|
|
17
|
-
* Agent 必须提供 reasoning.whyStandard / sources / confidence。
|
|
18
|
-
*/
|
|
19
|
-
export function buildReasoning(obj) {
|
|
20
|
-
const r = obj.reasoning;
|
|
21
|
-
if (!r || !r.whyStandard) return {};
|
|
22
|
-
return {
|
|
23
|
-
whyStandard: r.whyStandard,
|
|
24
|
-
sources: Array.isArray(r.sources) ? r.sources : [],
|
|
25
|
-
confidence: typeof r.confidence === 'number' ? r.confidence : 0.7,
|
|
26
|
-
qualitySignals: r.qualitySignals || {},
|
|
27
|
-
alternatives: Array.isArray(r.alternatives) ? r.alternatives : [],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 统一创建候选的内部方法 — 委托到 CandidateService.createFromToolParams()
|
|
33
|
-
* 保留此函数作为 MCP handler 层的快捷入口,保持向后兼容。
|
|
34
|
-
*/
|
|
35
|
-
async function _createCandidateItem(candidateService, item, source, extraMeta = {}) {
|
|
36
|
-
return candidateService.createFromToolParams(item, source, extraMeta, { userId: 'external_agent' });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ─── 限流检查 ──────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
// Recipe-Ready 检查已提取到 lib/shared/RecipeReadinessChecker.js
|
|
42
|
-
// 旧私有函数 _checkRecipeReadiness 已移除,统一使用 checkRecipeReadiness
|
|
43
|
-
|
|
44
|
-
async function _checkRateLimit(toolName, clientId) {
|
|
45
|
-
const { checkRecipeSave } = await import('../../../http/middleware/RateLimiter.js');
|
|
46
|
-
const projectRoot = process.cwd();
|
|
47
|
-
const limitCheck = checkRecipeSave(projectRoot, clientId || process.env.USER || 'mcp-client');
|
|
48
|
-
if (!limitCheck.allowed) {
|
|
49
|
-
return envelope({
|
|
50
|
-
success: false,
|
|
51
|
-
message: `提交过于频繁,请 ${limitCheck.retryAfter}s 后再试。`,
|
|
52
|
-
errorCode: 'RATE_LIMIT',
|
|
53
|
-
meta: { tool: toolName },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return null; // passed
|
|
57
|
-
}
|
|
58
10
|
|
|
59
11
|
// ─── 校验 & 去重 ───────────────────────────────────────────
|
|
60
12
|
|
|
@@ -104,6 +56,7 @@ export async function validateCandidate(ctx, args) {
|
|
|
104
56
|
}
|
|
105
57
|
|
|
106
58
|
export async function checkDuplicate(ctx, args) {
|
|
59
|
+
// SimilarityService 直接读磁盘 .md 文件,不依赖 Repository
|
|
107
60
|
const { findSimilarRecipes } = await import('../../../service/candidate/SimilarityService.js');
|
|
108
61
|
const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
|
|
109
62
|
const similar = findSimilarRecipes(projectRoot, args.candidate, {
|
|
@@ -113,211 +66,8 @@ export async function checkDuplicate(ctx, args) {
|
|
|
113
66
|
return envelope({ success: true, data: { similar }, meta: { tool: 'autosnippet_check_duplicate' } });
|
|
114
67
|
}
|
|
115
68
|
|
|
116
|
-
// ─── 提交 ──────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
export async function submitSingle(ctx, args) {
|
|
119
|
-
// 限流
|
|
120
|
-
const blocked = await _checkRateLimit('autosnippet_submit_candidate', args.clientId);
|
|
121
|
-
if (blocked) return blocked;
|
|
122
|
-
|
|
123
|
-
const candidateService = ctx.container.get('candidateService');
|
|
124
|
-
const result = await _createCandidateItem(
|
|
125
|
-
candidateService, args, args.source || 'mcp',
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// Recipe-Ready 诊断
|
|
129
|
-
const readiness = checkRecipeReadiness(args);
|
|
130
|
-
const data = { ...result };
|
|
131
|
-
if (!readiness.ready) {
|
|
132
|
-
data.recipeReadyHints = {
|
|
133
|
-
ready: false,
|
|
134
|
-
missingFields: readiness.missing,
|
|
135
|
-
suggestions: readiness.suggestions,
|
|
136
|
-
hint: '请补全以上字段后重新提交,或调用 autosnippet_enrich_candidates 进行完整性诊断',
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return envelope({ success: true, data, meta: { tool: 'autosnippet_submit_candidate' } });
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function submitBatch(ctx, args) {
|
|
144
|
-
if (!args.targetName || !Array.isArray(args.items) || args.items.length === 0) {
|
|
145
|
-
throw new Error('需要 targetName 与 items(非空数组)');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 限流
|
|
149
|
-
const blocked = await _checkRateLimit('autosnippet_submit_candidates', args.clientId);
|
|
150
|
-
if (blocked) return blocked;
|
|
151
|
-
|
|
152
|
-
// 去重
|
|
153
|
-
let items = args.items;
|
|
154
|
-
if (args.deduplicate !== false) {
|
|
155
|
-
const { aggregateCandidates } = await import('../../../service/candidate/CandidateAggregator.js');
|
|
156
|
-
const result = aggregateCandidates(items);
|
|
157
|
-
items = result.items;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// 逐条提交
|
|
161
|
-
const candidateService = ctx.container.get('candidateService');
|
|
162
|
-
const source = args.source || 'cursor-scan';
|
|
163
|
-
let count = 0;
|
|
164
|
-
const itemErrors = [];
|
|
165
|
-
for (let i = 0; i < items.length; i++) {
|
|
166
|
-
try {
|
|
167
|
-
await _createCandidateItem(candidateService, items[i], source, { targetName: args.targetName });
|
|
168
|
-
count++;
|
|
169
|
-
} catch (err) {
|
|
170
|
-
itemErrors.push({ index: i, title: items[i].title || '(untitled)', error: err.message });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const data = { count, total: items.length, targetName: args.targetName };
|
|
175
|
-
if (itemErrors.length > 0) data.errors = itemErrors;
|
|
176
|
-
|
|
177
|
-
// Recipe-Ready 统计
|
|
178
|
-
const notReady = items.filter(it => !checkRecipeReadiness(it).ready);
|
|
179
|
-
if (notReady.length > 0) {
|
|
180
|
-
// 汇总所有缺失字段(去重)
|
|
181
|
-
const allMissing = [...new Set(notReady.flatMap(it => checkRecipeReadiness(it).missing))];
|
|
182
|
-
data.recipeReadyHints = {
|
|
183
|
-
notReadyCount: notReady.length,
|
|
184
|
-
totalCount: items.length,
|
|
185
|
-
commonMissingFields: allMissing,
|
|
186
|
-
hint: `${notReady.length}/${items.length} 条候选缺少 Recipe 必要字段(${allMissing.join(', ')}),请补全后重新提交或调用 autosnippet_enrich_candidates 查漏`,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return envelope({ success: true, data, message: `已提交 ${count}/${items.length} 条候选,请在 Dashboard Candidates 页审核。`, meta: { tool: 'autosnippet_submit_candidates' } });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export async function submitDrafts(ctx, args) {
|
|
194
|
-
const { RecipeParser } = await import('../../../service/recipe/RecipeParser.js');
|
|
195
|
-
|
|
196
|
-
const projectRoot = process.cwd();
|
|
197
|
-
const parser = new RecipeParser();
|
|
198
|
-
const paths = Array.isArray(args.filePaths) ? args.filePaths : [args.filePaths].filter(Boolean);
|
|
199
|
-
if (paths.length === 0) throw new Error('filePaths 不能为空');
|
|
200
|
-
|
|
201
|
-
// 限流
|
|
202
|
-
const blocked = await _checkRateLimit('autosnippet_submit_draft_recipes', args.clientId);
|
|
203
|
-
if (blocked) return blocked;
|
|
204
|
-
|
|
205
|
-
const recipes = [];
|
|
206
|
-
const parseErrors = [];
|
|
207
|
-
const successFiles = [];
|
|
208
|
-
|
|
209
|
-
for (const fp of paths) {
|
|
210
|
-
try {
|
|
211
|
-
const absPath = path.isAbsolute(fp) ? fp : path.join(projectRoot, fp);
|
|
212
|
-
// 禁止操作知识库目录
|
|
213
|
-
const kbDir = Paths.getKnowledgeBaseDirName(projectRoot);
|
|
214
|
-
const rel = path.relative(projectRoot, absPath);
|
|
215
|
-
if (rel.startsWith(kbDir + '/') || rel.startsWith(kbDir + path.sep)) {
|
|
216
|
-
parseErrors.push(`🚫 ${fp} — 禁止操作知识库目录 ${kbDir}/`);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (!fs.existsSync(absPath)) { parseErrors.push(`❌ 文件不存在: ${fp}`); continue; }
|
|
220
|
-
|
|
221
|
-
const content = fs.readFileSync(absPath, 'utf8');
|
|
222
|
-
let parsed = [];
|
|
223
|
-
if (parser.isCompleteRecipe(content)) {
|
|
224
|
-
const r = parser.parse(content);
|
|
225
|
-
if (r) parsed.push(r);
|
|
226
|
-
} else {
|
|
227
|
-
parsed = parser.parseAll(content).filter(Boolean);
|
|
228
|
-
}
|
|
229
|
-
if (parsed.length === 0 && parser.isIntroOnly(content)) {
|
|
230
|
-
const r = parser.parse(content); // intro-only still parseable for frontmatter
|
|
231
|
-
if (r) parsed.push(r);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 校验
|
|
235
|
-
const { RecipeCandidateValidator } = await import('../../../service/recipe/RecipeCandidateValidator.js');
|
|
236
|
-
const validator = new RecipeCandidateValidator();
|
|
237
|
-
const valid = [];
|
|
238
|
-
for (const item of parsed) {
|
|
239
|
-
const result = validator.validate(item);
|
|
240
|
-
if (!result.errors || result.errors.length === 0) {
|
|
241
|
-
valid.push(item);
|
|
242
|
-
} else {
|
|
243
|
-
parseErrors.push(`❌ ${path.basename(fp)}: ${result.errors.join('; ')}`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
if (valid.length > 0) {
|
|
247
|
-
recipes.push(...valid.map(r => ({ ...r, _sourceFile: absPath })));
|
|
248
|
-
successFiles.push({ path: absPath, count: valid.length, name: path.basename(absPath) });
|
|
249
|
-
}
|
|
250
|
-
} catch (err) {
|
|
251
|
-
parseErrors.push(`❌ ${path.basename(fp)}: ${err.message}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (recipes.length === 0) {
|
|
256
|
-
return envelope({ success: false, message: `未能解析出有效 Recipe。${parseErrors.join('\n')}`, errorCode: 'PARSE_FAILED', meta: { tool: 'autosnippet_submit_draft_recipes' } });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 逐条提交 — 使用 _createCandidateItem 统一路径
|
|
260
|
-
const candidateService = ctx.container.get('candidateService');
|
|
261
|
-
const source = args.source || 'copilot-draft';
|
|
262
|
-
let count = 0;
|
|
263
|
-
const submitErrors = [];
|
|
264
|
-
for (const item of recipes) {
|
|
265
|
-
try {
|
|
266
|
-
// 将 RecipeParser 的字段映射到 candidate 通用字段
|
|
267
|
-
const normalized = {
|
|
268
|
-
code: item.code || '',
|
|
269
|
-
language: item.language || '',
|
|
270
|
-
category: item.category || 'general',
|
|
271
|
-
title: item.title || '',
|
|
272
|
-
summary: item.summary || item.summary_cn || '',
|
|
273
|
-
summary_cn: item.summary_cn || item.summary || '',
|
|
274
|
-
summary_en: item.summary_en || '',
|
|
275
|
-
description: item.description || item.summary_en || '',
|
|
276
|
-
trigger: item.trigger || '',
|
|
277
|
-
usageGuide: item.usageGuide || item.usageGuide_cn || '',
|
|
278
|
-
usageGuide_cn: item.usageGuide_cn || item.usageGuide || '',
|
|
279
|
-
usageGuide_en: item.usageGuide_en || '',
|
|
280
|
-
headers: item.headers || [],
|
|
281
|
-
rationale: item.rationale || '',
|
|
282
|
-
knowledgeType: item.knowledgeType || 'code-pattern',
|
|
283
|
-
tags: item.tags || [],
|
|
284
|
-
sourceFile: item._sourceFile || '',
|
|
285
|
-
// 草稿不含 reasoning — _createCandidateItem 会自动生成默认值
|
|
286
|
-
};
|
|
287
|
-
await _createCandidateItem(candidateService, normalized, source, { targetName: args.targetName || '_draft' });
|
|
288
|
-
count++;
|
|
289
|
-
} catch (err) {
|
|
290
|
-
submitErrors.push({ title: item.title || '(untitled)', error: err.message });
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// 删除成功文件
|
|
295
|
-
const deleted = [];
|
|
296
|
-
if (args.deleteAfterSubmit && count > 0) {
|
|
297
|
-
for (const f of successFiles) {
|
|
298
|
-
try { fs.unlinkSync(f.path); deleted.push(f.name); } catch { /* ignore */ }
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
let msg = `已提交 ${count}/${recipes.length} 条 Recipe 候选(target: ${args.targetName || '_draft'})。`;
|
|
303
|
-
if (deleted.length > 0) msg += ` 已删除草稿: ${deleted.join(', ')}。`;
|
|
304
|
-
if (parseErrors.length > 0) msg += `\n⚠️ 解析失败:\n${parseErrors.join('\n')}`;
|
|
305
|
-
if (submitErrors.length > 0) msg += `\n⚠️ 提交失败:\n${submitErrors.map(e => ` ${e.title}: ${e.error}`).join('\n')}`;
|
|
306
|
-
|
|
307
|
-
const data = { count, total: recipes.length, targetName: args.targetName || '_draft', deleted };
|
|
308
|
-
if (submitErrors.length > 0) data.errors = submitErrors;
|
|
309
|
-
return envelope({ success: true, data, message: msg, meta: { tool: 'autosnippet_submit_draft_recipes' } });
|
|
310
|
-
}
|
|
311
|
-
|
|
312
69
|
// ─── 语义字段缺失诊断(无 AI 依赖) ──────────────────────────
|
|
313
70
|
|
|
314
|
-
/**
|
|
315
|
-
* enrichCandidates — 诊断候选的语义字段缺失情况
|
|
316
|
-
*
|
|
317
|
-
* 设计原则:MCP 调用方是外部 AI Agent,不需要项目内置 AI 补全。
|
|
318
|
-
* 本工具仅做「字段完整性检查」,返回每个候选缺失了哪些语义字段,
|
|
319
|
-
* Agent 据此自行补全后调用 submit_candidates 更新。
|
|
320
|
-
*/
|
|
321
71
|
export async function enrichCandidates(ctx, args) {
|
|
322
72
|
const ids = args.candidateIds;
|
|
323
73
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
@@ -327,18 +77,15 @@ export async function enrichCandidates(ctx, args) {
|
|
|
327
77
|
throw new Error('Max 20 candidates per enrichment call');
|
|
328
78
|
}
|
|
329
79
|
|
|
330
|
-
const
|
|
331
|
-
if (!
|
|
80
|
+
const knowledgeService = ctx.container.get('knowledgeService');
|
|
81
|
+
if (!knowledgeService) throw new Error('KnowledgeService not available');
|
|
332
82
|
|
|
333
|
-
const SEMANTIC_KEYS = ['rationale', 'knowledgeType', 'complexity', 'scope', 'steps', 'constraints'];
|
|
334
|
-
// Recipe-Ready 必填字段(category/trigger/summary*/headers 等)
|
|
83
|
+
const SEMANTIC_KEYS = ['content.rationale', 'knowledgeType', 'complexity', 'scope', 'content.steps', 'constraints'];
|
|
335
84
|
const RECIPE_READY_KEYS = [
|
|
336
85
|
{ key: 'category', check: v => v && ['View','Service','Tool','Model','Network','Storage','UI','Utility'].includes(v), hint: 'category 必须为 8 标准值之一' },
|
|
337
86
|
{ key: 'trigger', check: v => v && v.startsWith('@'), hint: 'trigger 必须以 @ 开头' },
|
|
338
|
-
{ key: '
|
|
339
|
-
{ key: 'summary_en', check: v => !!v, hint: '英文摘要' },
|
|
87
|
+
{ key: 'description', check: v => !!v, hint: '知识条目描述' },
|
|
340
88
|
{ key: 'headers', check: v => Array.isArray(v) && v.length > 0, hint: '完整 import 语句数组' },
|
|
341
|
-
{ key: 'usageGuide', check: v => !!v, hint: '使用指南(Markdown ### 章节)' },
|
|
342
89
|
];
|
|
343
90
|
|
|
344
91
|
const results = [];
|
|
@@ -346,29 +93,31 @@ export async function enrichCandidates(ctx, args) {
|
|
|
346
93
|
let needsRecipeFields = 0;
|
|
347
94
|
for (const id of ids) {
|
|
348
95
|
try {
|
|
349
|
-
const
|
|
350
|
-
if (!
|
|
96
|
+
const entry = await knowledgeService.get(id);
|
|
97
|
+
if (!entry) {
|
|
351
98
|
results.push({ id, found: false, missingFields: [], recipeReadyMissing: [] });
|
|
352
99
|
continue;
|
|
353
100
|
}
|
|
354
|
-
const
|
|
101
|
+
const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
|
|
355
102
|
|
|
356
103
|
// 语义字段检查
|
|
357
104
|
const missing = [];
|
|
358
|
-
for (const
|
|
359
|
-
const
|
|
105
|
+
for (const keyPath of SEMANTIC_KEYS) {
|
|
106
|
+
const parts = keyPath.split('.');
|
|
107
|
+
let val = json;
|
|
108
|
+
for (const p of parts) { val = val?.[p]; }
|
|
360
109
|
if (val === undefined || val === null || val === '' ||
|
|
361
110
|
(typeof val === 'string' && val.trim() === '') ||
|
|
362
111
|
(Array.isArray(val) && val.length === 0) ||
|
|
363
112
|
(typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0)) {
|
|
364
|
-
missing.push(
|
|
113
|
+
missing.push(keyPath);
|
|
365
114
|
}
|
|
366
115
|
}
|
|
367
116
|
|
|
368
117
|
// Recipe-Ready 字段检查
|
|
369
118
|
const recipeReadyMissing = [];
|
|
370
119
|
for (const { key, check, hint } of RECIPE_READY_KEYS) {
|
|
371
|
-
const val =
|
|
120
|
+
const val = json[key];
|
|
372
121
|
if (!check(val)) {
|
|
373
122
|
recipeReadyMissing.push({ field: key, hint });
|
|
374
123
|
}
|
|
@@ -377,8 +126,10 @@ export async function enrichCandidates(ctx, args) {
|
|
|
377
126
|
results.push({
|
|
378
127
|
id,
|
|
379
128
|
found: true,
|
|
380
|
-
title:
|
|
381
|
-
language:
|
|
129
|
+
title: json.title || '',
|
|
130
|
+
language: json.language,
|
|
131
|
+
lifecycle: json.lifecycle,
|
|
132
|
+
kind: json.kind,
|
|
382
133
|
missingFields: missing,
|
|
383
134
|
recipeReadyMissing,
|
|
384
135
|
complete: missing.length === 0 && recipeReadyMissing.length === 0,
|
|
@@ -397,10 +148,10 @@ export async function enrichCandidates(ctx, args) {
|
|
|
397
148
|
needsEnrichment,
|
|
398
149
|
needsRecipeFields,
|
|
399
150
|
fullyComplete: ids.length - Math.max(needsEnrichment, needsRecipeFields),
|
|
400
|
-
|
|
151
|
+
entries: results,
|
|
401
152
|
hint: (needsEnrichment > 0 || needsRecipeFields > 0)
|
|
402
|
-
? '请 Agent 根据 missingFields(语义)和 recipeReadyMissing
|
|
403
|
-
: '
|
|
153
|
+
? '请 Agent 根据 missingFields(语义)和 recipeReadyMissing(必填)自行补全后重新提交'
|
|
154
|
+
: '所有条目字段完整',
|
|
404
155
|
},
|
|
405
156
|
meta: { tool: 'autosnippet_enrich_candidates' },
|
|
406
157
|
});
|
|
@@ -89,6 +89,7 @@ export async function guardAuditFiles(ctx, args) {
|
|
|
89
89
|
violations: f.violations,
|
|
90
90
|
summary: f.summary,
|
|
91
91
|
})),
|
|
92
|
+
...(result.crossFileViolations?.length ? { crossFileViolations: result.crossFileViolations } : {}),
|
|
92
93
|
},
|
|
93
94
|
meta: { tool: 'autosnippet_guard_audit_files' },
|
|
94
95
|
});
|
|
@@ -187,6 +188,7 @@ export async function scanProject(ctx, args) {
|
|
|
187
188
|
violations: f.violations,
|
|
188
189
|
summary: f.summary,
|
|
189
190
|
})),
|
|
191
|
+
...(guardAudit.crossFileViolations?.length ? { crossFileViolations: guardAudit.crossFileViolations } : {}),
|
|
190
192
|
} : null,
|
|
191
193
|
},
|
|
192
194
|
meta: { tool: 'autosnippet_scan_project' },
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Handlers — V3 知识条目提交 & 生命周期
|
|
3
|
+
* submitKnowledge, submitKnowledgeBatch, knowledgeLifecycle
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { envelope } from '../envelope.js';
|
|
7
|
+
import { checkRecipeReadiness } from '../../../shared/RecipeReadinessChecker.js';
|
|
8
|
+
|
|
9
|
+
// ─── 限流 ──────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
async function _checkRateLimit(toolName, clientId) {
|
|
12
|
+
const { checkRecipeSave } = await import('../../../http/middleware/RateLimiter.js');
|
|
13
|
+
const projectRoot = process.cwd();
|
|
14
|
+
const limitCheck = checkRecipeSave(projectRoot, clientId || process.env.USER || 'mcp-client');
|
|
15
|
+
if (!limitCheck.allowed) {
|
|
16
|
+
return envelope({
|
|
17
|
+
success: false,
|
|
18
|
+
message: `提交过于频繁,请 ${limitCheck.retryAfter}s 后再试。`,
|
|
19
|
+
errorCode: 'RATE_LIMIT',
|
|
20
|
+
meta: { tool: toolName },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── V3 字段增强 ────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 将 MCP wire format 增强为 V3 KnowledgeEntry 数据:
|
|
30
|
+
* - 确保 source 为 'mcp'
|
|
31
|
+
* - QualityScorer 评分(程序化)
|
|
32
|
+
* - RecipeExtractor 语义标签(程序化)
|
|
33
|
+
* - 其余 V3 字段由 Cursor 生成,缺失即留空(KnowledgeEntry 构造函数填默认值)
|
|
34
|
+
*/
|
|
35
|
+
function _enrichToV3(args, container) {
|
|
36
|
+
const data = { ...args };
|
|
37
|
+
|
|
38
|
+
// 来源标记(非 Cursor 职责)
|
|
39
|
+
if (!data.source) data.source = 'mcp';
|
|
40
|
+
|
|
41
|
+
// QualityScorer 评分(程序化)
|
|
42
|
+
try {
|
|
43
|
+
const qualityScorer = container?.get?.('qualityScorer');
|
|
44
|
+
if (qualityScorer) {
|
|
45
|
+
const codeForScore = data.content?.pattern || data.content?.markdown || '';
|
|
46
|
+
const scoreResult = qualityScorer.score({
|
|
47
|
+
...data,
|
|
48
|
+
code: codeForScore,
|
|
49
|
+
});
|
|
50
|
+
data.quality = {
|
|
51
|
+
completeness: 0,
|
|
52
|
+
adaptation: 0,
|
|
53
|
+
documentation: 0,
|
|
54
|
+
overall: scoreResult.score ?? 0,
|
|
55
|
+
grade: scoreResult.grade || '',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
} catch { /* best effort */ }
|
|
59
|
+
|
|
60
|
+
// RecipeExtractor 语义标签(程序化)
|
|
61
|
+
try {
|
|
62
|
+
const recipeExtractor = container?.get?.('recipeExtractor');
|
|
63
|
+
if (recipeExtractor) {
|
|
64
|
+
const codeForTags = data.content?.pattern || '';
|
|
65
|
+
if (codeForTags) {
|
|
66
|
+
const extracted = recipeExtractor.extractFromContent(
|
|
67
|
+
codeForTags, `${data.title || 'unknown'}.${data.language || 'swift'}`, ''
|
|
68
|
+
);
|
|
69
|
+
if (extracted.semanticTags?.length > 0) {
|
|
70
|
+
data.tags = [...new Set([...(data.tags || []), ...extracted.semanticTags])];
|
|
71
|
+
}
|
|
72
|
+
if ((!data.category || data.category === 'Utility') && extracted.category && extracted.category !== 'general') {
|
|
73
|
+
data.category = extracted.category;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch { /* best effort */ }
|
|
78
|
+
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── V3 wire format → KnowledgeService.create() ────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 单条知识提交 (autosnippet_submit_knowledge)
|
|
86
|
+
*
|
|
87
|
+
* MCP wire format → V3 增强 → KnowledgeService.create()
|
|
88
|
+
* 增强包括:source='mcp'、reasoning 默认值、Delivery 字段补齐、QualityScorer、语义标签。
|
|
89
|
+
*/
|
|
90
|
+
export async function submitKnowledge(ctx, args) {
|
|
91
|
+
// 限流
|
|
92
|
+
const blocked = await _checkRateLimit('autosnippet_submit_knowledge', args.client_id);
|
|
93
|
+
if (blocked) return blocked;
|
|
94
|
+
|
|
95
|
+
const service = ctx.container.get('knowledgeService');
|
|
96
|
+
|
|
97
|
+
// V3 字段增强
|
|
98
|
+
const enrichedData = _enrichToV3(args, ctx.container);
|
|
99
|
+
|
|
100
|
+
const entry = await service.create(enrichedData, { userId: 'mcp' });
|
|
101
|
+
|
|
102
|
+
// Recipe-Ready 诊断(兼容旧格式)
|
|
103
|
+
const readinessInput = _toReadinessInput(args);
|
|
104
|
+
const readiness = checkRecipeReadiness(readinessInput);
|
|
105
|
+
|
|
106
|
+
const data = {
|
|
107
|
+
id: entry.id,
|
|
108
|
+
lifecycle: entry.lifecycle,
|
|
109
|
+
title: entry.title,
|
|
110
|
+
kind: entry.kind,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (!readiness.ready) {
|
|
114
|
+
data.recipeReadyHints = {
|
|
115
|
+
ready: false,
|
|
116
|
+
missingFields: readiness.missing,
|
|
117
|
+
suggestions: readiness.suggestions,
|
|
118
|
+
hint: '请补全以上字段后重新提交,或调用 autosnippet_enrich_candidates 进行完整性诊断',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return envelope({
|
|
123
|
+
success: true,
|
|
124
|
+
data,
|
|
125
|
+
meta: { tool: 'autosnippet_submit_knowledge' },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 批量知识提交 (autosnippet_submit_knowledge_batch)
|
|
131
|
+
*/
|
|
132
|
+
export async function submitKnowledgeBatch(ctx, args) {
|
|
133
|
+
if (!args.target_name || !Array.isArray(args.items) || args.items.length === 0) {
|
|
134
|
+
throw new Error('需要 target_name 与 items(非空数组)');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 限流
|
|
138
|
+
const blocked = await _checkRateLimit('autosnippet_submit_knowledge_batch', args.client_id);
|
|
139
|
+
if (blocked) return blocked;
|
|
140
|
+
|
|
141
|
+
// 去重(可选)
|
|
142
|
+
let items = args.items;
|
|
143
|
+
if (args.deduplicate !== false) {
|
|
144
|
+
try {
|
|
145
|
+
const { aggregateCandidates } = await import('../../../service/candidate/CandidateAggregator.js');
|
|
146
|
+
// 对 title 字段做去重
|
|
147
|
+
const readinessItems = items.map(it => ({
|
|
148
|
+
...it,
|
|
149
|
+
code: it.content?.pattern || it.code || '',
|
|
150
|
+
}));
|
|
151
|
+
const result = aggregateCandidates(readinessItems);
|
|
152
|
+
// 保留原始 items 顺序中去重后的
|
|
153
|
+
if (result.items && result.items.length < items.length) {
|
|
154
|
+
const titles = new Set(result.items.map(it => it.title));
|
|
155
|
+
items = items.filter(it => titles.has(it.title));
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// CandidateAggregator 加载失败时降级:不去重
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const service = ctx.container.get('knowledgeService');
|
|
163
|
+
const source = args.source || 'cursor-scan';
|
|
164
|
+
let count = 0;
|
|
165
|
+
const itemErrors = [];
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < items.length; i++) {
|
|
168
|
+
try {
|
|
169
|
+
const itemData = _enrichToV3({ ...items[i], source }, ctx.container);
|
|
170
|
+
await service.create(itemData, { userId: 'mcp' });
|
|
171
|
+
count++;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
itemErrors.push({ index: i, title: items[i].title || '(untitled)', error: err.message });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const data = { count, total: items.length, targetName: args.target_name };
|
|
178
|
+
if (itemErrors.length > 0) data.errors = itemErrors;
|
|
179
|
+
|
|
180
|
+
// Recipe-Ready 统计
|
|
181
|
+
const notReady = items.filter(it => !checkRecipeReadiness(_toReadinessInput(it)).ready);
|
|
182
|
+
if (notReady.length > 0) {
|
|
183
|
+
const allMissing = [...new Set(notReady.flatMap(it => checkRecipeReadiness(_toReadinessInput(it)).missing))];
|
|
184
|
+
data.recipeReadyHints = {
|
|
185
|
+
notReadyCount: notReady.length,
|
|
186
|
+
totalCount: items.length,
|
|
187
|
+
commonMissingFields: allMissing,
|
|
188
|
+
hint: `${notReady.length}/${items.length} 条知识条目缺少必要字段(${allMissing.join(', ')}),请补全后重新提交`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return envelope({
|
|
193
|
+
success: true,
|
|
194
|
+
data,
|
|
195
|
+
message: `已提交 ${count}/${items.length} 条知识条目。`,
|
|
196
|
+
meta: { tool: 'autosnippet_submit_knowledge_batch' },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 知识条目生命周期操作 (autosnippet_knowledge_lifecycle)
|
|
202
|
+
*
|
|
203
|
+
* 简化为 3 状态: pending / active / deprecated
|
|
204
|
+
* 外部 Agent 允许 reactivate(废弃 → 待审核);发布/废弃由开发者在 Dashboard 操作
|
|
205
|
+
* 外部 Agent 也可以通过 submitKnowledge / submitKnowledgeBatch 提交新条目(→ pending)
|
|
206
|
+
*/
|
|
207
|
+
const MCP_ALLOWED_LIFECYCLE_ACTIONS = new Set(['reactivate']);
|
|
208
|
+
|
|
209
|
+
export async function knowledgeLifecycle(ctx, args) {
|
|
210
|
+
const { id, action } = args;
|
|
211
|
+
if (!id || !action) {
|
|
212
|
+
throw new Error('需要 id 和 action');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!MCP_ALLOWED_LIFECYCLE_ACTIONS.has(action)) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`[PERMISSION_DENIED] 外部 Agent 不允许执行 "${action}" 操作,仅支持: reactivate。发布、废弃等操作请在 Dashboard 中完成。提交新知识请使用 autosnippet_submit_knowledge 工具。`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const service = ctx.container.get('knowledgeService');
|
|
222
|
+
const context = { userId: 'mcp' };
|
|
223
|
+
|
|
224
|
+
const entry = await service.reactivate(id, context);
|
|
225
|
+
|
|
226
|
+
return envelope({
|
|
227
|
+
success: true,
|
|
228
|
+
data: {
|
|
229
|
+
id: entry.id,
|
|
230
|
+
lifecycle: entry.lifecycle,
|
|
231
|
+
title: entry.title,
|
|
232
|
+
action,
|
|
233
|
+
},
|
|
234
|
+
meta: { tool: 'autosnippet_knowledge_lifecycle' },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── 内部辅助 ──────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* V3 wire format → RecipeReadinessChecker 兼容格式
|
|
242
|
+
*/
|
|
243
|
+
function _toReadinessInput(args) {
|
|
244
|
+
return {
|
|
245
|
+
title: args.title,
|
|
246
|
+
code: args.content?.pattern || args.code || '',
|
|
247
|
+
language: args.language,
|
|
248
|
+
category: args.category,
|
|
249
|
+
trigger: args.trigger,
|
|
250
|
+
description: args.description,
|
|
251
|
+
headers: args.headers,
|
|
252
|
+
reasoning: args.reasoning ? {
|
|
253
|
+
whyStandard: args.reasoning.whyStandard,
|
|
254
|
+
sources: args.reasoning.sources,
|
|
255
|
+
confidence: args.reasoning.confidence,
|
|
256
|
+
} : undefined,
|
|
257
|
+
knowledgeType: args.knowledgeType,
|
|
258
|
+
complexity: args.complexity,
|
|
259
|
+
// Cursor Delivery 字段
|
|
260
|
+
kind: args.kind,
|
|
261
|
+
doClause: args.doClause,
|
|
262
|
+
dontClause: args.dontClause,
|
|
263
|
+
whenClause: args.whenClause,
|
|
264
|
+
topicHint: args.topicHint,
|
|
265
|
+
coreCode: args.coreCode,
|
|
266
|
+
};
|
|
267
|
+
}
|