UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

534 lines (533 loc) 29 kB
/** * MCP Handler — 维度完成通知 (dimension_complete) * * 外部 Agent 完成一个维度的分析后调用此 handler: * 1. Recipe 关联 — 标记 submit_knowledge 提交的 recipes 属于此维度 * 2. Skill 生成 — skillWorthy 维度自动生成 Project Skill * 3. Checkpoint 保存 — 持久化进度,支持断点续传 * 4. EpisodicMemory 写入 — 供 Dashboard 和增量 Bootstrap * 5. SemanticMemory 固化 — 提炼为项目级永久记忆 * 6. 进度推送 — Socket.io/EventBus 通知 Dashboard * 7. Hints 收集与分发 — 跨维度知识传递 * * 幂等性:同一 dimensionId 多次调用 → 覆盖更新,不产生重复 * * @module handlers/dimension-complete */ import Logger from '#infra/logging/Logger.js'; import { BootstrapEventEmitter } from '#service/bootstrap/BootstrapEventEmitter.js'; import { getDeveloperIdentity } from '#shared/developer-identity.js'; import { envelope } from '../envelope.js'; import { saveDimensionCheckpoint } from './bootstrap/pipeline/checkpoint.js'; import { BOOTSTRAP_COMPLETE_ACTIONS } from './bootstrap/shared/dimension-text.js'; import { generateSkill } from './bootstrap/shared/skill-generator.js'; import { getActiveSession } from './bootstrap-external.js'; const logger = Logger.getInstance(); export async function dimensionComplete(ctx, args) { const t0 = Date.now(); const { sessionId, dimensionId, submittedRecipeIds: rawSubmittedRecipeIds = [], analysisText, referencedFiles: rawReferencedFiles = [], keyFindings = [], candidateCount, crossDimensionHints, } = args; // referencedFiles / submittedRecipeIds 自动补全:若 Agent 未传递, // 从 SubmissionTracker 已记录的数据中恢复 let referencedFiles = rawReferencedFiles; let submittedRecipeIds = rawSubmittedRecipeIds; // ── 参数校验 ── if (!dimensionId) { return envelope({ success: false, message: 'Missing required parameter: dimensionId', errorCode: 'VALIDATION_ERROR', meta: { tool: 'autosnippet_dimension_complete' }, }); } if (!analysisText || analysisText.length < 10) { return envelope({ success: false, message: 'analysisText is required and must be at least 10 characters', errorCode: 'VALIDATION_ERROR', meta: { tool: 'autosnippet_dimension_complete' }, }); } if (!Array.isArray(submittedRecipeIds)) { return envelope({ success: false, message: 'submittedRecipeIds must be an array of recipe ID strings', errorCode: 'VALIDATION_ERROR', meta: { tool: 'autosnippet_dimension_complete' }, }); } // ── 获取 Session ── const session = getActiveSession(ctx.container, sessionId); if (!session) { return envelope({ success: false, message: sessionId ? `No active bootstrap session found with id: ${sessionId}` : 'No active bootstrap session. Call autosnippet_bootstrap first.', errorCode: 'SESSION_NOT_FOUND', meta: { tool: 'autosnippet_dimension_complete' }, }); } // R11: Session TTL 自动延长 — 每次 dimension_complete 调用时至少再延 1h if (session.expiresAt) { session.expiresAt = Math.max(session.expiresAt, Date.now() + 60 * 60 * 1000); } // ── 查找维度定义 ── const dim = session.dimensions.find((d) => d.id === dimensionId); if (!dim) { return envelope({ success: false, message: `Unknown dimensionId: "${dimensionId}". Valid dimensions: ${session.dimensions.map((d) => d.id).join(', ')}`, errorCode: 'VALIDATION_ERROR', meta: { tool: 'autosnippet_dimension_complete' }, }); } const projectRoot = session.projectRoot; // ── referencedFiles 自动补全 ── // 外部 Agent 常常忘记传 referencedFiles,从 SubmissionTracker 的 reasoning.sources 中恢复 if (referencedFiles.length === 0) { try { const submissions = session.submissionTracker.getSubmissions(dimensionId); const filesFromSources = new Set(); for (const sub of submissions) { for (const src of sub.sources) { // "BDVideoPlayer.h:37" → "BDVideoPlayer.h" filesFromSources.add(src.split(':')[0]); } } if (filesFromSources.size > 0) { referencedFiles = [...filesFromSources]; logger.debug(`[DimensionComplete] Auto-recovered ${referencedFiles.length} referencedFiles from submissions for "${dimensionId}"`); } } catch { /* best effort */ } } // ── submittedRecipeIds 自动补全 ── // 外部 Agent 常常忘记传 submittedRecipeIds(batch 接口不返回 ID 列表), // 从 SubmissionTracker 已记录的 recipeId 中恢复 if (submittedRecipeIds.length === 0) { try { const submissions = session.submissionTracker.getSubmissions(dimensionId); const recoveredIds = submissions .map((s) => s.recipeId) .filter((id) => Boolean(id)); if (recoveredIds.length > 0) { submittedRecipeIds = recoveredIds; logger.debug(`[DimensionComplete] Auto-recovered ${submittedRecipeIds.length} submittedRecipeIds from tracker for "${dimensionId}"`); } } catch { /* best effort */ } } // ═══════════════════════════════════════════════════════════ // 1. Recipe 关联 — 标记 recipes 的 dimensionId // ═══════════════════════════════════════════════════════════ let recipesBound = 0; if (submittedRecipeIds.length > 0) { try { const knowledgeService = ctx.container.get('knowledgeService'); if (knowledgeService) { for (const recipeId of submittedRecipeIds) { try { // 通过 updatable 字段标记 recipe 的维度关联 const entry = await knowledgeService.get(recipeId); if (entry) { let existingTags = []; if (Array.isArray(entry.tags)) { existingTags = entry.tags; } else if (typeof entry.tags === 'string') { try { const parsed = JSON.parse(entry.tags); existingTags = Array.isArray(parsed) ? parsed : []; } catch { // tags 不是有效 JSON,尝试按逗号分割 existingTags = entry.tags .split(',') .map((t) => t.trim()) .filter(Boolean); } } const newTags = [ ...new Set([ ...existingTags, `dimension:${dimensionId}`, `bootstrap:${session.id}`, ]), ]; await knowledgeService.update(recipeId, { category: dimensionId, tags: newTags, }, { userId: getDeveloperIdentity() }); recipesBound++; } } catch (e) { logger.debug(`[DimensionComplete] Failed to tag recipe ${recipeId}: ${e instanceof Error ? e.message : String(e)}`); } } } } catch (e) { logger.warn(`[DimensionComplete] Recipe tagging failed (degraded): ${e instanceof Error ? e.message : String(e)}`); } } // ═══════════════════════════════════════════════════════════ // 2. Skill 生成 (skillWorthy 维度) — 使用共享 skill-generator // 如果 analysisText 太短,自动从已提交的候选知识中合成结构化内容 // ═══════════════════════════════════════════════════════════ let skillCreated = false; if (dim.skillWorthy) { let effectiveAnalysis = analysisText; // 当 analysisText 不足以通过质量门控时,从候选知识中合成 if (analysisText.length < 500 && submittedRecipeIds.length > 0) { try { const knowledgeService = ctx.container.get('knowledgeService'); if (knowledgeService) { const parts = [`## ${dim.label || dimensionId} — 分析报告\n`]; if (analysisText.trim().length > 0) { parts.push(analysisText.trim(), ''); } for (const recipeId of submittedRecipeIds) { const entry = await knowledgeService.get(recipeId); if (!entry) { continue; } parts.push(`### ${entry.title || 'Untitled'}`); if (entry.description) { parts.push(entry.description); } if (entry.whenClause || entry.doClause || entry.dontClause) { parts.push(''); if (entry.whenClause) { parts.push(`- **When**: ${entry.whenClause}`); } if (entry.doClause) { parts.push(`- **Do**: ${entry.doClause}`); } if (entry.dontClause) { parts.push(`- **Don't**: ${entry.dontClause}`); } } if (entry.coreCode) { parts.push('', '```', entry.coreCode.substring(0, 500), '```'); } parts.push(''); } if (keyFindings.length > 0) { parts.push('## Key Findings', ''); for (const f of keyFindings) { parts.push(`- ${f}`); } } const synthesized = parts.join('\n'); if (synthesized.length > effectiveAnalysis.length) { effectiveAnalysis = synthesized; logger.info(`[DimensionComplete] Synthesized analysisText for "${dimensionId}" from ${submittedRecipeIds.length} candidates (${analysisText.length}${synthesized.length} chars)`); } } } catch (e) { logger.debug(`[DimensionComplete] Failed to synthesize analysisText: ${e instanceof Error ? e.message : String(e)}`); } } const skillResult = await generateSkill(ctx, dim, effectiveAnalysis, referencedFiles, keyFindings, 'external-agent-bootstrap'); skillCreated = skillResult.success; if (!skillCreated) { logger.warn(`[DimensionComplete] Skill skipped for "${dimensionId}": ${skillResult.error}`); } } // ═══════════════════════════════════════════════════════════ // 3. BootstrapSession 标记完成 + EpisodicMemory 写入 + Quality // ═══════════════════════════════════════════════════════════ const { updated, qualityReport } = session.markDimensionComplete(dimensionId, { analysisText, keyFindings, referencedFiles, recipeIds: submittedRecipeIds, candidateCount: candidateCount || submittedRecipeIds.length, }); // ═══════════════════════════════════════════════════════════ // 4. Checkpoint 保存(持久化,支持断点续传) // ═══════════════════════════════════════════════════════════ try { await saveDimensionCheckpoint(projectRoot, session.id, dimensionId, { candidateCount: candidateCount || submittedRecipeIds.length, analysisChars: analysisText.length, referencedFiles: referencedFiles.length, recipeIds: submittedRecipeIds, skillCreated, }); } catch (e) { logger.warn(`[DimensionComplete] Checkpoint save failed: ${e instanceof Error ? e.message : String(e)}`); } // ═══════════════════════════════════════════════════════════ // 5. SemanticMemory 固化 (提炼为项目级永久记忆) // ═══════════════════════════════════════════════════════════ try { const knowledgeGraphService = ctx.container.get('knowledgeGraphService'); if (knowledgeGraphService && keyFindings.length > 0) { // 将每个 keyFinding 创建为知识图谱中的实体 for (const finding of keyFindings) { await knowledgeGraphService.addEdge(dimensionId, 'dimension', finding.substring(0, 80), 'finding', 'discovered_in', { source: 'external-agent-bootstrap', sessionId: session.id }); } } } catch (e) { logger.debug(`[DimensionComplete] SemanticMemory fixation skipped: ${e instanceof Error ? e.message : String(e)}`); } // ═══════════════════════════════════════════════════════════ // 6. 进度推送 (BootstrapEventEmitter 统一封装) // ═══════════════════════════════════════════════════════════ const progress = session.getProgress(); const isComplete = session.isComplete; const emitter = new BootstrapEventEmitter(ctx.container); emitter.emitDimensionComplete(dimensionId, { type: dim.skillWorthy ? 'skill' : 'candidate', extracted: candidateCount || submittedRecipeIds.length, skillCreated, recipesBound, progress: `${progress.completed}/${progress.total}`, isBootstrapComplete: isComplete, source: 'external-agent', }); if (isComplete) { emitter.emitAllComplete(session.id, progress.total, 'external-agent'); } // ═══════════════════════════════════════════════════════════ // 6.5 Bootstrap 完成后,自动触发 Delivery / Panorama / Wiki / SemanticMemory (R4/R4.5/R5/R6) // ═══════════════════════════════════════════════════════════ let deliveryVerification = null; if (isComplete) { // R4: 自动触发 Cursor Delivery try { const { getServiceContainer } = await import('#inject/ServiceContainer.js'); const container = getServiceContainer(); if (container.services.cursorDeliveryPipeline) { const pipeline = container.get('cursorDeliveryPipeline'); const deliveryResult = await pipeline.deliver(); logger.info(`[DimensionComplete] Auto Cursor Delivery complete — ` + `A: ${deliveryResult.channelA?.rulesCount || 0} rules, ` + `B: ${deliveryResult.channelB?.topicCount || 0} topics, ` + `C: ${deliveryResult.channelC?.synced || 0} skills, ` + `F: ${deliveryResult.channelF?.filesWritten || 0} agent files`); } } catch (e) { logger.warn(`[DimensionComplete] Auto CursorDelivery failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); } // R4+: DeliveryVerifier — 交付完整性检查 try { const { DeliveryVerifier } = await import('#service/bootstrap/DeliveryVerifier.js'); const { resolveProjectRoot } = await import('#shared/resolveProjectRoot.js'); const projectRoot = resolveProjectRoot(ctx.container); const verifier = new DeliveryVerifier(projectRoot); const verification = verifier.verify(); if (!verification.allPassed) { logger.warn('[DimensionComplete] Delivery verification incomplete', { failures: verification.failures, }); } else { logger.info('[DimensionComplete] Delivery verification passed — all channels OK'); } // 附加到响应中的 completionExtras deliveryVerification = verification; } catch (e) { logger.warn(`[DimensionComplete] DeliveryVerifier failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); } // R4.5: Panorama 数据刷新(冷启动完成后知识库已填充,需重新计算全景) try { const { getServiceContainer: getPanoramaContainer } = await import('#inject/ServiceContainer.js'); const panoramaContainer = getPanoramaContainer(); const panoramaService = panoramaContainer.services.panoramaService ? panoramaContainer.get('panoramaService') : null; if (panoramaService && typeof panoramaService.rescan === 'function') { await panoramaService.rescan(); const overview = await panoramaService.getOverview(); logger.info(`[DimensionComplete] Panorama refreshed — ${overview.moduleCount} modules, ${overview.gapCount} gaps`); } } catch (e) { logger.warn(`[DimensionComplete] Panorama refresh failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); } // R5: 自动触发 Wiki 生成 (fire-and-forget) setImmediate(async () => { try { const { getServiceContainer: getWikiContainer } = await import('#inject/ServiceContainer.js'); const wikiContainer = getWikiContainer(); const { WikiGenerator } = await import('#service/wiki/WikiGenerator.js'); const moduleService = wikiContainer.get?.('moduleService'); const knowledgeService = wikiContainer.get?.('knowledgeService'); if (moduleService && knowledgeService) { const wikiGen = new WikiGenerator({ projectRoot, moduleService, knowledgeService, options: { mode: 'bootstrap' }, }); const wikiResult = await wikiGen.generate(); logger.info(`[DimensionComplete] Auto Wiki generation: ${wikiResult?.totalPages || 0} pages`); } } catch (e) { logger.warn(`[DimensionComplete] Wiki generation failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); } }); // R6: 全量 Semantic Memory 固化 (fire-and-forget) setImmediate(async () => { try { const { EpisodicConsolidator } = await import('#agent/domain/EpisodicConsolidator.js'); const db = ctx.container.get?.('database') ?? ctx.container.get?.('db'); if (db && session.sessionStore) { const { PersistentMemory } = await import('#agent/memory/PersistentMemory.js'); const { MemoryEmbeddingStore } = await import('#agent/memory/MemoryEmbeddingStore.js'); const semanticMemory = new PersistentMemory(db, { logger, embeddingStore: new MemoryEmbeddingStore(projectRoot), }); const consolidator = new EpisodicConsolidator(semanticMemory, { logger }); const result = await consolidator.consolidate(session.sessionStore, { bootstrapSession: session.id, clearPrevious: true, }); logger.info(`[DimensionComplete] Semantic Memory consolidation: +${result?.total?.added || 0} ADD, ~${result?.total?.updated || 0} UPDATE`); } } catch (e) { logger.warn(`[DimensionComplete] SemanticMemory consolidation failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); } }); } // ═══════════════════════════════════════════════════════════ // 7. Cross-Dimension Hints // ═══════════════════════════════════════════════════════════ if (crossDimensionHints) { session.storeHints(dimensionId, crossDimensionHints); } const accumulatedHints = session.getAccumulatedHints(); // ═══════════════════════════════════════════════════════════ // 8. 构建响应 (v2: 含质量评估 + 跨维度证据) // ═══════════════════════════════════════════════════════════ // v2: 获取跨维度累积证据 (帮助外部 Agent 做下游维度时避免重复和借鉴) const accumulatedEvidence = session.submissionTracker.getAccumulatedEvidence(dimensionId); // v2: 质量反馈构建 let qualityFeedback; if (qualityReport) { qualityFeedback = { totalScore: qualityReport.totalScore, pass: qualityReport.pass, scores: qualityReport.scores, suggestions: qualityReport.suggestions.length > 0 ? qualityReport.suggestions : undefined, }; if (qualityReport.pass) { logger.info(`[DimensionComplete] Quality assessment for "${dimensionId}": score=${qualityReport.totalScore}/100 PASS`); } else { logger.warn(`[DimensionComplete] Quality assessment for "${dimensionId}": score=${qualityReport.totalScore}/100 BELOW_THRESHOLD`); } } // Wiki 生成提示(冷启动完成时) const nextActions = isComplete ? BOOTSTRAP_COMPLETE_ACTIONS : undefined; // §9: 子包覆盖校验 — 检查 referencedFiles 是否覆盖了关键本地子包 let subpackageCoverageWarning; try { const snapshotCache = session.getSnapshotCache?.(); const localPkgs = snapshotCache?.localPackageModules; if (localPkgs && localPkgs.length > 0 && referencedFiles.length > 0) { const uncoveredPkgs = []; for (const pkg of localPkgs) { const pkgPrefix = pkg.packageName.replace(/\/$/, ''); const covered = referencedFiles.some((f) => f.includes(pkgPrefix) || f.includes(pkg.name)); if (!covered) { uncoveredPkgs.push(pkg.name); } } if (uncoveredPkgs.length > 0) { subpackageCoverageWarning = `本维度未覆盖以下本地子包: ${uncoveredPkgs.join(', ')}。` + `建议在分析中纳入这些模块的源码,以确保知识库完整性。`; logger.info(`[DimensionComplete] Subpackage coverage gap for "${dimensionId}": ${uncoveredPkgs.join(', ')}`); } } } catch { /* best effort */ } // v2: 为下游维度构建结构化提示 (基于累积证据) let evidenceHints; if (!isComplete && (accumulatedEvidence.completedDimSummaries.length > 0 || accumulatedEvidence.negativeSignals.length > 0)) { evidenceHints = { previousSubmissions: accumulatedEvidence.completedDimSummaries.map((s) => ({ dimId: s.dimId, submissionCount: s.submissionCount, titles: s.titles, referencedFiles: s.referencedFiles, })), // v3: 从 SessionStore 提取前序维度分析摘要 + 关键发现(对标内部 Agent 的 buildContextForDimension) previousDimensionAnalysis: (() => { try { const summaries = []; for (const dimSummary of accumulatedEvidence.completedDimSummaries) { const report = session.sessionStore.getDimensionReport(dimSummary.dimId); if (report) { summaries.push({ dimId: dimSummary.dimId, analysisSummary: (report.analysisText || '').substring(0, 500), keyFindings: (report.findings || []) .slice(0, 5) .map((f) => f.finding || f.content || ''), }); } } return summaries.length > 0 ? summaries : undefined; } catch { return undefined; } })(), sharedFiles: accumulatedEvidence.sharedFiles.length > 0 ? accumulatedEvidence.sharedFiles : undefined, negativeSignals: accumulatedEvidence.negativeSignals.length > 0 ? accumulatedEvidence.negativeSignals.map((s) => s.pattern) : undefined, usedTriggers: accumulatedEvidence.usedTriggers.length > 0 ? accumulatedEvidence.usedTriggers : undefined, _note: '以上为前序维度的分析证据,包含分析摘要和关键发现。请利用其中的文件引用和负空间信号,避免重复分析已覆盖的内容', }; } return envelope({ success: true, data: { dimensionId, updated, // true = 覆盖了已有记录(幂等更新) skillCreated, recipesBound, progress: `${progress.completed}/${progress.total}`, completedDimensions: progress.completedDimIds, remainingDimensions: progress.remainingDimIds, isBootstrapComplete: isComplete, accumulatedHints: Object.keys(accumulatedHints).length > 0 ? accumulatedHints : undefined, // v2: 质量评估反馈 qualityFeedback, // v2: 跨维度证据 (供后续维度利用) evidenceHints, // v3: 子包覆盖校验警告 subpackageCoverageWarning, // v3.1: 交付完整性验证 (仅 bootstrap 完成时) deliveryVerification: isComplete ? deliveryVerification : undefined, nextActions, }, meta: { tool: 'autosnippet_dimension_complete', responseTimeMs: Date.now() - t0, }, }); }