UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

286 lines (285 loc) 11.6 kB
/** * MCP Handlers — 知识浏览类 (V3: 使用 knowledgeService) * listByKind, listRecipes, getRecipe, recipeInsights, confirmUsage * * 投影原则:只交付 Agent 可操作的信号,不交付内部运营/审计数据。 * - _projectItem → list 列表摘要(精简 ~10 字段) * - _projectForAgent → get 详情(去除噪音 ~25 字段) */ import { envelope } from '../envelope.js'; // ─── 通用投影辅助 ──────────────────────────────────────────── /** * 构建 actionHint — "whenClause → doClause" 的一句话可操作摘要。 * Agent 在列表/搜索中即可判断是否需要深入获取该条目。 */ function _buildActionHint(json) { const doText = json.doClause || ''; const whenText = json.whenClause || ''; if (!doText && !whenText) { return undefined; } return `${whenText ? `${whenText} → ` : ''}${doText}`.replace(/ → $/, ''); } /** 只保留非空关系桶,压缩 Relations 输出体积。 */ function _compactRelations(relations) { if (!relations) { return undefined; } const compact = {}; for (const [type, list] of Object.entries(relations)) { if (Array.isArray(list) && list.length > 0) { compact[type] = list; } } return Object.keys(compact).length > 0 ? compact : undefined; } // ─── 列表投影 ──────────────────────────────────────────────── /** * 将 KnowledgeEntry 投影为列表摘要(精简版) * 移除: quality, stats, scope, tags, knowledgeType, 重复的 status/statistics * 新增: actionHint */ function _projectItem(r) { const json = typeof r.toJSON === 'function' ? r.toJSON() : r; return { id: json.id, title: json.title, trigger: json.trigger || '', kind: json.kind, language: json.language, category: json.category, lifecycle: json.lifecycle, complexity: json.complexity, description: (json.description || '').slice(0, 120), actionHint: _buildActionHint(json), }; } // ─── 详情投影 ──────────────────────────────────────────────── /** * 将 KnowledgeEntry 投影为 Agent 消费的详情格式。 * * Tier 1(黄金字段): trigger, doClause, dontClause, whenClause, coreCode, title, content.pattern, kind * Tier 2(上下文) : language, category, description, tags, rationale, steps, headers, reasoning.whyStandard * Tier 3(去除噪音): lifecycleHistory, autoApprovable, reviewedBy/At, sourceFile, * publishedAt/By, headerPaths, includeHeaders, quality.*, stats.*, * reasoning.sources/qualitySignals/alternatives, content.codeChanges/verification */ function _projectForAgent(json) { // content 精简:保留 pattern/markdown/rationale/steps,去除 codeChanges/verification const content = json.content ? { ...(json.content.pattern ? { pattern: json.content.pattern } : {}), ...(json.content.markdown ? { markdown: json.content.markdown } : {}), ...(json.content.rationale ? { rationale: json.content.rationale } : {}), ...((json.content.steps?.length ?? 0) > 0 ? { steps: json.content.steps } : {}), } : undefined; // reasoning 精简:保留 whyStandard + confidence + sources(可信度证据链) const reasoning = json.reasoning ? { ...(json.reasoning.whyStandard ? { whyStandard: json.reasoning.whyStandard } : {}), ...(json.reasoning.confidence != null ? { confidence: json.reasoning.confidence } : {}), ...((json.reasoning.sources?.length ?? 0) > 0 ? { sources: json.reasoning.sources } : {}), } : undefined; // constraints 精简:仅保留 guards 和 sideEffects const constraints = (json.constraints?.guards?.length ?? 0) > 0 || (json.constraints?.sideEffects?.length ?? 0) > 0 ? { ...((json.constraints?.guards?.length ?? 0) > 0 ? { guards: json.constraints.guards } : {}), ...((json.constraints?.sideEffects?.length ?? 0) > 0 ? { sideEffects: json.constraints.sideEffects } : {}), } : undefined; return { // ── 标识 ── id: json.id, title: json.title, description: json.description, trigger: json.trigger || '', // ── 分类 ── kind: json.kind, language: json.language, category: json.category, knowledgeType: json.knowledgeType, complexity: json.complexity, tags: (json.tags?.length ?? 0) > 0 ? json.tags : undefined, // ── Agent 可操作指令(Tier 1 黄金字段) ── whenClause: json.whenClause || undefined, doClause: json.doClause || undefined, dontClause: json.dontClause || undefined, coreCode: json.coreCode || undefined, // ── 内容 ── content: content && Object.keys(content).length > 0 ? content : undefined, // ── 上下文 ── headers: (json.headers?.length ?? 0) > 0 ? json.headers : undefined, reasoning: reasoning && Object.keys(reasoning).length > 0 ? reasoning : undefined, // ── 关系(仅非空桶) ── relations: _compactRelations(json.relations), // ── 约束(仅 guards + sideEffects) ── constraints, }; } export async function listByKind(ctx, kind, args) { const ks = ctx.container.get('knowledgeService'); const filters = { kind }; if (args.language) { filters.language = args.language; } if (args.category) { filters.category = args.category; } const result = await ks.list(filters, { page: 1, pageSize: args.limit || 20 }); const items = (result?.data || []).map(_projectItem); return envelope({ success: true, data: { kind, count: items.length, total: result?.pagination?.total || items.length, items }, meta: { tool: `autosnippet_list_${kind}s` }, }); } export async function listRecipes(ctx, args) { const ks = ctx.container.get('knowledgeService'); const filters = {}; if (args.kind) { filters.kind = args.kind; } if (args.language) { filters.language = args.language; } if (args.category) { filters.category = args.category; } if (args.knowledgeType) { filters.knowledgeType = args.knowledgeType; } if (args.complexity) { filters.complexity = args.complexity; } if (args.status) { filters.lifecycle = args.status; } const result = await ks.list(filters, { page: 1, pageSize: args.limit || 20 }); const items = (result?.data || []).map(_projectItem); return envelope({ success: true, data: { count: items.length, total: result?.pagination?.total || items.length, items }, meta: { tool: 'autosnippet_list_recipes' }, }); } export async function getRecipe(ctx, args) { if (!args.id) { throw new Error('id is required'); } const ks = ctx.container.get('knowledgeService'); const entry = await ks.get(args.id); if (!entry) { throw new Error(`Knowledge entry not found: ${args.id}`); } const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry; // Agent 投影:去除运营/审计/统计噪音,只交付可操作字段 const projected = _projectForAgent(json); return envelope({ success: true, data: projected, meta: { tool: 'autosnippet_get_recipe' } }); } export async function recipeInsights(ctx, args) { if (!args.id) { throw new Error('id is required'); } const ks = ctx.container.get('knowledgeService'); const entry = await ks.get(args.id); if (!entry) { throw new Error(`Knowledge entry not found: ${args.id}`); } const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry; // 聚合关系摘要 const relationsSummary = {}; if (json.relations) { for (const [type, targets] of Object.entries(json.relations)) { if (Array.isArray(targets) && targets.length > 0) { relationsSummary[type] = targets.length; } } } // 约束条件概览 const constraintsSummary = {}; if (json.constraints) { for (const [type, items] of Object.entries(json.constraints)) { if (Array.isArray(items) && items.length > 0) { constraintsSummary[type] = items; } } } const insights = { id: json.id, title: json.title, trigger: json.trigger || '', kind: json.kind, lifecycle: json.lifecycle, language: json.language, category: json.category, knowledgeType: json.knowledgeType, quality: { overall: json.quality?.overall ?? null, completeness: json.quality?.completeness ?? null, adaptation: json.quality?.adaptation ?? null, documentation: json.quality?.documentation ?? null, }, stats: { adoptions: json.stats?.adoptions ?? 0, applications: json.stats?.applications ?? 0, guardHits: json.stats?.guardHits ?? 0, views: json.stats?.views ?? 0, searchHits: json.stats?.searchHits ?? 0, }, content: { hasPattern: !!json.content?.pattern, hasRationale: !!json.content?.rationale, hasMarkdown: !!json.content?.markdown, stepsCount: json.content?.steps?.length ?? 0, codeChangesCount: json.content?.codeChanges?.length ?? 0, }, relations: relationsSummary, constraints: constraintsSummary, tags: json.tags || [], complexity: json.complexity, scope: json.scope, createdBy: json.createdBy, createdAt: json.createdAt, updatedAt: json.updatedAt, }; return envelope({ success: true, data: insights, meta: { tool: 'autosnippet_recipe_insights' } }); } export async function confirmUsage(ctx, args) { if (!args.recipeId) { throw new Error('recipeId is required'); } const ks = ctx.container.get('knowledgeService'); const usageType = args.usageType || 'adoption'; const feedback = args.feedback || null; await ks.incrementUsage(args.recipeId, usageType, { feedback, actor: 'mcp_user', }); // 持久化反馈到 FeedbackCollector(如有反馈内容) if (feedback) { try { const feedbackCollector = ctx.container.get('feedbackCollector'); if (feedbackCollector) { feedbackCollector.record('feedback', args.recipeId, { usageType, comment: feedback, }); } } catch { /* feedbackCollector 降级不影响主流程 */ } } return envelope({ success: true, data: { recipeId: args.recipeId, usageType, feedback }, message: `已记录使用 ${args.recipeId} 的${usageType === 'adoption' ? '采纳' : '应用'}`, meta: { tool: 'autosnippet_confirm_usage' }, }); }