autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
424 lines (423 loc) • 16.7 kB
JavaScript
/**
* MCP 整合 Handler — 参数路由层
*
* 将整合后的工具(autosnippet_search / knowledge / structure / graph / guard / skill)
* 按 operation / mode 参数路由到已有 handler 实现。
*
* 不包含业务逻辑,仅做参数解构 → 路由 → 转发。
*
* autosnippet_bootstrap 已迁移到 bootstrap-external.js(外部 Agent 路径)。
*/
import { getRequiredFieldsDescription } from '#domain/knowledge/FieldSpec.js';
import { getDeveloperIdentity } from '#shared/developer-identity.js';
import { envelope } from '../envelope.js';
import * as browseHandlers from './browse.js';
import * as guardHandlers from './guard.js';
import * as searchHandlers from './search.js';
import * as skillHandlers from './skill.js';
import * as structureHandlers from './structure.js';
// ─── autosnippet_search (整合 4 → 1) ────────────────────────
/**
* 统合搜索:根据 mode 参数路由到对应搜索 handler
* auto (默认) → search()
* keyword → keywordSearch()
* semantic → semanticSearch()
* context → contextSearch()
*/
export async function consolidatedSearch(ctx, args) {
const mode = args.mode || 'auto';
switch (mode) {
case 'keyword':
return searchHandlers.keywordSearch(ctx, args);
case 'semantic':
return searchHandlers.semanticSearch(ctx, args);
case 'context':
return searchHandlers.contextSearch(ctx, args);
default:
return searchHandlers.search(ctx, { ...args, mode });
}
}
// ─── autosnippet_knowledge (整合 7 → 1) ─────────────────────
/**
* 知识浏览:根据 operation 参数路由
* list (默认) → listByKind() 或 listRecipes()
* get → getRecipe()
* insights → recipeInsights()
* confirm_usage → confirmUsage()
*/
export async function consolidatedKnowledge(ctx, args) {
const op = args.operation || 'list';
switch (op) {
case 'list': {
const kind = args.kind;
if (kind && kind !== 'all') {
return browseHandlers.listByKind(ctx, kind, args);
}
return browseHandlers.listRecipes(ctx, args);
}
case 'get':
return browseHandlers.getRecipe(ctx, args);
case 'insights':
return browseHandlers.recipeInsights(ctx, args);
case 'confirm_usage':
// confirmUsage expects { recipeId, usageType, feedback }
// 适配:如果传了 id 但没传 recipeId,自动映射
if (args.id && !args.recipeId) {
args.recipeId = args.id;
}
return browseHandlers.confirmUsage(ctx, args);
default:
throw new Error(`Unknown knowledge operation: ${op}. Expected: list, get, insights, confirm_usage`);
}
}
// ─── autosnippet_structure (整合 3 → 1) ─────────────────────
/**
* 项目结构:根据 operation 参数路由
* targets (默认) → getTargets()
* files → getTargetFiles()
* metadata → getTargetMetadata()
*/
export async function consolidatedStructure(ctx, args) {
const op = args.operation || 'targets';
switch (op) {
case 'targets':
return structureHandlers.getTargets(ctx, args);
case 'files':
return structureHandlers.getTargetFiles(ctx, args);
case 'metadata':
return structureHandlers.getTargetMetadata(ctx, args);
default:
throw new Error(`Unknown structure operation: ${op}. Expected: targets, files, metadata`);
}
}
// ─── autosnippet_call_context (Phase 5) ─────────────────────
/** 调用链上下文查询:直接转发到 structure.callContext */
export async function consolidatedCallContext(ctx, args) {
return structureHandlers.callContext(ctx, args);
}
// ─── autosnippet_graph (整合 4 → 1) ─────────────────────────
/**
* 知识图谱:根据 operation 参数路由
* query → graphQuery()
* impact → graphImpact()
* path → graphPath()
* stats → graphStats()
*/
export async function consolidatedGraph(ctx, args) {
const op = args.operation;
if (!op) {
throw new Error('Missing required parameter: operation. Expected: query, impact, path, stats');
}
switch (op) {
case 'query':
return structureHandlers.graphQuery(ctx, args);
case 'impact':
return structureHandlers.graphImpact(ctx, args);
case 'path':
return structureHandlers.graphPath(ctx, args);
case 'stats':
return structureHandlers.graphStats(ctx);
default:
throw new Error(`Unknown graph operation: ${op}. Expected: query, impact, path, stats`);
}
}
// ─── autosnippet_guard (整合 3 → 1) ─────────────────────────
/**
* Guard 检查:按参数自动路由
* operation: 'reverse_audit' → guardReverseAudit() (Recipe→Code 反向验证)
* operation: 'coverage_matrix' → guardCoverageMatrix() (模块覆盖率矩阵)
* operation: 'compliance_report' → guardComplianceReport() (3D 合规报告)
* 无参数 → guardReview() (自动 git diff 检测 + inline recipe)
* 有 files → guardReview() (指定文件 + inline recipe) — files 为 string[] 或 {path}[]
* 有 code → guardCheck() (单文件内联检查)
*/
export async function consolidatedGuard(ctx, args) {
// operation 显式路由
if (args.operation === 'reverse_audit') {
return guardHandlers.guardReverseAudit(ctx, args);
}
if (args.operation === 'coverage_matrix') {
return guardHandlers.guardCoverageMatrix(ctx, args);
}
if (args.operation === 'compliance_report') {
return guardHandlers.guardComplianceReport(ctx, args);
}
// 有 code → 单文件检查(旧模式)
if (args.code) {
return guardHandlers.guardCheck(ctx, args);
}
// 有 files(string[] 或 {path}[])或无参数 → review 模式
// review 模式内部处理 files 参数和自动检测
return guardHandlers.guardReview(ctx, args);
}
// ─── autosnippet_skill (整合 6 → 1) ─────────────────────────
/**
* Skill 管理:根据 operation 参数路由
* list → listSkills()
* load → loadSkill()
* create → createSkill()
* update → updateSkill()
* delete → deleteSkill()
* suggest → suggestSkills()
*/
export async function consolidatedSkill(ctx, args) {
const op = args.operation;
if (!op) {
throw new Error('Missing required parameter: operation. Expected: list, load, create, update, delete, suggest, feedback');
}
// loadSkill expects { skillName }, map from { name }
if (args.name && !args.skillName) {
args.skillName = args.name;
}
switch (op) {
case 'list':
return skillHandlers.listSkills(ctx);
case 'load':
return skillHandlers.loadSkill(ctx, args);
case 'create':
return skillHandlers.createSkill(ctx, args);
case 'update':
return skillHandlers.updateSkill(ctx, args);
case 'delete':
return skillHandlers.deleteSkill(ctx, args);
case 'suggest':
return skillHandlers.suggestSkills(ctx);
case 'feedback':
return skillHandlers.recordFeedback(ctx, args);
default:
throw new Error(`Unknown skill operation: ${op}. Expected: list, load, create, update, delete, suggest, feedback`);
}
}
// ─── autosnippet_submit_knowledge (unified pipeline) ──────────────────────
/**
* 统一提交管线:单条与批量走同一代码路径。
*
* 流程:
* 1. 限流
* 2. V3 字段增强(MCP 特有预处理)
* 3. RecipeProductionGateway.create() — 统一管道
* 4. Bootstrap session 追踪
* 5. 返回统一结果
*
* 设计原则:
* - 不降级:缺字段不自动补全,要求 Agent 一次性生成完整数据
* - 不碎片化:优先增强已有 Recipe,而非总新建
* - 不重复提交:拒绝时不创建任何记录
* - 单条/批量完全一致的校验与融合逻辑
*/
export async function enhancedSubmitKnowledge(ctx, args) {
const { RecipeProductionGateway } = await import('#service/knowledge/RecipeProductionGateway.js');
const items = args.items;
if (!items || !Array.isArray(items) || items.length === 0) {
return envelope({
success: false,
errorCode: 'INVALID_INPUT',
message: 'items 数组是必需的且不能为空。请传入 items: [{ title, language, ... }]',
meta: { tool: 'autosnippet_submit_knowledge' },
});
}
const skipConsolidation = args.skipConsolidation === true;
const source = args.source || 'mcp';
const dimensionId = args.dimensionId;
const clientId = args.client_id;
const supersedes = args.supersedes;
// ── Step 1: 限流 ──
const { checkRecipeSave } = await import('#http/middleware/RateLimiter.js');
const { resolveProjectRoot } = await import('#shared/resolveProjectRoot.js');
const projectRoot = resolveProjectRoot(ctx.container);
const limitCheck = checkRecipeSave(projectRoot, clientId || process.env.USER || 'mcp-client');
if (!limitCheck.allowed) {
return envelope({
success: false,
message: `提交过于频繁,请 ${limitCheck.retryAfter}s 后再试。`,
errorCode: 'RATE_LIMIT',
meta: { tool: 'autosnippet_submit_knowledge' },
});
}
// ── Step 2: MCP 特有预处理 ──
// 注入批次级选项到各条目
for (const item of items) {
if (!item.source) {
item.source = source;
}
if (dimensionId && !item.dimensionId) {
item.dimensionId = dimensionId;
}
}
// 获取 bootstrapSession 已提交标题用于跨维度去重
let existingTitles;
try {
const sessionManager = ctx.container.get('bootstrapSessionManager');
const bsSession = sessionManager?.getSession?.();
if (bsSession?.submissionTracker?.getAllSubmittedTitles) {
existingTitles = bsSession.submissionTracker.getAllSubmittedTitles();
}
}
catch {
/* best effort */
}
// ── Step 3: 委托 RecipeProductionGateway 统一管道 ──
const knowledgeService = ctx.container.get('knowledgeService');
let consolidationAdvisor = null;
try {
consolidationAdvisor = ctx.container.get('consolidationAdvisor');
}
catch {
/* not registered */
}
let proposalRepository = null;
try {
proposalRepository = ctx.container.get('proposalRepository');
}
catch {
/* not registered */
}
const gateway = new RecipeProductionGateway({
knowledgeService,
projectRoot,
consolidationAdvisor: consolidationAdvisor ?? null,
proposalRepository: proposalRepository ?? null,
});
const gatewayResult = await gateway.create({
source: 'mcp-external',
items: items,
options: {
skipSimilarityCheck: true,
skipConsolidation,
supersedes,
existingTitles,
userId: getDeveloperIdentity(),
},
});
// ── Step 4: Bootstrap session 追踪 ──
for (const created of gatewayResult.created) {
_trackSubmission(ctx, items.find((it) => it.title === created.title) || {}, dimensionId, created.id);
}
for (const rej of gatewayResult.rejected) {
const item = items[rej.index] || {};
_trackRejection(ctx, item, dimensionId);
}
// ── Step 5: 构建统一响应 ──
const successCount = gatewayResult.created.length;
const data = {
count: successCount,
total: items.length,
};
if (gatewayResult.created.length > 0) {
data.ids = gatewayResult.created.map((c) => c.id);
}
if (gatewayResult.rejected.length > 0) {
const rejectedItems = gatewayResult.rejected.map((r) => ({
index: r.index,
title: r.title,
errors: r.errors,
warnings: r.warnings,
}));
const allMissing = [...new Set(rejectedItems.flatMap((it) => it.errors))];
data.rejectedItems = rejectedItems;
data.rejectedSummary = {
rejectedCount: rejectedItems.length,
commonErrors: allMissing,
message: `${rejectedItems.length}/${items.length} 条知识条目因校验未通过被拒绝。`,
};
}
if (gatewayResult.blocked.length > 0) {
data.blockedItems = gatewayResult.blocked;
data.blockedSummary = {
blockedCount: gatewayResult.blocked.length,
message: `${gatewayResult.blocked.length} 条因融合分析被阻塞(与已有 Recipe 重叠或实质性不足)。设 skipConsolidation: true 可跳过。`,
};
}
const createdProposals = [];
for (const m of gatewayResult.merged) {
createdProposals.push({
proposalId: m.proposalId,
type: m.type,
targetRecipe: { id: m.targetRecipeId, title: m.targetTitle },
status: m.status,
expiresAt: m.expiresAt,
message: m.message,
});
}
if (gatewayResult.supersedeProposal) {
createdProposals.push({
proposalId: gatewayResult.supersedeProposal.proposalId,
type: 'supersede',
targetRecipe: { id: supersedes, title: supersedes },
status: 'observing',
expiresAt: 0,
message: `已创建替代提案。`,
});
}
if (createdProposals.length > 0) {
data.proposals = createdProposals;
data.proposalSummary = {
proposalCount: createdProposals.length,
message: `${createdProposals.length} 条已创建进化提案,系统将在观察窗口到期后自动执行。无需额外操作。`,
};
}
// 全部拒绝 → 特殊错误响应
if (successCount === 0 && gatewayResult.rejected.length === items.length) {
const allMissing = [...new Set(gatewayResult.rejected.flatMap((it) => it.errors))];
return envelope({
success: false,
errorCode: 'INCOMPLETE_SUBMISSION',
message: `全部 ${items.length} 条知识条目被拒绝。请在单次调用中补齐所有字段后重新提交。`,
data: {
rejectedItems: data.rejectedItems,
requiredFields: getRequiredFieldsDescription(),
commonErrors: allMissing,
},
meta: { tool: 'autosnippet_submit_knowledge' },
});
}
const allOk = successCount === items.length;
return envelope({
success: successCount > 0,
data,
message: allOk
? `已提交 ${successCount} 条知识条目。`
: `已提交 ${successCount}/${items.length} 条知识条目。`,
meta: { tool: 'autosnippet_submit_knowledge' },
});
}
function _getSession(ctx) {
try {
const sessionManager = ctx.container.get('bootstrapSessionManager');
const session = sessionManager?.getSession?.();
if (!session?.submissionTracker) {
return null;
}
const progress = session.getProgress();
return { session, dimId: progress.remainingDimIds[0] || 'unknown' };
}
catch {
return null;
}
}
function _trackSubmission(ctx, item, dimensionId, recipeId) {
const s = _getSession(ctx);
if (!s) {
return;
}
try {
const dimId = dimensionId || item.dimensionId || s.dimId;
s.session.submissionTracker?.recordSubmission(dimId, item, recipeId);
}
catch {
/* best effort */
}
}
function _trackRejection(ctx, item, dimensionId) {
const s = _getSession(ctx);
if (!s) {
return;
}
try {
const dimId = dimensionId || item.dimensionId || s.dimId;
s.session.submissionTracker?.recordRejection(dimId, item.title || '(untitled)', 'validation failed');
}
catch {
/* best effort */
}
}