UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

383 lines (382 loc) 16.4 kB
/** * RecipeProductionGateway — 统一 Recipe 生产入口 * * 所有 Recipe 创建(Agent Tool / MCP / IDE Agent / Batch Import) * 通过此 Gateway 的统一管道,保证前置校验一致: * * 1. Schema Validation (UnifiedValidator) * 2. Similarity Check — 去重检测(可选跳过) * 3. Consolidation Scan — 融合/重组建议(可选) * 4. KnowledgeService.create() — 包含 ConfidenceRouter → staging / pending * 5. Quality Scoring — 质量评分 * 6. Supersede Proposal — 创建替代提案 * 7. Audit — 统一审计 */ import { UnifiedValidator } from '#domain/knowledge/UnifiedValidator.js'; /* ═══════════════════ Gateway ═══════════════════ */ export class RecipeProductionGateway { #knowledgeService; #projectRoot; #logger; #consolidationAdvisor; #proposalRepo; #findSimilarRecipes; constructor(deps) { this.#knowledgeService = deps.knowledgeService; this.#projectRoot = deps.projectRoot; this.#logger = deps.logger; this.#consolidationAdvisor = deps.consolidationAdvisor ?? null; this.#proposalRepo = deps.proposalRepository ?? null; this.#findSimilarRecipes = deps.findSimilarRecipes ?? null; } /** * 统一创建入口 * * Pipeline: * 1. Schema Validation (UnifiedValidator) * 2. Similarity Check (除非 skipSimilarityCheck) * 3. Consolidation Scan (除非 skipConsolidation) * 4. KnowledgeService.create() — ConfidenceRouter → staging / pending * 5. Quality Scoring * 6. Supersede Proposal 创建 (if supersedes) */ async create(request) { const { source, items, options = {} } = request; const userId = options.userId || this.#sourceToUserId(source); const result = { created: [], rejected: [], merged: [], blocked: [], duplicates: [], supersedeProposal: null, }; if (items.length === 0) { return result; } // ── Step 1: Schema Validation ── const validator = new UnifiedValidator({ existingTitles: options.existingTitles, existingFingerprints: options.existingFingerprints, }); const validItems = []; for (let i = 0; i < items.length; i++) { const item = items[i]; const validation = validator.validate(item, { systemInjectedFields: options.systemInjectedFields, skipUniqueness: options.skipUniqueness, }); if (!validation.pass) { result.rejected.push({ index: i, title: item.title || '(untitled)', reason: 'validation_failed', errors: validation.errors, warnings: validation.warnings, }); this.#logger?.info(`[Gateway] ✗ validation rejected item ${i}: ${validation.errors.join('; ')}`); } else { validItems.push({ index: i, item }); // 记录已提交标题/指纹以防批量内重复 validator.recordSubmission(item.title, item.content?.pattern); } } // ── Step 2: Similarity Check ── let afterSimilarityItems = validItems; if (!options.skipSimilarityCheck && this.#findSimilarRecipes) { const threshold = options.similarityThreshold ?? 0.7; afterSimilarityItems = []; for (const entry of validItems) { const { item, index } = entry; const contentObj = item.content && typeof item.content === 'object' ? item.content : { markdown: '' }; const cand = { title: item.title || '', summary: item.description || '', code: contentObj.markdown || contentObj.pattern || '', }; const similar = this.#findSimilarRecipes(this.#projectRoot, cand, { threshold: 0.5, topK: 5, }); const hasDuplicate = similar.some((s) => s.similarity >= threshold); if (hasDuplicate) { result.duplicates.push({ index, title: item.title || '(untitled)', similarTo: similar, }); this.#logger?.info(`[Gateway] ✗ duplicate blocked item ${index}: similarity ${similar[0]?.similarity}`); } else { afterSimilarityItems.push(entry); } } } // ── Step 3: Consolidation Scan ── let submittableItems = afterSimilarityItems; if (!options.skipConsolidation && this.#consolidationAdvisor && afterSimilarityItems.length > 0) { submittableItems = []; try { const candidates = afterSimilarityItems.map((e) => ({ title: e.item.title || '', category: e.item.category || e.item._category || '', ...e.item, })); const batchAdvice = this.#consolidationAdvisor.analyzeBatch(candidates); for (let ai = 0; ai < batchAdvice.items.length; ai++) { const { advice } = batchAdvice.items[ai]; const validEntry = afterSimilarityItems[ai]; if (!validEntry) { continue; } if (advice.action === 'create') { submittableItems.push(validEntry); } else if (this.#proposalRepo) { const proposal = this.#createProposalFromAdvice(advice, validEntry.item); if (proposal) { result.merged.push({ index: validEntry.index, proposalId: proposal.proposalId, type: proposal.type, targetRecipeId: proposal.targetRecipeId, targetTitle: proposal.targetTitle, status: proposal.status, expiresAt: proposal.expiresAt, message: proposal.message, }); } else { // Proposal 创建失败 → blocked result.blocked.push({ index: validEntry.index, title: validEntry.item.title || '(untitled)', consolidation: advice, }); } } else { // 无 ProposalRepository → blocked result.blocked.push({ index: validEntry.index, title: validEntry.item.title || '(untitled)', consolidation: advice, }); } } } catch (err) { this.#logger?.warn(`[Gateway] ConsolidationAdvisor error, falling back to direct submit: ${err instanceof Error ? err.message : String(err)}`); submittableItems = afterSimilarityItems; } } // ── Step 4: Create via KnowledgeService ── const createdIds = []; for (const { item } of submittableItems) { try { const data = this.#prepareCreateData(item, source, userId); const saved = await this.#knowledgeService.create(data, { userId }); result.created.push({ id: saved.id, title: saved.title, lifecycle: saved.lifecycle, raw: saved, }); createdIds.push(saved.id); // ── Step 5: Quality Scoring (best effort) ── try { await this.#knowledgeService.updateQuality(saved.id, { userId }); } catch { /* best effort — 不阻塞创建流程 */ } } catch (err) { result.rejected.push({ index: items.indexOf(item), title: item.title || '(untitled)', reason: 'create_failed', errors: [err instanceof Error ? err.message : String(err)], warnings: [], }); this.#logger?.warn(`[Gateway] ✗ create failed for "${item.title}": ${err instanceof Error ? err.message : String(err)}`); } } // ── Step 6: Supersede Proposal ── if (options.supersedes && createdIds.length > 0) { try { // 直接使用 ProposalRepository(Gateway 不依赖 ServiceContainer) if (this.#proposalRepo) { const proposal = this.#proposalRepo.create({ type: 'supersede', targetRecipeId: options.supersedes, relatedRecipeIds: createdIds, confidence: 0.9, source: source === 'mcp-external' ? 'ide-agent' : 'ide-agent', description: `Supersede proposal: ${createdIds.length} new recipe(s) replace ${options.supersedes}`, evidence: [{ snapshotAt: Date.now(), newRecipeIds: createdIds }], }); if (proposal) { result.supersedeProposal = { proposalId: proposal.id }; } } } catch (err) { this.#logger?.warn(`[Gateway] Supersede proposal creation failed: ${err instanceof Error ? err.message : String(err)}`); } } this.#logger?.info(`[Gateway] create complete: ${result.created.length} created, ${result.rejected.length} rejected, ${result.merged.length} merged, ${result.duplicates.length} duplicates | source=${source}`); return result; } /* ═══════════════════ Private ═══════════════════ */ #sourceToUserId(source) { switch (source) { case 'agent-tool': return 'agent'; case 'mcp-external': return 'mcp'; case 'ide-agent': return 'ide-agent'; case 'batch-import': return 'batch-import'; } } #prepareCreateData(item, source, _userId) { const contentObj = item.content && typeof item.content === 'object' ? item.content : { markdown: '', pattern: '' }; const reasoning = item.reasoning || { whyStandard: '', sources: ['agent'], confidence: 0.7, }; if (Array.isArray(reasoning.sources) && reasoning.sources.length === 0) { reasoning.sources = ['agent']; } return { language: item.language || '', category: item.category || item._category || 'general', knowledgeType: item.knowledgeType || 'code-pattern', source: item.source || this.#sourceLabel(source), title: item.title || '', description: item.description || '', tags: item.tags || [], trigger: item.trigger || '', kind: item.kind || 'pattern', topicHint: item.topicHint || '', whenClause: item.whenClause || '', doClause: item.doClause || '', dontClause: item.dontClause || '', coreCode: item.coreCode || contentObj.pattern || '', sourceRefs: item.sourceRefs || [], content: contentObj, reasoning, headers: item.headers || [], usageGuide: item.usageGuide || '', scope: item.scope || '', complexity: item.complexity || '', sourceFile: '', agentNotes: item.agentNotes || null, aiInsight: reasoning.whyStandard || item.description || null, }; } #sourceLabel(source) { switch (source) { case 'agent-tool': return 'agent'; case 'mcp-external': return 'mcp'; case 'ide-agent': return 'ide-agent'; case 'batch-import': return 'batch-import'; } } #createProposalFromAdvice(advice, item) { if (!this.#proposalRepo) { return null; } const evidence = [ { snapshotAt: Date.now(), candidateTitle: item.title, candidateCategory: item.category, analysisReason: advice.reason, mergeDirection: advice.mergeDirection, }, ]; if (advice.action === 'merge' && advice.targetRecipe) { const proposal = this.#proposalRepo.create({ type: 'merge', targetRecipeId: advice.targetRecipe.id, confidence: advice.confidence, source: 'ide-agent', description: advice.reason, evidence, }); if (!proposal) { return null; } return { proposalId: proposal.id, type: 'merge', targetRecipeId: advice.targetRecipe.id, targetTitle: advice.targetRecipe.title, status: proposal.status, expiresAt: proposal.expiresAt, message: `已为「${advice.targetRecipe.title}」创建融合提案,${proposal.status === 'observing' ? '观察窗口 72h 后自动执行' : '等待开发者确认'}。`, }; } if (advice.action === 'reorganize' && advice.reorganizeTargets?.length) { const target = advice.reorganizeTargets[0]; const proposal = this.#proposalRepo.create({ type: 'reorganize', targetRecipeId: target.id, relatedRecipeIds: advice.reorganizeTargets.slice(1).map((t) => t.id), confidence: advice.confidence, source: 'ide-agent', description: advice.reason, evidence, }); if (!proposal) { return null; } return { proposalId: proposal.id, type: 'reorganize', targetRecipeId: target.id, targetTitle: target.title, status: proposal.status, expiresAt: proposal.expiresAt, message: `已为 ${advice.reorganizeTargets.length} 条 Recipe 创建重组提案,需开发者在 Dashboard 确认。`, }; } if (advice.action === 'insufficient' && advice.coveredBy?.length) { const target = advice.coveredBy[0]; const proposal = this.#proposalRepo.create({ type: 'enhance', targetRecipeId: target.id, confidence: advice.confidence, source: 'ide-agent', description: advice.reason, evidence, }); if (!proposal) { return null; } return { proposalId: proposal.id, type: 'enhance', targetRecipeId: target.id, targetTitle: target.title, status: proposal.status, expiresAt: proposal.expiresAt, message: `候选独立价值不足,已创建增强提案建议补充到「${target.title}」。`, }; } return null; } }