UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

225 lines (224 loc) 8.45 kB
/** * MCP Handlers — 候选校验 & 字段诊断 (V3: 使用 knowledgeService) * validateCandidate, checkDuplicate, enrichCandidates * * 注意: submitSingle, submitBatch, submitDrafts 已移至 V3 knowledge handlers * (autosnippet_submit_knowledge / submit_knowledge_batch / knowledge_lifecycle) */ import { resolveProjectRoot } from '#shared/resolveProjectRoot.js'; import { envelope } from '../envelope.js'; // ─── 校验 & 去重 ─────────────────────────────────────────── export async function validateCandidate(ctx, args) { // Cast to CandidateInput — Agent input is runtime-dynamic, validation checks shape const c = (args.candidate || {}); const errors = []; const warnings = []; const suggestions = []; // Layer 1: 核心必填 if (!c.title?.trim()) { errors.push('缺少 title'); } if (!c.code?.trim() && args.strict) { errors.push('strict 模式下需要 code'); } if (!c.language) { warnings.push('缺少 language'); } // Layer 2: 分类 if (!c.category) { warnings.push('缺少 category'); } if (!c.knowledgeType) { warnings.push('缺少 knowledgeType(code-pattern/architecture/best-practice/...)'); } if (!c.complexity) { suggestions.push({ field: 'complexity', value: 'intermediate' }); } // Layer 3: 描述文档 if (!c.trigger?.trim()) { warnings.push('缺少 trigger(建议 @ 开头)'); } if (c.trigger && !c.trigger.startsWith('@')) { suggestions.push({ field: 'trigger', value: `@${c.trigger.replace(/^@+/, '')}` }); } if (!c.summary?.trim() && !c.description?.trim()) { warnings.push('缺少 summary 或 description'); } if (!c.usageGuide?.trim()) { warnings.push('缺少 usageGuide'); } // Layer 4: 结构化内容 if (!c.rationale) { warnings.push('缺少 rationale(设计原理)'); } if (!Array.isArray(c.headers) || c.headers.length === 0) { warnings.push('缺少 headers(import 声明)'); } if (!c.steps && !c.codeChanges) { suggestions.push({ field: 'steps', value: '[{title, description, code}]' }); } // Layer 5: 约束与关系 if (!c.constraints) { suggestions.push({ field: 'constraints', value: '{boundaries[], preconditions[], sideEffects[], guards[]}', }); } // Reasoning 推理依据 if (!c.reasoning) { errors.push('缺少 reasoning(推理依据 — whyStandard + sources + confidence)'); } else { if (!c.reasoning.whyStandard?.trim()) { errors.push('reasoning.whyStandard 不能为空'); } if (!Array.isArray(c.reasoning.sources) || c.reasoning.sources.length === 0) { errors.push('reasoning.sources 至少包含一项来源'); } if (typeof c.reasoning.confidence !== 'number' || c.reasoning.confidence < 0 || c.reasoning.confidence > 1) { warnings.push('reasoning.confidence 应为 0-1 的数字'); } } const ok = errors.length === 0; return envelope({ success: ok, data: { ok, errors, warnings, suggestions }, meta: { tool: 'autosnippet_validate_candidate' }, }); } export async function checkDuplicate(ctx, args) { // SimilarityService 直接读磁盘 .md 文件,不依赖 Repository const { findSimilarRecipes } = await import('#service/candidate/SimilarityService.js'); const projectRoot = resolveProjectRoot(ctx.container); const candidate = (args.candidate ?? {}); const similar = findSimilarRecipes(projectRoot, candidate, { threshold: args.threshold ?? 0.7, topK: args.topK ?? 5, }); return envelope({ success: true, data: { similar }, meta: { tool: 'autosnippet_check_duplicate' }, }); } // ─── 语义字段缺失诊断(无 AI 依赖) ────────────────────────── export async function enrichCandidates(ctx, args) { const ids = args.candidateIds; if (!Array.isArray(ids) || ids.length === 0) { throw new Error('candidateIds array is required and must not be empty'); } if (ids.length > 20) { throw new Error('Max 20 candidates per enrichment call'); } const knowledgeService = ctx.container.get('knowledgeService'); if (!knowledgeService) { throw new Error('KnowledgeService not available'); } const SEMANTIC_KEYS = [ 'content.rationale', 'knowledgeType', 'complexity', 'scope', 'content.steps', 'constraints', ]; const RECIPE_READY_KEYS = [ { key: 'category', check: (v) => typeof v === 'string' && ['View', 'Service', 'Tool', 'Model', 'Network', 'Storage', 'UI', 'Utility'].includes(v), hint: 'category 必须为 8 标准值之一', }, { key: 'trigger', check: (v) => typeof v === 'string' && v.startsWith('@'), hint: 'trigger 必须以 @ 开头', }, { key: 'description', check: (v) => !!v, hint: '知识条目描述' }, { key: 'headers', check: (v) => Array.isArray(v) && v.length > 0, hint: '完整 import 语句数组', }, ]; const results = []; let needsEnrichment = 0; let needsRecipeFields = 0; for (const id of ids) { try { const entry = await knowledgeService.get(id); if (!entry) { results.push({ id, found: false, missingFields: [], recipeReadyMissing: [] }); continue; } const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry; // 语义字段检查 const missing = []; for (const keyPath of SEMANTIC_KEYS) { const parts = keyPath.split('.'); let val = json; for (const p of parts) { val = val?.[p]; } if (val === undefined || val === null || val === '' || (typeof val === 'string' && val.trim() === '') || (Array.isArray(val) && val.length === 0) || (typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0)) { missing.push(keyPath); } } // Recipe-Ready 字段检查 const recipeReadyMissing = []; for (const { key, check, hint } of RECIPE_READY_KEYS) { const val = json[key]; if (!check(val)) { recipeReadyMissing.push({ field: key, hint }); } } results.push({ id, found: true, title: json.title || '', language: json.language, lifecycle: json.lifecycle, kind: json.kind, missingFields: missing, recipeReadyMissing, complete: missing.length === 0 && recipeReadyMissing.length === 0, }); if (missing.length > 0) { needsEnrichment++; } if (recipeReadyMissing.length > 0) { needsRecipeFields++; } } catch (err) { results.push({ id, found: false, error: err instanceof Error ? err.message : String(err), missingFields: [], recipeReadyMissing: [], }); } } return envelope({ success: true, data: { total: ids.length, needsEnrichment, needsRecipeFields, fullyComplete: ids.length - Math.max(needsEnrichment, needsRecipeFields), entries: results, hint: needsEnrichment > 0 || needsRecipeFields > 0 ? '请 Agent 根据 missingFields(语义)和 recipeReadyMissing(必填)自行补全后重新提交' : '所有条目字段完整', }, meta: { tool: 'autosnippet_enrich_candidates' }, }); }