autosnippet 2.8.3 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +5 -5
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +9 -27
  11. package/lib/core/ast/ProjectGraph.js +160 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +109 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +125 -0
  19. package/lib/domain/knowledge/values/Content.js +86 -0
  20. package/lib/domain/knowledge/values/Quality.js +93 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  22. package/lib/domain/knowledge/values/Relations.js +168 -0
  23. package/lib/domain/knowledge/values/Stats.js +87 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +48 -0
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
  27. package/lib/external/mcp/McpServer.js +7 -5
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
  29. package/lib/external/mcp/handlers/bootstrap.js +121 -12
  30. package/lib/external/mcp/handlers/browse.js +77 -73
  31. package/lib/external/mcp/handlers/candidate.js +29 -276
  32. package/lib/external/mcp/handlers/guard.js +2 -0
  33. package/lib/external/mcp/handlers/knowledge.js +205 -0
  34. package/lib/external/mcp/handlers/skill.js +4 -2
  35. package/lib/external/mcp/handlers/structure.js +25 -23
  36. package/lib/external/mcp/handlers/system.js +10 -12
  37. package/lib/external/mcp/tools.js +125 -138
  38. package/lib/http/HttpServer.js +4 -8
  39. package/lib/http/middleware/requestLogger.js +3 -3
  40. package/lib/http/routes/ai.js +17 -1
  41. package/lib/http/routes/extract.js +48 -4
  42. package/lib/http/routes/knowledge.js +246 -0
  43. package/lib/http/routes/search.js +12 -17
  44. package/lib/http/routes/skills.js +44 -1
  45. package/lib/infrastructure/cache/GraphCache.js +143 -0
  46. package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
  47. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  48. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  49. package/lib/infrastructure/realtime/RealtimeService.js +14 -2
  50. package/lib/injection/ServiceContainer.js +164 -63
  51. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  52. package/lib/repository/token/TokenUsageStore.js +162 -0
  53. package/lib/service/automation/DirectiveDetector.js +2 -3
  54. package/lib/service/automation/FileWatcher.js +67 -28
  55. package/lib/service/automation/XcodeIntegration.js +931 -156
  56. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  57. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  58. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  59. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  60. package/lib/service/chat/AnalystAgent.js +25 -14
  61. package/lib/service/chat/CandidateGuardrail.js +1 -1
  62. package/lib/service/chat/ChatAgent.js +280 -48
  63. package/lib/service/chat/ContextWindow.js +92 -8
  64. package/lib/service/chat/HandoffProtocol.js +26 -1
  65. package/lib/service/chat/ProducerAgent.js +11 -9
  66. package/lib/service/chat/tools.js +298 -194
  67. package/lib/service/guard/GuardCheckEngine.js +114 -10
  68. package/lib/service/guard/GuardService.js +59 -48
  69. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  70. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  71. package/lib/service/knowledge/KnowledgeService.js +725 -0
  72. package/lib/service/search/SearchEngine.js +92 -19
  73. package/lib/service/skills/SignalCollector.js +15 -9
  74. package/lib/service/skills/SkillAdvisor.js +13 -11
  75. package/lib/service/snippet/SnippetFactory.js +5 -5
  76. package/lib/service/spm/SpmService.js +119 -18
  77. package/package.json +1 -1
  78. package/scripts/install-cursor-skill.js +0 -6
  79. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  80. package/skills/autosnippet-analysis/SKILL.md +15 -7
  81. package/skills/autosnippet-candidates/SKILL.md +6 -6
  82. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  83. package/skills/autosnippet-concepts/SKILL.md +7 -6
  84. package/skills/autosnippet-create/SKILL.md +13 -13
  85. package/skills/autosnippet-intent/SKILL.md +3 -2
  86. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  87. package/skills/autosnippet-recipes/SKILL.md +16 -4
  88. package/templates/constitution.yaml +1 -1
  89. package/templates/copilot-instructions.md +6 -6
  90. package/templates/recipes-setup/README.md +3 -3
  91. package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
  92. package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
  93. package/lib/cli/CandidateSyncService.js +0 -261
  94. package/lib/cli/SyncService.js +0 -356
  95. package/lib/domain/candidate/Candidate.js +0 -196
  96. package/lib/domain/candidate/CandidateRepository.js +0 -107
  97. package/lib/domain/candidate/Reasoning.js +0 -52
  98. package/lib/domain/recipe/Recipe.js +0 -421
  99. package/lib/domain/recipe/RecipeRepository.js +0 -54
  100. package/lib/domain/types/CandidateStatus.js +0 -52
  101. package/lib/http/routes/candidates.js +0 -559
  102. package/lib/http/routes/recipes.js +0 -397
  103. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  104. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  105. package/lib/service/candidate/CandidateAggregator.js +0 -52
  106. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  107. package/lib/service/candidate/CandidateService.js +0 -973
  108. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  109. package/lib/service/recipe/RecipeService.js +0 -786
  110. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -1,65 +1,70 @@
1
1
  /**
2
- * MCP Handlers — 知识浏览类
3
- * listByKind, listRecipes, getRecipe, recipeInsights, complianceReport, confirmUsage
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 recipeService = ctx.container.get('recipeService');
25
+ const ks = ctx.container.get('knowledgeService');
10
26
  const filters = { kind };
11
- if (args.status) filters.status = args.status;
12
- if (args.language) filters.language = args.language;
13
- if (args.category) filters.category = args.category;
14
- const result = await recipeService.listRecipes(filters, { page: 1, pageSize: args.limit || 20 });
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 recipeService = ctx.container.get('recipeService');
35
+ const ks = ctx.container.get('knowledgeService');
27
36
  const filters = {};
28
- if (args.kind) filters.kind = args.kind;
29
- if (args.language) filters.language = args.language;
30
- if (args.category) filters.category = args.category;
31
- if (args.knowledgeType) filters.knowledgeType = args.knowledgeType;
32
- if (args.status) filters.status = args.status;
33
- if (args.complexity) filters.complexity = args.complexity;
34
- const result = await recipeService.listRecipes(filters, { page: 1, pageSize: args.limit || 20 });
35
- const items = (result?.data || result?.items || []).map(r => ({
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 recipeService = ctx.container.get('recipeService');
48
- const recipe = await recipeService.getRecipe(args.id);
49
- if (!recipe) throw new Error(`Recipe not found: ${args.id}`);
50
- return envelope({ success: true, data: recipe, meta: { tool: 'autosnippet_get_recipe' } });
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 recipeService = ctx.container.get('recipeService');
56
- const recipe = await recipeService.getRecipe(args.id);
57
- if (!recipe) throw new Error(`Recipe not found: ${args.id}`);
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 (recipe.relations) {
62
- for (const [type, targets] of Object.entries(recipe.relations)) {
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 (recipe.constraints) {
72
- for (const [type, items] of Object.entries(recipe.constraints)) {
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: recipe.id,
81
- title: recipe.title,
82
- trigger: recipe.trigger || '',
83
- kind: recipe.kind,
84
- status: recipe.status,
85
- language: recipe.language,
86
- category: recipe.category,
87
- knowledgeType: recipe.knowledgeType,
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: recipe.quality?.overall ?? null,
90
- codeCompleteness: recipe.quality?.codeCompleteness ?? null,
91
- projectAdaptation: recipe.quality?.projectAdaptation ?? null,
92
- documentationClarity: recipe.quality?.documentationClarity ?? null,
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
- statistics: {
95
- adoptionCount: recipe.statistics?.adoptionCount ?? 0,
96
- applicationCount: recipe.statistics?.applicationCount ?? 0,
97
- guardHitCount: recipe.statistics?.guardHitCount ?? 0,
98
- viewCount: recipe.statistics?.viewCount ?? 0,
99
- successCount: recipe.statistics?.successCount ?? 0,
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: !!recipe.content?.pattern,
104
- hasRationale: !!recipe.content?.rationale,
105
- hasMarkdown: !!recipe.content?.markdown,
106
- stepsCount: recipe.content?.steps?.length ?? 0,
107
- codeChangesCount: recipe.content?.codeChanges?.length ?? 0,
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: recipe.tags || [],
112
- complexity: recipe.complexity,
113
- scope: recipe.scope,
114
- createdBy: recipe.createdBy,
115
- createdAt: recipe.createdAt,
116
- updatedAt: recipe.updatedAt,
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 recipeService = ctx.container.get('recipeService');
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 recipeService.incrementUsage(args.recipeId, usageType, {
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: `已记录 Recipe ${args.recipeId} 的${usageType === 'adoption' ? '采纳' : '应用'}`,
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 — 候选提交 & 校验 & AI 补全
3
- * validateCandidate, checkDuplicate, submitSingle, submitBatch, submitDrafts, enrichCandidates
4
- * + 辅助: buildReasoning, _createCandidateItem
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[], sideEffects[], guards[]}' });
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 candidateService = ctx.container.get('candidateService');
331
- if (!candidateService) throw new Error('CandidateService not available');
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', '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: 'summary', check: v => !!v, hint: '中文摘要(summary / summary_cn)' },
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: 'usageGuide', check: v => !!v, hint: '使用指南(Markdown ### 章节)' },
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 candidate = await candidateService.candidateRepository.findById(id);
350
- if (!candidate) {
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 meta = candidate.metadata || {};
103
+ const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
355
104
 
356
105
  // 语义字段检查
357
106
  const missing = [];
358
- for (const key of SEMANTIC_KEYS) {
359
- const val = meta[key];
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(key);
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 = key === 'category' ? candidate.category : meta[key];
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: meta.title || '',
381
- language: candidate.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
- candidates: results,
153
+ entries: results,
401
154
  hint: (needsEnrichment > 0 || needsRecipeFields > 0)
402
- ? '请 Agent 根据 missingFields(语义)和 recipeReadyMissing(Recipe 必填)自行补全后重新提交'
403
- : '所有候选字段完整,可直接审核为 Recipe',
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' },