autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
78 lines (77 loc) • 3.31 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import { getProjectRecipesPath } from '../../infrastructure/config/Paths.js';
import { jaccardSimilarity, tokenizeForSimilarity } from '../../shared/similarity.js';
/** 计算候选与单个 Recipe 的综合相似度 */
function computeSimilarity(candidate, recipe) {
const titleSim = jaccardSimilarity(tokenizeForSimilarity(candidate.title), tokenizeForSimilarity(recipe.title));
const summarySim = jaccardSimilarity(tokenizeForSimilarity(candidate.summary || candidate.description || ''), tokenizeForSimilarity(recipe.summary || recipe.description || ''));
const codeSim = jaccardSimilarity(tokenizeForSimilarity(candidate.code, 3), tokenizeForSimilarity(recipe.code, 3));
// 加权: title 30%, summary 30%, code 40%
return titleSim * 0.3 + summarySim * 0.3 + codeSim * 0.4;
}
/** 从磁盘读取所有 Recipe MD 文件并提取基本结构 */
function loadRecipesFromDisk(recipesDir) {
const recipes = [];
if (!fs.existsSync(recipesDir)) {
return recipes;
}
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name.startsWith('.')) {
continue;
}
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
}
else if (entry.isFile() && entry.name.endsWith('.md')) {
try {
const content = fs.readFileSync(full, 'utf8');
const titleMatch = content.match(/^#\s+(.+)/m);
const _fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
const codeMatch = content.match(/```\w*\n([\s\S]*?)```/);
const summaryMatch = content.match(/summary[_cn]*:\s*(.+)/i);
recipes.push({
file: path.relative(recipesDir, full),
title: titleMatch?.[1]?.trim() || path.basename(full, '.md'),
summary: summaryMatch?.[1]?.trim() || '',
code: codeMatch?.[1]?.trim() || '',
});
}
catch {
/* skip unreadable */
}
}
}
};
walk(recipesDir);
return recipes;
}
/**
* 在项目知识库中查找与候选相似的 Recipe
* @param projectRoot 项目根目录
* @param candidate { title, summary, usageGuide, code }
* @param [opts] { threshold: 0.7, topK: 5 }
* @returns >}
*/
export function findSimilarRecipes(projectRoot, candidate, opts = {}) {
const threshold = opts.threshold ?? 0.7;
const topK = opts.topK ?? 5;
const recipesDir = getProjectRecipesPath(projectRoot);
const recipes = loadRecipesFromDisk(recipesDir);
const results = [];
for (const recipe of recipes) {
const sim = computeSimilarity(candidate, recipe);
if (sim >= threshold) {
results.push({
file: recipe.file,
title: recipe.title,
similarity: Math.round(sim * 1000) / 1000,
});
}
}
results.sort((a, b) => b.similarity - a.similarity);
return results.slice(0, topK);
}
export default { findSimilarRecipes };