UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

586 lines (585 loc) 22.7 kB
/** * MCP Handlers — 项目结构 & 知识图谱 * getTargets, getTargetFiles, getTargetMetadata, graphQuery, graphImpact, graphPath, graphStats */ import fs from 'node:fs'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import * as Paths from '#infra/config/Paths.js'; import { LanguageService } from '#shared/LanguageService.js'; import { resolveProjectRoot } from '#shared/resolveProjectRoot.js'; import { envelope } from '../envelope.js'; // ─── Discoverer 缓存 ───────────────────────────────────── // 同一 projectRoot 在模块生命期内只初始化一次 let _discovererCache = null; // { projectRoot, discoverer, targets } async function _getLoadedDiscoverer(ctx) { const projectRoot = resolveProjectRoot(ctx?.container); if (_discovererCache && _discovererCache.projectRoot === projectRoot) { return _discovererCache; } // 优先使用 DiscovererRegistry(多语言统一接口) const { getDiscovererRegistry } = await import('#core/discovery/index.js'); const registry = getDiscovererRegistry(); const discoverer = await registry.detect(projectRoot); await discoverer.load(projectRoot); const targets = (await discoverer.listTargets()) || []; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- structural duck-typing across module boundary _discovererCache = { projectRoot, discoverer: discoverer, targets: targets, }; return _discovererCache; } function _findTarget(targets, targetName) { const t = targets.find((t) => t.name === targetName); if (!t) { throw new Error(`Target not found: ${targetName}`); } return t; } /** 推断语言 — 委托给 LanguageService */ function _inferLang(filename) { return LanguageService.inferLang(filename); } /** 推断 Target 职责 */ function _inferTargetRole(targetName) { const n = targetName.toLowerCase(); if (/core|kit|shared|common|foundation|base/i.test(n)) { return 'core'; } if (/service|manager|provider|repository|store/i.test(n)) { return 'service'; } if (/ui|view|screen|component|widget/i.test(n)) { return 'ui'; } if (/network|api|http|grpc|socket/i.test(n)) { return 'networking'; } if (/storage|database|cache|persist|realm|coredata/i.test(n)) { return 'storage'; } if (/test|spec|mock|stub|fake/i.test(n)) { return 'test'; } if (/app|main|launch|entry/i.test(n)) { return 'app'; } if (/router|coordinator|navigation/i.test(n)) { return 'routing'; } if (/util|helper|extension|tool/i.test(n)) { return 'utility'; } if (/model|entity|dto|schema/i.test(n)) { return 'model'; } if (/auth|login|session|token/i.test(n)) { return 'auth'; } if (/config|setting|environment|constant/i.test(n)) { return 'config'; } return 'feature'; } // ═══════════════════════════════════════════════════════════ // Handler: getTargets // ═══════════════════════════════════════════════════════════ export async function getTargets(ctx, args = {}) { const { discoverer, targets } = await _getLoadedDiscoverer(ctx); const includeSummary = args.includeSummary !== false; // 默认 true if (!includeSummary) { return envelope({ success: true, data: { targets }, meta: { tool: 'autosnippet_structure' } }); } // 带摘要:每个 target 附加文件数、语言统计、推断职责 const enriched = []; const globalLangStats = {}; let totalFiles = 0; for (const t of targets) { let fileCount = 0; const langStats = {}; try { const fileList = await discoverer.getTargetFiles(t); fileCount = fileList.length; for (const f of fileList) { const lang = _inferLang(f.name); langStats[lang] = (langStats[lang] || 0) + 1; globalLangStats[lang] = (globalLangStats[lang] || 0) + 1; } } catch { /* skip */ } totalFiles += fileCount; enriched.push({ name: t.name, packageName: t.packageName || null, type: t.type || 'target', inferredRole: _inferTargetRole(t.name), fileCount, languageStats: langStats, }); } return envelope({ success: true, data: { targets: enriched, summary: { targetCount: targets.length, totalFiles, languageStats: globalLangStats }, }, meta: { tool: 'autosnippet_structure' }, }); } // ═══════════════════════════════════════════════════════════ // Handler: getTargetFiles // ═══════════════════════════════════════════════════════════ export async function getTargetFiles(ctx, args) { if (!args.targetName) { throw new Error('targetName is required'); } const { discoverer, targets } = await _getLoadedDiscoverer(ctx); const target = _findTarget(targets, args.targetName); // 使用 Discoverer.getTargetFiles — 统一接口定位源文件 const rawFiles = await discoverer.getTargetFiles(target); const includeContent = args.includeContent || false; const contentMaxLines = args.contentMaxLines || 100; const maxFiles = args.maxFiles || 500; const files = []; for (const f of rawFiles) { if (files.length >= maxFiles) { break; } const entry = { name: f.name, path: f.path, relativePath: f.relativePath, language: _inferLang(f.name), size: f.size || 0, }; if (includeContent) { try { const raw = await readFile(f.path, 'utf8'); const lines = raw.split('\n'); entry.content = lines.slice(0, contentMaxLines).join('\n'); entry.totalLines = lines.length; entry.truncated = lines.length > contentMaxLines; } catch { entry.content = null; entry.totalLines = 0; entry.truncated = false; } } files.push(entry); } // 文件语言统计 const langStats = {}; for (const f of files) { langStats[f.language] = (langStats[f.language] || 0) + 1; } return envelope({ success: true, data: { targetName: args.targetName, files, fileCount: files.length, totalAvailable: rawFiles.length, languageStats: langStats, }, meta: { tool: 'autosnippet_structure' }, }); } // ═══════════════════════════════════════════════════════════ // Handler: getTargetMetadata // ═══════════════════════════════════════════════════════════ export async function getTargetMetadata(ctx, args) { if (!args.targetName) { throw new Error('targetName is required'); } const { targets } = await _getLoadedDiscoverer(ctx); const target = _findTarget(targets, args.targetName); const projectRoot = _discovererCache.projectRoot; // ── 基础元数据 ── const meta = { name: target.name, path: target.path || null, packageName: target.packageName || null, packagePath: target.packagePath || null, type: target.type || 'target', language: target.language || null, framework: target.framework || null, inferredRole: _inferTargetRole(target.name), targetDir: target.targetDir || null, sourcesPath: target.info?.path || null, sources: target.info?.sources || null, dependencies: target.info?.dependencies || target.metadata?.dependencies || [], }; // ── SPM 图谱 (spmmap.json) ── try { const knowledgeDir = Paths.getProjectKnowledgePath(projectRoot); const mapPath = path.join(knowledgeDir, 'AutoSnippet.spmmap.json'); if (fs.existsSync(mapPath)) { const raw = await readFile(mapPath, 'utf8'); const graph = JSON.parse(raw)?.graph || null; if (target.packageName && graph?.packages?.[target.packageName]) { const pkg = graph.packages[target.packageName]; meta.packageDir = pkg.packageDir; meta.packageSwift = pkg.packageSwift; meta.packageTargets = pkg.targets || []; } } } catch { /* ignore */ } // ── 知识图谱关系 (knowledge_edges) ── try { const graphService = ctx.container?.get('knowledgeGraphService'); if (graphService) { const edges = await graphService.getEdges(target.name, 'module', 'both'); meta.graphEdges = { outgoing: (edges.outgoing || []).map((e) => ({ toId: e.toId, toType: e.toType, relation: e.relation, })), incoming: (edges.incoming || []).map((e) => ({ fromId: e.fromId, fromType: e.fromType, relation: e.relation, })), }; } } catch { /* knowledge_edges may not exist */ } return envelope({ success: true, data: meta, meta: { tool: 'autosnippet_structure' } }); } export async function graphQuery(ctx, args) { const graphService = ctx.container.get('knowledgeGraphService'); if (!graphService) { return envelope({ success: false, message: 'KnowledgeGraphService not available — knowledge_edges 表可能未初始化', meta: { tool: 'autosnippet_graph' }, }); } const nodeType = args.nodeType || 'recipe'; const direction = args.direction || 'both'; let data; try { if (args.relation) { data = await graphService.getRelated(args.nodeId, nodeType, args.relation); } else { data = await graphService.getEdges(args.nodeId, nodeType, direction); } } catch (err) { // knowledge_edges 表不存在时 graceful 降级到 relations 字段 if (err instanceof Error && err.message?.includes('no such table')) { data = await _fallbackRelationsFromRecipe(ctx, args.nodeId, args.relation, direction); return envelope({ success: true, data, meta: { tool: 'autosnippet_graph', source: 'relations-fallback' }, }); } throw err; } return envelope({ success: true, data, meta: { tool: 'autosnippet_graph' } }); } export async function graphImpact(ctx, args) { const graphService = ctx.container.get('knowledgeGraphService'); if (!graphService) { return envelope({ success: false, message: 'KnowledgeGraphService not available — knowledge_edges 表可能未初始化', meta: { tool: 'autosnippet_graph' }, }); } const nodeType = args.nodeType || 'recipe'; let impacted; try { impacted = await graphService.getImpactAnalysis(args.nodeId, nodeType, args.maxDepth ?? 3); } catch (err) { // knowledge_edges 表不存在时 graceful 降级 if (err instanceof Error && err.message?.includes('no such table')) { impacted = await _fallbackImpactFromRecipe(ctx, args.nodeId); return envelope({ success: true, data: { nodeId: args.nodeId, impactedCount: impacted.length, impacted, degraded: true, degradedReason: 'knowledge_edges 表不存在,仅从 relations 字段反查', }, meta: { tool: 'autosnippet_graph', source: 'relations-fallback' }, }); } throw err; } return envelope({ success: true, data: { nodeId: args.nodeId, impactedCount: impacted.length, impacted }, meta: { tool: 'autosnippet_graph' }, }); } /** 降级:从 knowledge_entries.relations 提取关系(不依赖 knowledge_edges 表) */ async function _fallbackRelationsFromRecipe(ctx, nodeId, relation, direction) { try { const knowledgeService = ctx.container.get('knowledgeService'); const entry = await knowledgeService.get(nodeId); if (!entry) { return { outgoing: [], incoming: [] }; } const relJson = typeof entry.relations?.toJSON === 'function' ? entry.relations.toJSON() : entry.relations || {}; const outgoing = []; if (direction === 'both' || direction === 'out') { for (const [relType, targets] of Object.entries(relJson)) { if (relation && relType !== relation) { continue; } for (const t of Array.isArray(targets) ? targets : []) { outgoing.push({ fromId: nodeId, fromType: 'knowledge', toId: t.target || t.id || t, toType: 'knowledge', relation: relType, }); } } } // 反向查找:其他条目中 relations 包含当前 nodeId const incoming = []; if (direction === 'both' || direction === 'in') { const knowledgeRepo = ctx.container.get('knowledgeRepository'); const reverseRows = await knowledgeRepo.findByRelationLike(nodeId, nodeId); for (const row of reverseRows) { try { const rels = JSON.parse(row.relations || '{}'); for (const [relType, targets] of Object.entries(rels)) { if (relation && relType !== relation) { continue; } for (const t of Array.isArray(targets) ? targets : []) { const targetId = t.target || t.id || t; if (targetId === nodeId) { incoming.push({ fromId: row.id, fromType: 'knowledge', toId: nodeId, toType: 'knowledge', relation: relType, }); } } } } catch { /* ignore parse error */ } } } return { outgoing, incoming }; } catch { return { outgoing: [], incoming: [] }; } } /** 降级:从 knowledge_entries.relations 反查受影响的条目 */ async function _fallbackImpactFromRecipe(ctx, nodeId) { try { const knowledgeRepo = ctx.container.get('knowledgeRepository'); const rows = await knowledgeRepo.findByRelationLike(nodeId, nodeId); const impacted = []; for (const row of rows) { try { const rels = JSON.parse(row.relations || '{}'); for (const [relType, targets] of Object.entries(rels)) { for (const t of Array.isArray(targets) ? targets : []) { if ((t.target || t.id || t) === nodeId) { impacted.push({ id: row.id, title: row.title, type: 'knowledge', relation: relType, depth: 1, }); } } } } catch { /* ignore */ } } return impacted; } catch { return []; } } // ─── graph_path — 路径查找 ───────────────────────────────── export async function graphPath(ctx, args) { if (!args.fromId || !args.toId) { throw new Error('fromId and toId are required'); } const graphService = ctx.container.get('knowledgeGraphService'); if (!graphService) { return envelope({ success: false, message: 'KnowledgeGraphService not available', meta: { tool: 'autosnippet_graph' }, }); } const fromType = args.fromType || 'recipe'; const toType = args.toType || 'recipe'; const maxDepth = Math.min(Math.max(args.maxDepth ?? 5, 1), 10); let result; try { result = await graphService.findPath(args.fromId, fromType, args.toId, toType, maxDepth); } catch (err) { if (err instanceof Error && err.message?.includes('no such table')) { // 降级:用 relations 字段做单跳查找 result = await _fallbackPathFromRecipe(ctx, args.fromId, args.toId); return envelope({ success: true, data: result, meta: { tool: 'autosnippet_graph', source: 'relations-fallback' }, }); } throw err; } return envelope({ success: true, data: result, meta: { tool: 'autosnippet_graph' } }); } /** 降级路径查找:只能发现 1-hop 直接关系 */ async function _fallbackPathFromRecipe(ctx, fromId, toId) { try { const knowledgeService = ctx.container.get('knowledgeService'); const entry = await knowledgeService.get(fromId); if (!entry) { return { found: false, path: [], depth: -1 }; } const relJson = typeof entry.relations?.toJSON === 'function' ? entry.relations.toJSON() : entry.relations || {}; for (const [relType, targets] of Object.entries(relJson)) { for (const t of Array.isArray(targets) ? targets : []) { const targetId = t.target || t.id || t; if (targetId === toId) { return { found: true, path: [ { from: { id: fromId, type: 'knowledge' }, to: { id: toId, type: 'knowledge' }, relation: relType, }, ], depth: 1, }; } } } return { found: false, path: [], depth: -1 }; } catch { return { found: false, path: [], depth: -1 }; } } // ─── call_context — 调用链上下文 (Phase 5) ────────────────── /** * autosnippet_call_context handler * 查询方法的调用者、被调用者、影响半径 */ export async function callContext(ctx, args) { if (!args.methodName) { throw new Error('Missing required parameter: methodName'); } const ceg = ctx.container.get('codeEntityGraph'); if (!ceg) { return envelope({ success: false, message: 'CodeEntityGraph not available — 请先运行 bootstrap', meta: { tool: 'autosnippet_call_context' }, }); } const direction = args.direction || 'both'; const maxDepth = Math.min(Math.max(args.maxDepth ?? 2, 1), 5); const result = {}; try { if (direction === 'callers' || direction === 'both') { result.callers = ceg.getCallers(args.methodName, maxDepth); } if (direction === 'callees' || direction === 'both') { result.callees = ceg.getCallees(args.methodName, maxDepth); } if (direction === 'impact') { result.impact = ceg.getCallImpactRadius(args.methodName); } } catch (err) { if (err instanceof Error && err.message?.includes('no such table')) { return envelope({ success: true, data: { methodName: args.methodName, callers: [], callees: [], note: 'knowledge_edges 表不存在,请运行 bootstrap 后再查询', }, meta: { tool: 'autosnippet_call_context' }, }); } throw err; } return envelope({ success: true, data: { methodName: args.methodName, direction, maxDepth, ...result, }, meta: { tool: 'autosnippet_call_context' }, }); } // ─── graph_stats — 图谱统计 ──────────────────────────────── export async function graphStats(ctx) { const graphService = ctx.container.get('knowledgeGraphService'); if (!graphService) { return envelope({ success: false, message: 'KnowledgeGraphService not available', meta: { tool: 'autosnippet_graph' }, }); } let stats; try { stats = await graphService.getStats(); } catch (err) { if (err instanceof Error && err.message?.includes('no such table')) { return envelope({ success: true, data: { totalEdges: 0, byRelation: {}, nodeTypes: [], note: 'knowledge_edges 表不存在,请运行数据库迁移', }, meta: { tool: 'autosnippet_graph' }, }); } throw err; } return envelope({ success: true, data: stats, meta: { tool: 'autosnippet_graph' } }); }