bookworm-smart-assistant/scripts/skill-chain-recommender.js

196 lines
5.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* 技能链推荐引擎 (v5.1)
*
* 基于 composable 关系图 + 工作流模式
* 推荐技能调用链 (最多 4 )
*
* 核心函数:
* buildComposableGraph(index) 邻接表
* recommendChain(primarySkill, graph, workflowPatterns, maxLen) 推荐链
*/
const fs = require('fs');
const path = require('path');
/**
* skills-index.json 构建 composable 关系邻接表
* @param {Object} index - skills-index.json
* @returns {Object} { skillName { enhances, requires, enhancedBy } }
*/
function buildComposableGraph(index) {
const graph = {};
// 初始化所有技能
for (const skill of (index.skills || [])) {
graph[skill.name] = { enhances: [], requires: [], enhancedBy: [] };
}
// 填充关系
for (const skill of (index.skills || [])) {
const comp = skill.composable || {};
if (comp.enhances) {
for (const target of comp.enhances) {
if (graph[skill.name]) {
graph[skill.name].enhances.push(target);
}
// 反向关系
if (graph[target]) {
graph[target].enhancedBy.push(skill.name);
}
}
}
if (comp.requires) {
for (const target of comp.requires) {
if (graph[skill.name]) {
graph[skill.name].requires.push(target);
}
}
}
}
return graph;
}
/**
* 推荐技能调用链
* @param {string} primarySkill - 主技能
* @param {Object} graph - buildComposableGraph 的结果
* @param {Object} workflowPatterns - minePatterns 的结果 (可选)
* @param {number} maxLen - 最大链长 (默认 4)
* @returns {Array<{ chain: string[], score: number, reason: string }>} 推荐链列表
*/
function recommendChain(primarySkill, graph, workflowPatterns, maxLen = 4) {
const chains = [];
const node = graph[primarySkill];
if (!node) return chains;
// 策略 1: enhances 链 (A enhances B enhances C)
const enhanceChain = [primarySkill];
const visited = new Set([primarySkill]);
let current = primarySkill;
for (let i = 1; i < maxLen; i++) {
const currentNode = graph[current];
if (!currentNode || currentNode.enhances.length === 0) break;
// 选择未访问过的 enhances 目标
const next = currentNode.enhances.find(s => !visited.has(s) && graph[s]);
if (!next) break;
enhanceChain.push(next);
visited.add(next);
current = next;
}
if (enhanceChain.length >= 2) {
chains.push({
chain: enhanceChain,
score: enhanceChain.length * 0.3,
reason: 'composable-enhances',
});
}
// 策略 2: requires 链 (先执行依赖, 再执行主技能)
if (node.requires.length > 0) {
const reqChain = [...node.requires.slice(0, maxLen - 1), primarySkill];
chains.push({
chain: reqChain,
score: reqChain.length * 0.25,
reason: 'composable-requires',
});
}
// 策略 3: 工作流模式驱动的链
if (workflowPatterns && workflowPatterns.bigrams) {
const wfChain = [primarySkill];
let wfCurrent = primarySkill;
const wfVisited = new Set([primarySkill]);
for (let i = 1; i < maxLen; i++) {
// 找 bigram 中 wfCurrent 的最高频后继
let bestNext = null;
let bestCount = 0;
for (const [key, count] of Object.entries(workflowPatterns.bigrams)) {
const [from, to] = key.split('→');
if (from === wfCurrent && !wfVisited.has(to) && count > bestCount) {
bestNext = to;
bestCount = count;
}
}
if (!bestNext) break;
wfChain.push(bestNext);
wfVisited.add(bestNext);
wfCurrent = bestNext;
}
if (wfChain.length >= 2) {
chains.push({
chain: wfChain,
score: wfChain.length * 0.2 + 0.1,
reason: 'workflow-pattern',
});
}
}
// 策略 4: enhancedBy → primarySkill → enhances (上下文丰富链)
if (node.enhancedBy.length > 0 && node.enhances.length > 0) {
const contextChain = [
node.enhancedBy[0],
primarySkill,
...node.enhances.slice(0, maxLen - 2),
].slice(0, maxLen);
chains.push({
chain: contextChain,
score: contextChain.length * 0.2,
reason: 'context-bridge',
});
}
// 按分数排序并去重
chains.sort((a, b) => b.score - a.score);
return chains;
}
// 模块导出
if (typeof module !== 'undefined') {
module.exports = { buildComposableGraph, recommendChain };
}
// CLI 入口
if (require.main === module) {
const detectClaudeRoot = () => require('./paths.config.js').PATHS.root;
const ROOT = detectClaudeRoot();
const indexFile = path.join(ROOT, 'skills-index.json');
if (!fs.existsSync(indexFile)) {
console.error('skills-index.json 不存在');
process.exit(1);
}
const skill = process.argv[2];
if (!skill) {
console.log('Usage: node skill-chain-recommender.js <skillName>');
process.exit(0);
}
const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
const graph = buildComposableGraph(index);
const chains = recommendChain(skill, graph, null, 4);
console.log(`=== 技能链推荐: ${skill} ===`);
if (chains.length === 0) {
console.log('无推荐链');
} else {
for (const c of chains) {
console.log(` [${c.reason}] ${c.chain.join(' → ')} (score: ${c.score.toFixed(2)})`);
}
}
}