UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

284 lines (283 loc) 13.5 kB
/** * SkillAdvisor — 基于使用模式的 Skill 推荐引擎 * * 分析项目使用行为并推荐创建 Skill: * 1. Guard 违规模式 — 同类违规反复出现 → 编码规范 Skill * 2. Memory 偏好积累 — 用户偏好超过阈值 → 约定总结 Skill * 3. Recipe 分布缺口 — 某类 Recipe 高频使用但无对应 Skill * 4. 搜索 miss — 高频搜索但低命中 → 知识盲区 Skill * * 设计原则: * - 只做分析和推荐,不自动创建(由 Agent 决策执行 create_skill) * - 静默降级:任何数据源读取失败不影响其他维度 * - 零 AI 调用 — 纯规则分析,确保即时返回 * - 推荐结果包含 draft 草稿(name + description + rationale), * Agent 可直接调用 create_skill 创建 */ import fs from 'node:fs'; import path from 'node:path'; import { getProjectSkillsPath } from '../../infrastructure/config/Paths.js'; export class SkillAdvisor { #projectRoot; #knowledgeRepo; #auditRepo; constructor(projectRoot, { knowledgeRepo, auditRepo } = {}) { this.#projectRoot = projectRoot; this.#knowledgeRepo = knowledgeRepo || null; this.#auditRepo = auditRepo || null; } /** * 生成 Skill 推荐列表 * * @returns {{ * suggestions: Array<{ * name: string, * description: string, * rationale: string, * source: string, * priority: 'high' | 'medium' | 'low', * signals: object * }>, * analysisContext: object * }} */ async suggest() { const existingSkills = this.#listExistingProjectSkills(); const suggestions = []; const analysisContext = {}; // ── 维度 1: Guard 违规模式 ── try { const guardInsights = await this.#analyzeGuardPatterns(); analysisContext.guard = guardInsights.summary; suggestions.push(...guardInsights.suggestions.filter((s) => !existingSkills.has(s.name))); } catch { /* silent */ } // ── 维度 2: Memory 偏好积累 ── try { const memoryInsights = this.#analyzeMemoryPatterns(); analysisContext.memory = memoryInsights.summary; suggestions.push(...memoryInsights.suggestions.filter((s) => !existingSkills.has(s.name))); } catch { /* silent */ } // ── 维度 3: Recipe 分布与使用 ── try { const recipeInsights = await this.#analyzeRecipePatterns(); analysisContext.recipes = recipeInsights.summary; suggestions.push(...recipeInsights.suggestions.filter((s) => !existingSkills.has(s.name))); } catch { /* silent */ } // ── 维度 4: 候选积压 ── try { const candidateInsights = await this.#analyzeCandidatePatterns(); analysisContext.candidates = candidateInsights.summary; suggestions.push(...candidateInsights.suggestions.filter((s) => !existingSkills.has(s.name))); } catch { /* silent */ } // 按优先级排序:high > medium > low const priorityOrder = { high: 0, medium: 1, low: 2 }; suggestions.sort((a, b) => (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2)); return { suggestions, existingProjectSkills: [...existingSkills], analysisContext, hint: suggestions.length > 0 ? `发现 ${suggestions.length} 个 Skill 创建建议。你可以使用 autosnippet_skill({ operation: "create" }) 直接创建,也可以根据 rationale 自行判断是否需要。` : '当前项目使用模式暂无明确的 Skill 创建建议。继续使用后会积累更多信号。', }; } // ═══════════════════════════════════════════════════════ // 维度 1: Guard 违规模式分析 // ═══════════════════════════════════════════════════════ async #analyzeGuardPatterns() { const suggestions = []; if (!this.#auditRepo) { return { summary: 'AuditRepo 不可用', suggestions }; } try { const rows = await this.#auditRepo.findTopGuardViolationRules(3, 5); if (rows.length > 0) { const topRule = rows[0]; suggestions.push({ name: `project-guard-${_kebab(topRule.ruleName || 'common')}`, description: `项目编码规范 — 基于高频 Guard 违规「${topRule.ruleName}」(${topRule.cnt} 次)自动推荐`, rationale: `Guard 规则「${topRule.ruleName}」被违反 ${topRule.cnt} 次,说明团队可能不了解此规范。创建 Skill 可以让 AI 在编码时主动提醒,并提供正确写法参考。`, source: 'guard_violations', priority: topRule.cnt >= 10 ? 'high' : 'medium', signals: { ruleName: topRule.ruleName, violationCount: topRule.cnt, allRules: rows }, }); } return { summary: { violationRules: rows.length, topViolations: rows.slice(0, 3) }, suggestions, }; } catch { return { summary: 'Guard audit_log 查询失败', suggestions }; } } // ═══════════════════════════════════════════════════════ // 维度 2: Memory 偏好分析 // ═══════════════════════════════════════════════════════ #analyzeMemoryPatterns() { const suggestions = []; const memoryPath = path.join(this.#projectRoot, '.autosnippet', 'memory.jsonl'); if (!fs.existsSync(memoryPath)) { return { summary: '无 Memory 记录', suggestions }; } try { const raw = fs.readFileSync(memoryPath, 'utf-8').trim(); if (!raw) { return { summary: '无 Memory 记录', suggestions }; } const entries = raw .split('\n') .map((l) => { try { return JSON.parse(l); } catch { return null; } }) .filter(Boolean); const preferences = entries.filter((e) => e.type === 'preference'); if (preferences.length >= 5) { // 有足够多的偏好积累 → 建议归纳为 Skill const sample = preferences .slice(-5) .map((p) => p.content) .join('\n- '); suggestions.push({ name: 'project-conventions', description: `项目约定总结 — 基于 ${preferences.length} 条团队偏好自动推荐`, rationale: `Memory 中已积累 ${preferences.length} 条用户偏好(如"我们不用…"、"以后都…"),建议归纳为一个 Skill 文档,让 AI 在每次对话中都能参考:\n- ${sample}`, source: 'memory_preferences', priority: preferences.length >= 10 ? 'high' : 'medium', signals: { totalPreferences: preferences.length, recentSamples: preferences.slice(-5) }, }); } return { summary: { totalEntries: entries.length, preferences: preferences.length }, suggestions, }; } catch { return { summary: 'Memory 读取失败', suggestions }; } } // ═══════════════════════════════════════════════════════ // 维度 3: Recipe 分布与使用热度 // ═══════════════════════════════════════════════════════ async #analyzeRecipePatterns() { const suggestions = []; if (!this.#knowledgeRepo) { return { summary: 'KnowledgeRepo 不可用', suggestions }; } try { // 按 category 分布 const categories = await this.#knowledgeRepo.countGroupByCategory(); // 按 language 分布 const languages = await this.#knowledgeRepo.countGroupByLanguage(); // 高频使用但无自定义 Skill 的 category const topCategory = categories[0]; if (topCategory && topCategory.cnt >= 10) { const catName = topCategory.category.toLowerCase(); suggestions.push({ name: `project-${_kebab(catName)}-patterns`, description: `${topCategory.category} 模式汇总 — 该类 Recipe 数量最多(${topCategory.cnt} 条),建议创建专属开发指南`, rationale: `项目中 ${topCategory.category} 类 Recipe 高达 ${topCategory.cnt} 条,占比最大。创建一个 Skill 汇总此类别的核心设计模式、常见用法和注意事项,让 AI 在处理相关代码时有更精准的参考。`, source: 'recipe_distribution', priority: 'low', signals: { category: topCategory.category, recipeCount: topCategory.cnt, allCategories: categories, }, }); } // 高使用量 Recipe 统计 let hotRecipes = []; try { hotRecipes = await this.#knowledgeRepo.findHotRecipesByUsage(5, 10); } catch { /* 查询失败时降级为空 */ } return { summary: { categories: categories.length, languages, hotRecipeCount: hotRecipes.length }, suggestions, }; } catch { return { summary: 'Recipe 查询失败', suggestions }; } } // ═══════════════════════════════════════════════════════ // 维度 4: 候选积压分析 // ═══════════════════════════════════════════════════════ async #analyzeCandidatePatterns() { const suggestions = []; if (!this.#knowledgeRepo) { return { summary: 'KnowledgeRepo 不可用', suggestions }; } try { const stats = await this.#knowledgeRepo.getLifecycleCounts(); const rejected = stats?.deprecated ?? 0; // 大量被拒绝 → 提示候选质量 Skill if (rejected >= 10) { suggestions.push({ name: 'project-candidate-quality', description: `候选提交质量指南 — ${rejected} 条候选被拒,建议创建提交标准 Skill`, rationale: `已有 ${rejected} 条候选被驳回(总计 ${stats?.total ?? 0} 条)。创建一个 Skill 明确项目的候选提交标准(哪些代码值得提取、必填字段要求、质量标杆),可以减少返工。`, source: 'candidate_rejection', priority: rejected >= 20 ? 'high' : 'medium', signals: { total: stats?.total ?? 0, pending: stats?.pending ?? 0, rejected }, }); } return { summary: stats || {}, suggestions, }; } catch { return { summary: '候选查询失败', suggestions }; } } // ═══════════════════════════════════════════════════════ // 辅助方法 // ═══════════════════════════════════════════════════════ /** 列出已有的项目级 Skill 名称集合(避免重复推荐) */ #listExistingProjectSkills() { const names = new Set(); const dir = getProjectSkillsPath(this.#projectRoot); try { fs.readdirSync(dir, { withFileTypes: true }) .filter((d) => d.isDirectory()) .forEach((d) => { names.add(d.name); }); } catch { /* no project skills */ } return names; } } /** 字符串转 kebab-case(简化版) */ function _kebab(str) { return (str || 'unknown') .replace(/[^a-zA-Z0-9\s-]/g, '') .replace(/\s+/g, '-') .toLowerCase() .substring(0, 30); } export default SkillAdvisor;