autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
429 lines (427 loc) • 23 kB
JavaScript
/**
* MCP Handlers — Bootstrap 冷启动知识库初始化 (内部 Agent 路径)
*
* ⚠️ 本文件是「内部 Agent」冷启动路径 — 由 AutoSnippet 内置的 Analyst/Producer
* 双 Agent AI pipeline 自动完成知识提取。需要配置 AI Provider (API Key)。
*
* 调用方:
* - CLI: `asd bootstrap --knowledge`
* - AgentRuntime: `bootstrapKnowledgeTool` (infrastructure.js)
* - Dashboard HTTP: POST /api/bootstrap/knowledge
*
* 外部 Agent 路径(Cursor/Copilot 等 IDE Agent)请参见:
* - bootstrap-external.js — 无参数 Mission Briefing 入口
* - dimension-complete.js — 维度完成通知
* 外部 Agent 使用 read_file/grep_search 等原生能力自行分析代码,
* 不经过本文件的 Phase 5 AI pipeline。
*
* 内部 Agent 架构 (v5 + Async Fill):
*
* 同步阶段(快速返回,~1-3s):
* Phase 1 → 文件收集(SPM Target 源文件扫描)
* Phase 1.5 → AST 代码结构分析(Tree-sitter)
* Phase 2 → SPM 依赖关系 → knowledge_edges(模块级图谱)
* Phase 3 → Guard 规则审计
* Phase 4 → 构建响应骨架(filesByTarget + analysisFramework + 任务清单)
*
* 异步阶段(后台逐一填充,通过 Socket.io 推送进度):
* Phase 5 → 微观维度 × 子主题提取代码特征 → 创建 N 条 Candidate(PENDING 状态)
* skillWorthy 维度仅提取内容,不创建 Candidate(避免与 Skill 重复)
* anti-pattern 已移除 — 代码问题由 Guard 独立处理
* Phase 5.5 → 宏观维度(architecture/code-standard/project-profile/agent-guidelines)
* 自动聚合为 Project Skill → 写入 AutoSnippet/skills/(不产生 Candidate)
*
* 进度推送事件(Socket.io + EventBus):
* bootstrap:started — 骨架创建完成,携带任务清单
* bootstrap:task-started — 单个维度开始填充
* bootstrap:task-completed — 单个维度填充完成
* bootstrap:task-failed — 单个维度失败
* bootstrap:all-completed — 全部维度完成(前端弹出通知)
*
* 模块结构:
* bootstrap.js ← 内部 Agent 主入口 (本文件)
* bootstrap-external.js ← 外部 Agent 主入口 (Mission Briefing)
* bootstrap/patterns.js ← 多语言代码模式匹配(内部 Agent 专用)
* bootstrap/dimensions.js ← 7 维度知识提取器(内部 Agent 专用)
* bootstrap/projectSkills.js ← Phase 5.5 Project Skill 生成(内部 Agent 专用)
*/
import path from 'node:path';
import { getInternalAgentRequiredFields } from '#domain/knowledge/FieldSpec.js';
import { CleanupService } from '#service/cleanup/CleanupService.js';
import { resolveProjectRoot } from '#shared/resolveProjectRoot.js';
import { buildProjectSnapshot } from '#types/project-snapshot-builder.js';
import { toSessionCache } from '#types/snapshot-views.js';
import { envelope } from '../envelope.js';
import { fillDimensionsV3 } from './bootstrap/pipeline/orchestrator.js';
import { bootstrapRefine } from './bootstrap/refine.js';
import { buildTaskDefs, dispatchPipelineFill, startTaskManagerSession, } from './bootstrap/shared/async-fill-helpers.js';
import { runAllPhases } from './bootstrap/shared/bootstrap-phases.js';
import { buildInternalNextSteps } from './bootstrap/shared/dimension-text.js';
import { summarizePanorama } from './bootstrap/shared/panorama-utils.js';
import { getOrCreateSessionManager } from './bootstrap/shared/session-helpers.js';
import { buildTargetFileMap } from './bootstrap/shared/target-file-map.js';
import { buildLanguageExtension } from './LanguageExtensions.js';
import { inferTargetRole } from './TargetClassifier.js';
export { bootstrapRefine };
/**
* bootstrapKnowledge — 一键初始化知识库 (Skill-aware)
*
* 覆盖 7 大知识维度: 项目规范、使用习惯、架构模式、代码模式、最佳实践、项目库特征、Agent开发注意事项
* (注意:反模式/代码问题由 Guard 独立处理,不在 Bootstrap 覆盖范围)
* 为每个维度自动创建 Candidate(PENDING),由内置 Analyst/Producer pipeline 分析代码。
*
* ⚠️ 本函数是内部 Agent 路径。外部 Agent 使用 bootstrap-external.js 的 Mission Briefing + dimension_complete 流程。
*
* @param ctx { container, logger }
* @param [args.maxFiles=500] 最大扫描文件数
* @param [args.skipGuard=false] 是否跳过 Guard 审计
* @param [args.contentMaxLines=120] 每文件读取最大行数
* @param [args.incremental=true] 是否启用增量 Bootstrap (自动检测变更, 仅重跑受影响维度)
*/
export async function bootstrapKnowledge(ctx, args) {
const t0 = Date.now();
const projectRoot = resolveProjectRoot(ctx.container);
// v5.0: 增量 Bootstrap 开关 (默认启用, 自动检测是否可增量)
const enableIncremental = args.incremental !== false;
const maxFiles = args.maxFiles || 500;
const skipGuard = args.skipGuard || false;
const contentMaxLines = args.contentMaxLines || 120;
const skipAsyncFill = args.skipAsyncFill || false;
// ═══════════════════════════════════════════════════════════
// Step 0: 全量清理 (与 bootstrap-external 对齐)
// 冷启动需要干净的初始状态:清除 DB + 文件系统缓存
// ═══════════════════════════════════════════════════════════
const db = ctx.container.get('database');
const cleanupService = new CleanupService({
projectRoot,
db,
logger: ctx.logger,
});
const cleanupResult = await cleanupService.fullReset();
ctx.logger.info('[Bootstrap-Internal] fullReset complete', {
tables: cleanupResult.clearedTables.length,
files: cleanupResult.deletedFiles,
errors: cleanupResult.errors.length,
});
// ═══════════════════════════════════════════════════════════
// Phase 1-4: 共享管线(文件收集→AST→依赖→Guard→维度解析)
// ═══════════════════════════════════════════════════════════
const phaseResults = await runAllPhases(projectRoot, ctx, {
maxFiles,
skipGuard,
clearOldData: true,
generateReport: true,
generateAstContext: true,
incremental: enableIncremental,
sourceTag: 'bootstrap',
});
if (phaseResults.isEmpty) {
return envelope({
success: true,
data: { report: phaseResults.report, message: 'No source files found, nothing to bootstrap' },
meta: { tool: 'autosnippet_bootstrap', responseTimeMs: Date.now() - t0 },
});
}
// ═══════════════════════════════════════════════════════════
// 构建 ProjectSnapshot — 统一数据来源
// ═══════════════════════════════════════════════════════════
const snapshot = buildProjectSnapshot({
projectRoot,
sourceTag: 'bootstrap',
...phaseResults,
report: phaseResults.report,
});
// 从 snapshot 派生局部别名(兼容既有 responseData 构建逻辑)
const { allFiles, allTargets, discoverer, ast: astProjectSummary, astContext, dependencyGraph: depGraphData, depEdgesWritten, guardAudit, activeDimensions, enhancementPackInfo, enhancementPatterns, enhancementGuardRules, language, targetsSummary, incrementalPlan, codeEntityGraph: codeEntityResult, callGraph: callGraphResult, localPackageModules, warnings: phaseWarnings, phaseReport, } = snapshot;
const langStats = language.stats;
const primaryLang = language.primaryLang;
const langProfile = language;
// 构建兼容的 report 对象(保持原有 API 格式)
const report = {
phases: {
fileCollection: {
discoverer: discoverer.id,
discovererName: discoverer.displayName,
targets: allTargets.length,
files: allFiles.length,
truncated: allFiles.length >= maxFiles,
},
incrementalEvaluation: incrementalPlan
? {
mode: incrementalPlan.mode,
canIncremental: incrementalPlan.canIncremental,
affectedDimensions: incrementalPlan.affectedDimensions,
skippedDimensions: incrementalPlan.skippedDimensions,
reason: incrementalPlan.reason,
diff: incrementalPlan.diff
? {
added: incrementalPlan.diff.added.length,
modified: incrementalPlan.diff.modified.length,
deleted: incrementalPlan.diff.deleted.length,
unchanged: incrementalPlan.diff.unchanged.length,
changeRatio: incrementalPlan.diff.changeRatio,
}
: null,
}
: undefined,
astAnalysis: {
classes: astProjectSummary?.classes?.length || 0,
protocols: astProjectSummary?.protocols?.length || 0,
categories: astProjectSummary?.categories?.length || 0,
patterns: Object.keys(astProjectSummary?.patternStats || {}),
},
codeEntityGraph: phaseReport?.phases?.entityGraph || {
entityCount: 0,
edgeCount: 0,
ms: 0,
},
callGraph: phaseReport?.phases?.callGraph
? {
entities: phaseReport.phases.callGraph.result
?.entitiesUpserted || 0,
edges: phaseReport.phases.callGraph.result
?.edgesCreated || 0,
ms: phaseReport.phases.callGraph.ms || 0,
}
: { entities: 0, edges: 0, ms: 0 },
dependencyGraph: { edgesWritten: depEdgesWritten || 0 },
enhancementPacks: {
matched: enhancementPackInfo,
extraDimensions: enhancementPackInfo.length,
guardRules: enhancementGuardRules?.length || 0,
patterns: enhancementPatterns?.length || 0,
},
guardAudit: {
totalViolations: guardAudit?.summary?.totalViolations || 0,
filesWithViolations: (guardAudit?.files || []).filter((f) => f.violations.length > 0).length,
skipped: skipGuard,
enhancementRulesInjected: enhancementGuardRules?.length || 0,
},
},
totals: {
files: allFiles.length,
graphEdges: depEdgesWritten || 0,
guardViolations: guardAudit?.summary?.totalViolations || 0,
},
};
// ═══════════════════════════════════════════════════════════
// Phase 4.5: 构建响应 — filesByTarget + analysisFramework
// ═══════════════════════════════════════════════════════════
const targetFileMap = buildTargetFileMap(allFiles, contentMaxLines, true);
let dimensions = activeDimensions;
// 如果调用方指定了维度子集,只保留匹配的维度
if (args.dimensions?.length) {
const requestedIds = new Set(args.dimensions);
dimensions = dimensions.filter((d) => requestedIds.has(d.id));
ctx.logger.info(`[Bootstrap] Dimension filter: ${dimensions.map((d) => d.id).join(', ')}`);
}
const responseData = {
// Step 0 清理信息(与 bootstrap-external 对齐)
cleanup: {
deletedRecipes: cleanupResult.deletedFiles,
clearedTables: cleanupResult.clearedTables.length,
dbCleared: true,
errors: cleanupResult.errors,
trash: cleanupResult.trash ?? null,
purgedTrash: cleanupResult.purgedTrash ?? null,
},
report,
targets: targetsSummary ||
allTargets.map((t) => {
const name = typeof t === 'string' ? t : t.name;
return {
name,
type: t.type || 'target',
packageName: t.packageName || undefined,
inferredRole: inferTargetRole(name),
fileCount: (targetFileMap[name] || []).length,
};
}),
// 响应中只返回每个 target 的高优先级文件摘要(不含 content),
// 避免 500+ 文件清单导致响应过大。完整文件列表保留在服务端供 Phase 5 使用。
filesByTarget: Object.fromEntries(Object.entries(targetFileMap).map(([target, files]) => {
const sorted = [...files].sort((a, b) => (Number(b.priority) || 0) - (Number(a.priority) || 0));
const top = sorted.slice(0, 10);
return [
target,
{
totalFiles: files.length,
topFiles: top.map(({ content, ...meta }) => meta),
...(files.length > 10 ? { truncated: true } : {}),
},
];
})),
dependencyGraph: depGraphData
? {
nodes: (depGraphData.nodes || []).map((n) => ({
id: typeof n === 'string' ? n : n.id,
label: typeof n === 'string' ? n : n.label,
})),
edges: depGraphData.edges || [],
}
: null,
languageStats: langStats,
primaryLanguage: primaryLang,
secondaryLanguages: langProfile.secondary,
isMultiLang: langProfile.isMultiLang,
languageExtension: buildLanguageExtension(primaryLang),
guardSummary: guardAudit
? {
totalViolations: guardAudit.summary?.totalViolations || 0,
errors: guardAudit.summary?.errors || 0,
warnings: guardAudit.summary?.warnings || 0,
}
: null,
guardViolationFiles: guardAudit
? (guardAudit.files || [])
.filter((f) => f.violations.length > 0)
.map((f) => ({
filePath: f.filePath,
violations: f.violations.map((v) => ({
ruleId: v.ruleId,
severity: v.severity,
message: v.message,
line: v.line,
})),
}))
: [],
// 9 维度分析框架(4 Skill-only + 2 dualOutput + 3 Candidate-only)
// 注意:anti-pattern 已移除,代码问题由 Guard 独立处理
analysisFramework: {
dimensions,
skillWorthyDimensions: dimensions.filter((d) => d.skillWorthy).map((d) => d.id),
candidateOnlyDimensions: dimensions.filter((d) => !d.skillWorthy).map((d) => d.id),
candidateRequiredFields: getInternalAgentRequiredFields(),
submissionTool: 'autosnippet_submit_knowledge_batch',
expectedOutput: `候选知识(微观代码维度:code-pattern/best-practice/event-and-data-flow + 语言条件扫描)+ Project Skills(宏观叙事维度:code-standard/architecture/project-profile/agent-guidelines + 语言条件扫描)— 共 ${dimensions.length} 个维度`,
},
// AST 代码结构分析上下文(供 Agent 使用)
astContext: astContext || null,
astSummary: astProjectSummary
? {
classes: astProjectSummary.classes?.length || 0,
protocols: astProjectSummary.protocols?.length || 0,
categories: astProjectSummary.categories?.length || 0,
patterns: Object.keys(astProjectSummary.patternStats || {}),
metrics: astProjectSummary.projectMetrics
? {
totalMethods: astProjectSummary.projectMetrics.totalMethods,
avgMethodsPerClass: astProjectSummary.projectMetrics.avgMethodsPerClass,
maxNestingDepth: astProjectSummary.projectMetrics.maxNestingDepth,
complexMethods: astProjectSummary.projectMetrics.complexMethods?.length || 0,
longMethods: astProjectSummary.projectMetrics.longMethods?.length || 0,
}
: null,
}
: null,
// Enhancement Pack 检测到的额外模式
enhancementPacks: enhancementPackInfo.length > 0
? {
matched: enhancementPackInfo,
patterns: enhancementPatterns,
guardRules: enhancementGuardRules.length,
}
: null,
// 代码实体图谱摘要(与 bootstrap-external 对齐)
codeEntityGraph: codeEntityResult
? {
totalEntities: codeEntityResult.entityCount || 0,
totalEdges: codeEntityResult.edgeCount || 0,
}
: null,
// 调用图谱摘要(与 bootstrap-external 对齐)
callGraph: callGraphResult
? {
entitiesUpserted: callGraphResult.entitiesUpserted || 0,
edgesCreated: callGraphResult.edgesCreated || 0,
}
: null,
// 全景分析摘要(与 bootstrap-external 对齐)
panorama: snapshot.panorama ? summarizePanorama(snapshot.panorama) : null,
// 本地子包模块(与 bootstrap-external mustCoverModules 对齐)
localPackageModules: localPackageModules.length > 0 ? localPackageModules : null,
// Phase 1-4 警告(与 bootstrap-external 对齐)
warnings: phaseWarnings.length > 0 ? phaseWarnings : undefined,
// 引导 Agent 下一步操作(共享文本层)
nextSteps: buildInternalNextSteps(dimensions),
};
// ═══════════════════════════════════════════════════════════
// Phase 4.6: BootstrapSessionManager — 缓存 Phase 结果供 wiki_plan 复用
// (与 bootstrap-external 对齐)
// ═══════════════════════════════════════════════════════════
try {
const sessionManager = getOrCreateSessionManager(ctx.container);
const bsSession = sessionManager.createSession({
projectRoot,
dimensions: dimensions.map((d) => ({
...d,
skillMeta: d.skillMeta ?? undefined,
})),
projectContext: {
projectName: path.basename(projectRoot),
primaryLang,
fileCount: allFiles.length,
modules: depGraphData?.nodes?.length || 0,
},
});
bsSession.setSnapshotCache(toSessionCache(snapshot));
responseData.sessionId = bsSession.id;
}
catch (e) {
ctx.logger.warn(`[Bootstrap-Internal] BootstrapSessionManager setup failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`);
}
// ═══════════════════════════════════════════════════════════
// Phase 5: 创建异步任务 — 骨架先返回,内容后填充
//
// 策略变更(v5):
// 旧:同步遍历所有维度 → 提取 + 创建 Candidate → 一次性返回
// 新:快速创建任务清单 → 立即返回骨架 → 异步逐维度填充内容
// 前端通过 Socket.io 接收进度更新,卡片 loading → 完成
// ═══════════════════════════════════════════════════════════
// 构建任务定义列表
const taskDefs = buildTaskDefs(dimensions);
// 启动 BootstrapTaskManager 会话(通过正式 DI 获取单例)
const bootstrapSession = startTaskManagerSession(ctx.container, taskDefs, ctx.logger, 'Bootstrap');
// 立即构建骨架响应
responseData.bootstrapSession = bootstrapSession ? bootstrapSession.toJSON() : null;
responseData.bootstrapCandidates = { created: 0, failed: 0, errors: [], status: 'filling' };
responseData.autoSkills = { created: 0, failed: 0, skills: [], errors: [], status: 'filling' };
responseData.message = `Bootstrap 骨架已创建: ${allFiles.length} files, ${allTargets.length} targets, ${taskDefs.length} 个维度任务已排队,正在后台逐一填充...`;
// ── 异步后台填充(fire-and-forget)──
// skipAsyncFill: CLI 非 --wait 模式跳过异步填充,避免进程退出后 DB 断连
if (!skipAsyncFill) {
dispatchPipelineFill({
snapshot,
ctx: ctx,
bootstrapSession,
targetFileMap,
projectRoot,
}, dimensions, fillDimensionsV3, 'Bootstrap');
}
else {
ctx.logger.info(`[Bootstrap] Async fill skipped (skipAsyncFill=true)`);
}
// ── SkillHooks: onBootstrapStarted (fire-and-forget) ──
try {
const skillHooks = ctx.container.get('skillHooks');
skillHooks
.run('onBootstrapComplete', {
filesScanned: allFiles.length,
targetsFound: allTargets.length,
candidatesCreated: 0, // 异步填充中,初始为 0
candidatesFailed: 0,
autoSkillsCreated: 0,
autoSkills: [],
}, { projectRoot: ctx.container.get('database')?.filename || '' })
.catch(() => { }); // fire-and-forget
}
catch {
/* skillHooks not available */
}
return envelope({
success: true,
data: responseData,
meta: { tool: 'autosnippet_bootstrap', responseTimeMs: Date.now() - t0 },
});
}
// bootstrapRefine → 已提取到 bootstrap/refine.js(通过顶部 re-export)