autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
520 lines (519 loc) • 25 kB
JavaScript
/**
* composite.js — 组合工具 + 元工具 (6)
*
* 34. analyze_code Guard + Recipe 搜索组合
* 35. knowledge_overview 知识库全貌一次获取
* 36. submit_with_check 查重 + 提交组合
* ── get_tool_details 元工具: 查询工具 Schema
* ── plan_task 元工具: 任务规划
* ── review_my_output 元工具: 自我质量审查
*/
import { getInternalAgentRequiredFields, getSystemInjectedFields, } from '#domain/knowledge/FieldSpec.js';
import { findSimilarRecipes } from '#service/candidate/SimilarityService.js';
import { checkDimensionType, DIMENSION_DISPLAY_GROUP, stripProjectNamePrefix, } from './_shared.js';
// ────────────────────────────────────────────────────────────
// 34. analyze_code — 组合工具 (Guard + Recipe 搜索)
// ────────────────────────────────────────────────────────────
export const analyzeCode = {
name: 'analyze_code',
description: '综合分析一段代码:Guard 规范检查 + 相关 Recipe 搜索。一次调用完成完整分析,减少多轮工具调用。',
parameters: {
type: 'object',
properties: {
code: { type: 'string', description: '待分析的源码' },
language: { type: 'string', description: '编程语言 (swift/objc/javascript 等)' },
filePath: { type: 'string', description: '文件路径(可选,用于上下文)' },
},
required: ['code'],
},
handler: async (params, ctx) => {
const { code, language, filePath } = params;
const results = {};
// 并行执行 Guard 检查 + Recipe 搜索
const [guardResult, searchResult] = await Promise.all([
(async () => {
try {
const engine = ctx.container.get('guardCheckEngine');
const violations = engine.checkCode(code, language || 'unknown', { scope: 'file' });
return { violationCount: violations.length, violations };
}
catch {
try {
const guardService = ctx.container.get('guardService');
const matches = await guardService.checkCode(code, { language });
return { violationCount: matches.length, violations: matches };
}
catch {
return { violationCount: 0, violations: [] };
}
}
})(),
(async () => {
try {
const searchEngine = ctx.container.get('searchEngine');
// 取代码首段作为搜索词
const query = code.substring(0, 200).replace(/\n/g, ' ');
const rawResults = await searchEngine.search(query, { limit: 5 });
return { results: rawResults || [], total: rawResults?.length || 0 };
}
catch {
return { results: [], total: 0 };
}
})(),
]);
results.guard = guardResult;
results.relatedRecipes = searchResult;
results.filePath = filePath || '(inline)';
const hasFindings = guardResult.violationCount > 0 || searchResult.total > 0;
results._meta = {
confidence: hasFindings ? 'high' : 'low',
hint: hasFindings
? `已完成 Guard 检查(${guardResult.violationCount} 个违规)+ Recipe 搜索(${searchResult.total} 条匹配)。`
: '未发现 Guard 违规,也未找到相关 Recipe。可能需要先冷启动知识库。',
};
return results;
},
};
// ────────────────────────────────────────────────────────────
// 35. knowledge_overview — 组合工具 (一次获取全部类型的 Recipe 统计)
// ────────────────────────────────────────────────────────────
export const knowledgeOverview = {
name: 'knowledge_overview',
description: '一次性获取知识库全貌:各类型 Recipe 分布 + 候选状态 + 知识图谱概况 + 质量概览。比分别调用 get_project_stats + search_recipes 更高效。',
parameters: {
type: 'object',
properties: {
includeTopRecipes: { type: 'boolean', description: '是否包含热门 Recipe 列表,默认 true' },
limit: { type: 'number', description: '每类返回数量,默认 5' },
},
},
handler: async (params, ctx) => {
const { includeTopRecipes = true, limit = 5 } = params;
const result = {};
// 并行获取统计 + 可选的热门列表
const [statsResult, feedbackResult] = await Promise.all([
(async () => {
try {
const knowledgeService = ctx.container.get('knowledgeService');
return knowledgeService.getStats();
}
catch {
return null;
}
})(),
(async () => {
if (!includeTopRecipes) {
return null;
}
try {
const feedbackCollector = ctx.container.get('feedbackCollector');
return feedbackCollector.getTopRecipes(limit);
}
catch {
return null;
}
})(),
]);
if (statsResult) {
result.knowledge = statsResult;
}
// 知识图谱统计
try {
const kgService = ctx.container.get('knowledgeGraphService');
result.knowledgeGraph = kgService.getStats();
}
catch {
/* KG not available */
}
if (feedbackResult) {
result.topRecipes = feedbackResult;
}
const recipes = result.recipes;
const recipeCount = recipes?.total || recipes?.count || 0;
result._meta = {
confidence: recipeCount > 0 ? 'high' : 'none',
hint: recipeCount === 0 ? '知识库为空,建议先执行冷启动(bootstrap_knowledge)。' : null,
};
return result;
},
};
// ────────────────────────────────────────────────────────────
// 36. submit_with_check — 组合工具 (查重 + 提交)
// ────────────────────────────────────────────────────────────
export const submitWithCheck = {
name: 'submit_with_check',
description: '安全提交候选:先执行查重检测,无重复则自动提交。一次调用完成 check_duplicate + submit_knowledge。',
parameters: {
type: 'object',
properties: {
content: {
type: 'object',
description: '{ markdown: "项目特写 Markdown (≥200字符,含代码块)", rationale: "设计原理说明 (必填)", pattern: "核心代码 3-8 行,语法完整" }',
},
title: { type: 'string', description: '候选标题' },
description: { type: 'string', description: '中文简述 ≤80 字' },
trigger: { type: 'string', description: '@前缀 kebab-case 唯一标识符' },
kind: { type: 'string', enum: ['rule', 'pattern', 'fact'] },
topicHint: {
type: 'string',
enum: ['networking', 'ui', 'data', 'architecture', 'conventions'],
},
whenClause: { type: 'string', description: '触发场景英文' },
doClause: { type: 'string', description: '正向指令英文' },
dontClause: { type: 'string', description: '反向约束英文' },
coreCode: { type: 'string', description: '3-8 行纯代码骨架,语法完整可复制' },
tags: { type: 'array', items: { type: 'string' } },
reasoning: { type: 'object', description: '{ whyStandard, sources, confidence }' },
// ── V3 扩展字段 (与 submit_knowledge 对齐) ──
headers: {
type: 'array',
items: { type: 'string' },
description: '依赖的 import/require 行(无 import 时传 [])',
},
usageGuide: { type: 'string', description: '使用指南 Markdown(### 章节格式)' },
scope: {
type: 'string',
enum: ['universal', 'project-specific', 'team-convention'],
description: '适用范围',
},
complexity: {
type: 'string',
enum: ['basic', 'intermediate', 'advanced'],
description: '复杂度',
},
sourceFile: {
type: 'string',
description: 'Recipe md 文件相对路径(由系统自动设置,无需手动填写)',
},
threshold: { type: 'number', description: '相似度阈值,默认 0.7' },
supersedes: {
type: 'string',
description: '被替代的旧 Recipe ID。传入后将创建 supersede 提案,72h 观察窗口后自动替代。',
},
},
required: getInternalAgentRequiredFields(),
},
handler: async (params, ctx) => {
const projectRoot = ctx.projectRoot;
// ── 标题正规化:剥离冗余的项目名前缀 ──
if (params.title) {
params.title = stripProjectNamePrefix(params.title, projectRoot);
}
// ── Bootstrap 维度类型校验 ──
const dimMeta = ctx._dimensionMeta;
if (dimMeta && ctx.source === 'system') {
const rejected = checkDimensionType(dimMeta, params, ctx.logger);
if (rejected) {
return rejected;
}
if (!params.tags) {
params.tags = [];
}
if (!params.tags.includes(dimMeta.id)) {
params.tags.push(dimMeta.id);
}
if (!params.tags.includes('bootstrap')) {
params.tags.push('bootstrap');
}
}
// ── 系统自动设置 ──
const item = {
...params,
language: ctx._projectLanguage || '',
category: dimMeta ? dimMeta.id : 'general',
knowledgeType: dimMeta?.allowedKnowledgeTypes?.[0] || params.knowledgeType || 'code-pattern',
source: ctx.source === 'system' ? 'bootstrap' : 'agent',
agentNotes: dimMeta
? { dimensionId: dimMeta.id, outputType: dimMeta.outputType || 'candidate' }
: null,
};
if (dimMeta && ctx.source === 'system') {
const displayGroup = DIMENSION_DISPLAY_GROUP[dimMeta.id] || dimMeta.id;
item.tags = [...new Set([...(item.tags || []), displayGroup])];
}
// ── 委托 RecipeProductionGateway 统一管道(含查重) ──
try {
const { RecipeProductionGateway } = await import('#service/knowledge/RecipeProductionGateway.js');
const gateway = new RecipeProductionGateway({
knowledgeService: ctx.container.get('knowledgeService'),
projectRoot,
logger: ctx.logger,
proposalRepository: ctx.container.get('proposalRepository') ?? null,
findSimilarRecipes,
});
const gatewayResult = await gateway.create({
source: 'agent-tool',
items: [item],
options: {
skipSimilarityCheck: false,
skipConsolidation: true,
similarityThreshold: params.threshold || 0.7,
supersedes: params.supersedes,
existingTitles: ctx._submittedTitles,
existingFingerprints: ctx._submittedPatterns,
systemInjectedFields: getSystemInjectedFields(),
userId: 'agent',
},
});
// ── 映射 Gateway 结果 → 原有返回格式 ──
// 验证拒绝
if (gatewayResult.rejected.length > 0 && gatewayResult.created.length === 0) {
const rej = gatewayResult.rejected[0];
if (rej.reason === 'validation_failed') {
return {
submitted: false,
status: 'rejected',
reason: 'validation_failed',
errors: rej.errors,
warnings: rej.warnings,
_meta: {
confidence: 'high',
hint: '请根据错误信息调整内容后重新提交。',
},
};
}
return { submitted: false, reason: 'submit_error', error: rej.errors.join('\n') };
}
// 重复阻止
if (gatewayResult.duplicates.length > 0) {
const dup = gatewayResult.duplicates[0];
return {
submitted: false,
reason: 'duplicate_blocked',
similar: dup.similarTo,
highestSimilarity: dup.similarTo[0]?.similarity || 0,
_meta: {
confidence: 'high',
hint: `发现高度相似 Recipe(相似度 ${((dup.similarTo[0]?.similarity || 0) * 100).toFixed(0)}%),已阻止提交。`,
},
};
}
// 成功创建
if (gatewayResult.created.length > 0) {
const created = gatewayResult.created[0];
const raw = created.raw;
const entry = typeof raw.toJSON === 'function'
? raw.toJSON()
: raw;
// 获取低相似度匹配(如有)
const contentObj2 = params.content && typeof params.content === 'object'
? params.content
: { markdown: '', pattern: '' };
const cand = {
title: params.title || '',
summary: params.description || '',
code: contentObj2.markdown || contentObj2.pattern || '',
};
const similar = findSimilarRecipes(projectRoot, cand, { threshold: 0.5, topK: 5 });
return {
submitted: true,
entry,
similar: similar.length > 0 ? similar : [],
...(gatewayResult.supersedeProposal
? { _supersedeProposal: gatewayResult.supersedeProposal }
: {}),
_meta: {
confidence: 'high',
hint: similar.length > 0
? `已提交,但有 ${similar.length} 个低相似度匹配。`
: '已提交,无重复风险。',
},
};
}
return { submitted: false, reason: 'submit_error', error: 'No items created' };
}
catch (err) {
return { submitted: false, reason: 'submit_error', error: err.message };
}
},
};
// ═══════════════════════════════════════════════════════
// 元工具: Lazy Tool Schema 按需加载
// ═══════════════════════════════════════════════════════
/**
* get_tool_details — 查询工具的完整参数 schema
*
* 与 Cline .clinerules 按需加载类似:
* System Prompt 只包含工具名+一行描述,LLM 需要调用某个工具前
* 先通过此元工具获取完整参数定义,避免 prompt 过长浪费 token。
*/
export const getToolDetails = {
name: 'get_tool_details',
description: '查询指定工具的完整参数 Schema。在调用不熟悉的工具之前,先用此工具获取参数详情。',
parameters: {
type: 'object',
properties: {
toolName: {
type: 'string',
description: '要查询的工具名称(snake_case)',
},
},
required: ['toolName'],
},
handler: async ({ toolName }, context) => {
const registry = context.container?.get('toolRegistry');
if (!registry) {
return { error: 'ToolRegistry not available' };
}
const schemas = registry.getToolSchemas();
const found = schemas.find((t) => t.name === toolName);
if (!found) {
const allNames = schemas.map((t) => t.name);
return {
error: `Tool "${toolName}" not found`,
availableTools: allNames,
};
}
return {
name: found.name,
description: found.description,
parameters: found.parameters,
};
},
};
// ─── 元工具: 任务规划 ───────────────────────────────────
export const planTask = {
name: 'plan_task',
description: '分析当前任务并制定结构化执行计划。在开始复杂任务前调用此工具可提高执行效率和决策质量。输出将记录到日志供审计,但不会改变实际执行流程。',
parameters: {
type: 'object',
properties: {
steps: {
type: 'array',
description: '执行步骤列表',
items: {
type: 'object',
properties: {
id: { type: 'number', description: '步骤序号' },
action: { type: 'string', description: '具体动作描述' },
tool: { type: 'string', description: '计划使用的工具名' },
depends_on: { type: 'array', items: { type: 'number' }, description: '依赖的步骤 ID' },
},
required: ['id', 'action'],
},
},
strategy: {
type: 'string',
description: '执行策略说明(如: 先搜索补充示例再批量提交)',
},
estimated_iterations: {
type: 'number',
description: '预估需要的迭代轮数',
},
},
required: ['steps', 'strategy'],
},
handler: async (params, context) => {
const plan = {
steps: params.steps || [],
strategy: params.strategy || '',
estimatedIterations: params.estimated_iterations || params.steps?.length || 1,
};
context.logger?.info('[plan_task] execution plan', plan);
return {
status: 'plan_recorded',
stepCount: plan.steps.length,
strategy: plan.strategy,
message: `执行计划已记录 (${plan.steps.length} 步, 预估 ${plan.estimatedIterations} 轮迭代)。开始按计划执行。`,
};
},
};
// ─── 元工具: 自我质量审查 ───────────────────────────────
export const reviewMyOutput = {
name: 'review_my_output',
description: '回查本次会话中已提交的候选,检查质量红线是否满足。包括: 项目特写风格、description 泛化措辞、代码示例来源标注、Cursor 交付字段完整性等。返回通过/问题列表。建议在提交完所有候选后调用一次进行自检。',
parameters: {
type: 'object',
properties: {
check_rules: {
type: 'array',
description: '要检查的质量规则(可选, 默认检查全部)',
items: { type: 'string' },
},
},
},
handler: async (params, context) => {
const submitted = (context._sessionToolCalls || []).filter((tc) => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check');
if (submitted.length === 0) {
return { status: 'no_candidates', message: '本次会话尚未提交任何候选。' };
}
const issues = [];
const checked = [];
for (const tc of submitted) {
const p = (tc.params || {});
const contentObj3 = (p.content && typeof p.content === 'object' ? p.content : {});
const markdown = contentObj3.markdown || '';
const title = p.title || '';
const description = p.description || '';
const candidateIssues = [];
// 检查 1: 项目特写后缀
if (!title.includes('— 项目特写') && !markdown.includes('— 项目特写')) {
candidateIssues.push('缺少 "— 项目特写" 后缀');
}
// 检查 2: 项目特写融合叙事质量 — 必须同时包含代码和描述性文字
const hasCodeBlock = /```[\s\S]*?```/.test(markdown);
if (!hasCodeBlock) {
candidateIssues.push('特写缺少代码示例,应包含基本用法代码');
}
// 去掉代码块后,剩余描述性文字应足够
const proseLength = markdown
.replace(/```[\s\S]*?```/g, '')
.replace(/[#>\-*`\n]/g, '')
.trim().length;
if (proseLength < 50) {
candidateIssues.push('特写缺少项目特点描述,应融合基本用法和项目特点');
}
// 检查 3: description 泛化措辞
if (/本模块|该文件|这个类|该项目/.test(description)) {
candidateIssues.push('description 使用了泛化措辞,应引用具体类名和数字');
}
// 检查 4: description 过短
if (description.length < 15) {
candidateIssues.push(`description 过短 (${description.length} 字), 应≥15字并包含具体类名和数字`);
}
// 检查 5: content.markdown 过短(可能是空壳)
if (markdown.length < 200) {
candidateIssues.push(`content.markdown 文档过短 (${markdown.length} 字), 可能缺少实质内容`);
}
// 检查 6: 代码示例来源
const hasSourceAnnotation = /\([^)]*\.\w+[^)]*:\d+\)|\([^)]*\.\w+[^)]*\)/.test(markdown);
if (hasCodeBlock && !hasSourceAnnotation) {
candidateIssues.push('代码示例可能缺少来源文件标注 (建议标注 "来源: FileName.m:行号")');
}
// 检查 7: Cursor 交付字段
if (!p.trigger) {
candidateIssues.push('缺少 trigger 字段');
}
if (!p.doClause) {
candidateIssues.push('缺少 doClause 字段');
}
if (!p.kind) {
candidateIssues.push('缺少 kind 字段');
}
if (candidateIssues.length > 0) {
issues.push({ title, issues: candidateIssues });
}
checked.push({
title,
passed: candidateIssues.length === 0,
issueCount: candidateIssues.length,
});
}
if (issues.length === 0) {
return {
status: 'all_passed',
checkedCount: submitted.length,
message: `✅ ${submitted.length} 条候选全部通过质量检查。`,
};
}
const issueLines = issues.flatMap(({ title, issues: iss }) => iss.map((i) => `• "${title}": ${i}`));
return {
status: 'issues_found',
checkedCount: submitted.length,
passedCount: submitted.length - issues.length,
failedCount: issues.length,
details: checked,
message: `⚠️ ${issues.length}/${submitted.length} 条候选存在质量问题:\n${issueLines.join('\n')}\n\n请修正后重新提交。`,
};
},
};