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
|
@@ -1,65 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP Handlers — 知识浏览类
|
|
3
|
-
* listByKind, listRecipes, getRecipe, recipeInsights,
|
|
2
|
+
* MCP Handlers — 知识浏览类 (V3: 使用 knowledgeService)
|
|
3
|
+
* listByKind, listRecipes, getRecipe, recipeInsights, confirmUsage
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { envelope } from '../envelope.js';
|
|
7
7
|
|
|
8
|
+
/** 将 KnowledgeEntry 的 V3 字段投影为列表摘要 */
|
|
9
|
+
function _projectItem(r) {
|
|
10
|
+
const json = typeof r.toJSON === 'function' ? r.toJSON() : r;
|
|
11
|
+
return {
|
|
12
|
+
id: json.id, title: json.title, description: json.description,
|
|
13
|
+
trigger: json.trigger || '', lifecycle: json.lifecycle, kind: json.kind,
|
|
14
|
+
language: json.language, category: json.category,
|
|
15
|
+
knowledge_type: json.knowledge_type, complexity: json.complexity,
|
|
16
|
+
scope: json.scope, tags: json.tags || [],
|
|
17
|
+
quality: json.quality || null, stats: json.stats || null,
|
|
18
|
+
// 兼容旧字段名
|
|
19
|
+
status: json.lifecycle, knowledgeType: json.knowledge_type,
|
|
20
|
+
statistics: json.stats,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
export async function listByKind(ctx, kind, args) {
|
|
9
|
-
const
|
|
25
|
+
const ks = ctx.container.get('knowledgeService');
|
|
10
26
|
const filters = { kind };
|
|
11
|
-
if (args.
|
|
12
|
-
if (args.
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const items = (result?.data || result?.items || []).map(r => ({
|
|
16
|
-
id: r.id, title: r.title || r.name, description: r.description,
|
|
17
|
-
trigger: r.trigger || '', status: r.status, language: r.language, category: r.category,
|
|
18
|
-
knowledgeType: r.knowledgeType, kind: r.kind,
|
|
19
|
-
complexity: r.complexity, scope: r.scope, tags: r.tags || [],
|
|
20
|
-
quality: r.quality || null, statistics: r.statistics || null,
|
|
21
|
-
}));
|
|
27
|
+
if (args.language) filters.language = args.language;
|
|
28
|
+
if (args.category) filters.category = args.category;
|
|
29
|
+
const result = await ks.list(filters, { page: 1, pageSize: args.limit || 20 });
|
|
30
|
+
const items = (result?.data || []).map(_projectItem);
|
|
22
31
|
return envelope({ success: true, data: { kind, count: items.length, total: result?.pagination?.total || items.length, items }, meta: { tool: `autosnippet_list_${kind}s` } });
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export async function listRecipes(ctx, args) {
|
|
26
|
-
const
|
|
35
|
+
const ks = ctx.container.get('knowledgeService');
|
|
27
36
|
const filters = {};
|
|
28
|
-
if (args.kind)
|
|
29
|
-
if (args.language)
|
|
30
|
-
if (args.category)
|
|
31
|
-
if (args.knowledgeType)
|
|
32
|
-
if (args.
|
|
33
|
-
if (args.
|
|
34
|
-
const result = await
|
|
35
|
-
const items = (result?.data ||
|
|
36
|
-
id: r.id, title: r.title, description: r.description,
|
|
37
|
-
trigger: r.trigger || '', status: r.status, language: r.language, category: r.category,
|
|
38
|
-
kind: r.kind, knowledgeType: r.knowledgeType, complexity: r.complexity,
|
|
39
|
-
scope: r.scope, tags: r.tags,
|
|
40
|
-
quality: r.quality, statistics: r.statistics,
|
|
41
|
-
}));
|
|
37
|
+
if (args.kind) filters.kind = args.kind;
|
|
38
|
+
if (args.language) filters.language = args.language;
|
|
39
|
+
if (args.category) filters.category = args.category;
|
|
40
|
+
if (args.knowledgeType) filters.knowledgeType = args.knowledgeType;
|
|
41
|
+
if (args.complexity) filters.complexity = args.complexity;
|
|
42
|
+
if (args.status) filters.lifecycle = args.status;
|
|
43
|
+
const result = await ks.list(filters, { page: 1, pageSize: args.limit || 20 });
|
|
44
|
+
const items = (result?.data || []).map(_projectItem);
|
|
42
45
|
return envelope({ success: true, data: { count: items.length, total: result?.pagination?.total || items.length, items }, meta: { tool: 'autosnippet_list_recipes' } });
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export async function getRecipe(ctx, args) {
|
|
46
49
|
if (!args.id) throw new Error('id is required');
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
|
|
50
|
+
const ks = ctx.container.get('knowledgeService');
|
|
51
|
+
const entry = await ks.get(args.id);
|
|
52
|
+
if (!entry) throw new Error(`Knowledge entry not found: ${args.id}`);
|
|
53
|
+
const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
|
|
54
|
+
return envelope({ success: true, data: json, meta: { tool: 'autosnippet_get_recipe' } });
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
export async function recipeInsights(ctx, args) {
|
|
54
58
|
if (!args.id) throw new Error('id is required');
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
59
|
+
const ks = ctx.container.get('knowledgeService');
|
|
60
|
+
const entry = await ks.get(args.id);
|
|
61
|
+
if (!entry) throw new Error(`Knowledge entry not found: ${args.id}`);
|
|
62
|
+
const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
|
|
58
63
|
|
|
59
64
|
// 聚合关系摘要
|
|
60
65
|
const relationsSummary = {};
|
|
61
|
-
if (
|
|
62
|
-
for (const [type, targets] of Object.entries(
|
|
66
|
+
if (json.relations) {
|
|
67
|
+
for (const [type, targets] of Object.entries(json.relations)) {
|
|
63
68
|
if (Array.isArray(targets) && targets.length > 0) {
|
|
64
69
|
relationsSummary[type] = targets.length;
|
|
65
70
|
}
|
|
@@ -68,8 +73,8 @@ export async function recipeInsights(ctx, args) {
|
|
|
68
73
|
|
|
69
74
|
// 约束条件概览
|
|
70
75
|
const constraintsSummary = {};
|
|
71
|
-
if (
|
|
72
|
-
for (const [type, items] of Object.entries(
|
|
76
|
+
if (json.constraints) {
|
|
77
|
+
for (const [type, items] of Object.entries(json.constraints)) {
|
|
73
78
|
if (Array.isArray(items) && items.length > 0) {
|
|
74
79
|
constraintsSummary[type] = items;
|
|
75
80
|
}
|
|
@@ -77,43 +82,42 @@ export async function recipeInsights(ctx, args) {
|
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
const insights = {
|
|
80
|
-
id:
|
|
81
|
-
title:
|
|
82
|
-
trigger:
|
|
83
|
-
kind:
|
|
84
|
-
|
|
85
|
-
language:
|
|
86
|
-
category:
|
|
87
|
-
|
|
85
|
+
id: json.id,
|
|
86
|
+
title: json.title,
|
|
87
|
+
trigger: json.trigger || '',
|
|
88
|
+
kind: json.kind,
|
|
89
|
+
lifecycle: json.lifecycle,
|
|
90
|
+
language: json.language,
|
|
91
|
+
category: json.category,
|
|
92
|
+
knowledge_type: json.knowledge_type,
|
|
88
93
|
quality: {
|
|
89
|
-
overall:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
overall: json.quality?.overall ?? null,
|
|
95
|
+
completeness: json.quality?.completeness ?? null,
|
|
96
|
+
adaptation: json.quality?.adaptation ?? null,
|
|
97
|
+
documentation: json.quality?.documentation ?? null,
|
|
93
98
|
},
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
feedbackScore: recipe.statistics?.feedbackScore ?? 0,
|
|
99
|
+
stats: {
|
|
100
|
+
adoptions: json.stats?.adoptions ?? 0,
|
|
101
|
+
applications: json.stats?.applications ?? 0,
|
|
102
|
+
guard_hits: json.stats?.guard_hits ?? 0,
|
|
103
|
+
views: json.stats?.views ?? 0,
|
|
104
|
+
search_hits: json.stats?.search_hits ?? 0,
|
|
101
105
|
},
|
|
102
106
|
content: {
|
|
103
|
-
hasPattern: !!
|
|
104
|
-
hasRationale: !!
|
|
105
|
-
hasMarkdown: !!
|
|
106
|
-
stepsCount:
|
|
107
|
-
codeChangesCount:
|
|
107
|
+
hasPattern: !!json.content?.pattern,
|
|
108
|
+
hasRationale: !!json.content?.rationale,
|
|
109
|
+
hasMarkdown: !!json.content?.markdown,
|
|
110
|
+
stepsCount: json.content?.steps?.length ?? 0,
|
|
111
|
+
codeChangesCount: json.content?.code_changes?.length ?? 0,
|
|
108
112
|
},
|
|
109
113
|
relations: relationsSummary,
|
|
110
114
|
constraints: constraintsSummary,
|
|
111
|
-
tags:
|
|
112
|
-
complexity:
|
|
113
|
-
scope:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
tags: json.tags || [],
|
|
116
|
+
complexity: json.complexity,
|
|
117
|
+
scope: json.scope,
|
|
118
|
+
created_by: json.created_by,
|
|
119
|
+
created_at: json.created_at,
|
|
120
|
+
updated_at: json.updated_at,
|
|
117
121
|
};
|
|
118
122
|
|
|
119
123
|
return envelope({ success: true, data: insights, meta: { tool: 'autosnippet_recipe_insights' } });
|
|
@@ -121,11 +125,11 @@ export async function recipeInsights(ctx, args) {
|
|
|
121
125
|
|
|
122
126
|
export async function confirmUsage(ctx, args) {
|
|
123
127
|
if (!args.recipeId) throw new Error('recipeId is required');
|
|
124
|
-
const
|
|
128
|
+
const ks = ctx.container.get('knowledgeService');
|
|
125
129
|
const usageType = args.usageType || 'adoption';
|
|
126
130
|
const feedback = args.feedback || null;
|
|
127
131
|
|
|
128
|
-
await
|
|
132
|
+
await ks.incrementUsage(args.recipeId, usageType, {
|
|
129
133
|
feedback,
|
|
130
134
|
actor: 'mcp_user',
|
|
131
135
|
});
|
|
@@ -146,7 +150,7 @@ export async function confirmUsage(ctx, args) {
|
|
|
146
150
|
return envelope({
|
|
147
151
|
success: true,
|
|
148
152
|
data: { recipeId: args.recipeId, usageType, feedback },
|
|
149
|
-
message:
|
|
153
|
+
message: `已记录使用 ${args.recipeId} 的${usageType === 'adoption' ? '采纳' : '应用'}`,
|
|
150
154
|
meta: { tool: 'autosnippet_confirm_usage' },
|
|
151
155
|
});
|
|
152
156
|
}
|
|
@@ -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
|
|
|
@@ -88,13 +40,13 @@ export async function validateCandidate(ctx, args) {
|
|
|
88
40
|
if (!c.steps && !c.codeChanges) suggestions.push({ field: 'steps', value: '[{title, description, code}]' });
|
|
89
41
|
|
|
90
42
|
// Layer 5: 约束与关系
|
|
91
|
-
if (!c.constraints) suggestions.push({ field: 'constraints', value: '{boundaries[], preconditions[],
|
|
43
|
+
if (!c.constraints) suggestions.push({ field: 'constraints', value: '{boundaries[], preconditions[], side_effects[], guards[]}' });
|
|
92
44
|
|
|
93
45
|
// Reasoning 推理依据
|
|
94
46
|
if (!c.reasoning) {
|
|
95
47
|
errors.push('缺少 reasoning(推理依据 — whyStandard + sources + confidence)');
|
|
96
48
|
} else {
|
|
97
|
-
if (!c.reasoning.whyStandard?.trim()) errors.push('reasoning.whyStandard 不能为空');
|
|
49
|
+
if (!c.reasoning.whyStandard?.trim() && !c.reasoning.why_standard?.trim()) errors.push('reasoning.whyStandard 不能为空');
|
|
98
50
|
if (!Array.isArray(c.reasoning.sources) || c.reasoning.sources.length === 0) errors.push('reasoning.sources 至少包含一项来源');
|
|
99
51
|
if (typeof c.reasoning.confidence !== 'number' || c.reasoning.confidence < 0 || c.reasoning.confidence > 1) warnings.push('reasoning.confidence 应为 0-1 的数字');
|
|
100
52
|
}
|
|
@@ -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,17 @@ 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', '
|
|
334
|
-
// Recipe-Ready 必填字段(category/trigger/summary*/headers 等)
|
|
83
|
+
const SEMANTIC_KEYS = ['content.rationale', 'knowledge_type', '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: '
|
|
87
|
+
{ key: 'summary_cn', check: v => !!v, hint: '中文摘要' },
|
|
339
88
|
{ key: 'summary_en', check: v => !!v, hint: '英文摘要' },
|
|
340
89
|
{ key: 'headers', check: v => Array.isArray(v) && v.length > 0, hint: '完整 import 语句数组' },
|
|
341
|
-
{ key: '
|
|
90
|
+
{ key: 'usage_guide_cn', check: v => !!v, hint: '使用指南(Markdown)' },
|
|
342
91
|
];
|
|
343
92
|
|
|
344
93
|
const results = [];
|
|
@@ -346,29 +95,31 @@ export async function enrichCandidates(ctx, args) {
|
|
|
346
95
|
let needsRecipeFields = 0;
|
|
347
96
|
for (const id of ids) {
|
|
348
97
|
try {
|
|
349
|
-
const
|
|
350
|
-
if (!
|
|
98
|
+
const entry = await knowledgeService.get(id);
|
|
99
|
+
if (!entry) {
|
|
351
100
|
results.push({ id, found: false, missingFields: [], recipeReadyMissing: [] });
|
|
352
101
|
continue;
|
|
353
102
|
}
|
|
354
|
-
const
|
|
103
|
+
const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
|
|
355
104
|
|
|
356
105
|
// 语义字段检查
|
|
357
106
|
const missing = [];
|
|
358
|
-
for (const
|
|
359
|
-
const
|
|
107
|
+
for (const keyPath of SEMANTIC_KEYS) {
|
|
108
|
+
const parts = keyPath.split('.');
|
|
109
|
+
let val = json;
|
|
110
|
+
for (const p of parts) { val = val?.[p]; }
|
|
360
111
|
if (val === undefined || val === null || val === '' ||
|
|
361
112
|
(typeof val === 'string' && val.trim() === '') ||
|
|
362
113
|
(Array.isArray(val) && val.length === 0) ||
|
|
363
114
|
(typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0)) {
|
|
364
|
-
missing.push(
|
|
115
|
+
missing.push(keyPath);
|
|
365
116
|
}
|
|
366
117
|
}
|
|
367
118
|
|
|
368
119
|
// Recipe-Ready 字段检查
|
|
369
120
|
const recipeReadyMissing = [];
|
|
370
121
|
for (const { key, check, hint } of RECIPE_READY_KEYS) {
|
|
371
|
-
const val =
|
|
122
|
+
const val = json[key];
|
|
372
123
|
if (!check(val)) {
|
|
373
124
|
recipeReadyMissing.push({ field: key, hint });
|
|
374
125
|
}
|
|
@@ -377,8 +128,10 @@ export async function enrichCandidates(ctx, args) {
|
|
|
377
128
|
results.push({
|
|
378
129
|
id,
|
|
379
130
|
found: true,
|
|
380
|
-
title:
|
|
381
|
-
language:
|
|
131
|
+
title: json.title || '',
|
|
132
|
+
language: json.language,
|
|
133
|
+
lifecycle: json.lifecycle,
|
|
134
|
+
kind: json.kind,
|
|
382
135
|
missingFields: missing,
|
|
383
136
|
recipeReadyMissing,
|
|
384
137
|
complete: missing.length === 0 && recipeReadyMissing.length === 0,
|
|
@@ -397,10 +150,10 @@ export async function enrichCandidates(ctx, args) {
|
|
|
397
150
|
needsEnrichment,
|
|
398
151
|
needsRecipeFields,
|
|
399
152
|
fullyComplete: ids.length - Math.max(needsEnrichment, needsRecipeFields),
|
|
400
|
-
|
|
153
|
+
entries: results,
|
|
401
154
|
hint: (needsEnrichment > 0 || needsRecipeFields > 0)
|
|
402
|
-
? '请 Agent 根据 missingFields(语义)和 recipeReadyMissing
|
|
403
|
-
: '
|
|
155
|
+
? '请 Agent 根据 missingFields(语义)和 recipeReadyMissing(必填)自行补全后重新提交'
|
|
156
|
+
: '所有条目字段完整',
|
|
404
157
|
},
|
|
405
158
|
meta: { tool: 'autosnippet_enrich_candidates' },
|
|
406
159
|
});
|
|
@@ -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' },
|