Files
metona-ollama-desktop/src/renderer/services/skill-manager.ts
T
thzxx f1625b4353 feat: v4.2 智能进化 — 技能自动生成 + 用户画像 + 人格模板
新增功能:

1. 技能自动生成系统 (Skill Loop)
   - 新增 skills 表:id/name/description/trigger_keywords/tool_chain/success_count/fail_count
   - skill-manager.ts: 从成功工具调用链自动提取可复用技能
   - 关键词匹配 + 成功率加权的技能检索
   - 匹配到的技能自动注入 system prompt(含工具链步骤和成功率)
   - Agent Loop 结束后自动提取,开始前自动匹配注入
   - DB/IPC/Preload 全链路支持

2. 用户画像模型
   - 自动从对话中推断技术栈(14 种技术关键词检测)
   - 画像存储在 settings.user_profile
   - 自动注入 system prompt(技术栈/角色/风格)
   - 随使用不断积累完善

3. 人格模板切换(6 种预设)
   - 🤖 通用助手(默认,温度跟随设置)
   - 💻 编程助手(温度 0.3,注重代码质量)
   - ✍️ 写作助手(温度 0.8,注重文采流畅)
   - 📊 数据分析(温度 0.2,注重精确逻辑)
   - 🌐 翻译助手(温度 0.4,注重信达雅)
   - 🔍 代码审查(温度 0.3,注重安全性能)
   - 设置面板 3×2 网格按钮,点击切换 + 自动调整温度

修改文件:9 个已有 + 1 个新建(skill-manager.ts)
2026-04-18 09:22:25 +08:00

303 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SkillManager - 技能自动生成系统 (v4.2)
* 从成功的工具调用链中提取可复用技能,匹配用户意图后注入上下文
*/
import { state, KEYS } from '../state/state.js';
import { generateId } from '../utils/utils.js';
import { logInfo, logWarn, logSkill } from './log-service.js';
import type { ToolCallRecord, ChatSession } from '../types.js';
/** 技能数据结构 */
export interface Skill {
id: string;
name: string;
description: string;
trigger_keywords: string | null;
tool_chain: string; // JSON: ToolChainStep[]
success_count: number;
fail_count: number;
avg_duration_ms: number;
created_at: number;
updated_at: number;
last_used_at: number | null;
}
/** 工具链步骤 */
interface ToolChainStep {
name: string;
description: string; // 该步骤的作用描述
args_hint: string; // 参数提示(哪些参数是关键的)
}
/** 最小工具链长度,太短的不算技能 */
const MIN_CHAIN_LENGTH = 2;
/** 技能匹配最低阈值 */
const MATCH_THRESHOLD = 0.3;
/**
* 从 Agent Loop 完成后的工具调用记录中提取技能
* 只提取成功的、链长 >= 2 的工具调用序列
*/
export async function extractSkillsFromToolRecords(
toolRecords: ToolCallRecord[],
userMessage: string,
sessionTitle: string
): Promise<number> {
const bridge = (window as any).metonaDesktop;
if (!bridge?.db?.saveSkill) return 0;
// 只看成功的工具调用
const successfulTools = toolRecords.filter(t => t.status === 'success');
if (successfulTools.length < MIN_CHAIN_LENGTH) return 0;
// 提取工具链
const chain: ToolChainStep[] = successfulTools.map(t => ({
name: t.name,
description: describeToolStep(t),
args_hint: extractKeyArgs(t)
}));
const chainNames = chain.map(s => s.name).join(' → ');
// 检查是否已有完全相同的技能链
const existingSkills: Skill[] = await bridge.db.getAllSkills();
const duplicate = existingSkills.find(s => {
try {
const existingChain: ToolChainStep[] = JSON.parse(s.tool_chain);
return existingChain.map((c: ToolChainStep) => c.name).join(' → ') === chainNames;
} catch { return false; }
});
if (duplicate) {
// 已有相同技能,不重复创建
logInfo(`技能已存在,跳过提取: ${duplicate.name}`);
return 0;
}
// 从用户消息中提取触发关键词
const keywords = extractKeywords(userMessage);
// 生成技能名称和描述
const name = generateSkillName(chain, userMessage);
const description = generateSkillDescription(chain, sessionTitle);
const skill: Skill = {
id: `skill_${generateId()}`,
name,
description,
trigger_keywords: keywords,
tool_chain: JSON.stringify(chain),
success_count: 1,
fail_count: 0,
avg_duration_ms: toolRecords.reduce((sum, t) => sum + (t.timestamp - (toolRecords[0]?.timestamp || t.timestamp)), 0),
created_at: Date.now(),
updated_at: Date.now(),
last_used_at: null
};
try {
await bridge.db.saveSkill(skill);
logSkill(`新技能提取: ${name}`, `链: ${chainNames}`);
return 1;
} catch (err) {
logWarn('技能保存失败', (err as Error).message);
return 0;
}
}
/**
* 匹配用户消息与已有技能
* 返回匹配度最高的技能列表
*/
export async function matchSkills(userMessage: string, limit = 3): Promise<Skill[]> {
const bridge = (window as any).metonaDesktop;
if (!bridge?.db?.getAllSkills) return [];
try {
const allSkills: Skill[] = await bridge.db.getAllSkills();
if (allSkills.length === 0) return [];
const messageLower = userMessage.toLowerCase();
const messageWords = new Set(messageLower.split(/[\s,,。!?、;:""''()\[\]【】]+/).filter(w => w.length > 1));
const scored = allSkills.map(skill => {
let score = 0;
// 关键词匹配
if (skill.trigger_keywords) {
const kwSet = new Set(skill.trigger_keywords.toLowerCase().split(',').map(k => k.trim()));
for (const kw of kwSet) {
if (kw && messageLower.includes(kw)) score += 0.4;
}
}
// 名称和描述匹配
const nameWords = skill.name.toLowerCase().split(/[\s→\-]+/);
for (const w of nameWords) {
if (w.length > 1 && messageWords.has(w)) score += 0.2;
}
// 成功率加成
const total = skill.success_count + skill.fail_count;
if (total > 0) score *= (0.5 + 0.5 * (skill.success_count / total));
return { skill, score };
});
return scored
.filter(s => s.score >= MATCH_THRESHOLD)
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(s => s.skill);
} catch {
return [];
}
}
/**
* 从匹配的技能构建 system prompt 上下文
*/
export function buildSkillContext(skills: Skill[]): string {
if (skills.length === 0) return '';
const parts = skills.map(skill => {
let chain: ToolChainStep[] = [];
try { chain = JSON.parse(skill.tool_chain); } catch { return ''; }
const steps = chain.map((s, i) =>
` ${i + 1}. ${s.name}${s.description}${s.args_hint ? ` — 参数: ${s.args_hint}` : ''}`
).join('\n');
const total = skill.success_count + skill.fail_count;
const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100;
return `【技能】${skill.name}\n${skill.description}\n成功执行 ${skill.success_count} 次(成功率 ${rate}%\n工具链:\n${steps}`;
}).filter(Boolean);
if (parts.length === 0) return '';
return `【可用技能提示 — 以下技能曾成功完成过类似任务,可作为参考】\n\n${parts.join('\n\n')}\n\n提示:如果当前任务与某个技能匹配,可以参考其工具链步骤,但根据实际情况调整参数。`;
}
/**
* 获取所有技能(用于 UI 展示)
*/
export async function getAllSkillsForUI(): Promise<Skill[]> {
const bridge = (window as any).metonaDesktop;
if (!bridge?.db?.getAllSkills) return [];
try {
return await bridge.db.getAllSkills();
} catch {
return [];
}
}
/**
* 删除技能
*/
export async function deleteSkillById(id: string): Promise<boolean> {
const bridge = (window as any).metonaDesktop;
if (!bridge?.db?.deleteSkill) return false;
try {
await bridge.db.deleteSkill(id);
return true;
} catch {
return false;
}
}
/**
* 清空所有技能
*/
export async function clearAllSkills(): Promise<boolean> {
const bridge = (window as any).metonaDesktop;
if (!bridge?.db?.clearAllSkills) return false;
try {
await bridge.db.clearAllSkills();
return true;
} catch {
return false;
}
}
// ── 辅助函数 ──
/** 生成工具步骤描述 */
function describeToolStep(t: ToolCallRecord): string {
const args = t.arguments || {};
switch (t.name) {
case 'read_file': return `读取文件 ${args.path || ''}`;
case 'write_file': return `写入文件 ${args.path || ''}`;
case 'list_directory': return `列出目录 ${args.path || ''}`;
case 'search_files': return `搜索文件 "${args.query || ''}"`;
case 'web_search': return `联网搜索 "${args.query || ''}"`;
case 'web_fetch': return `抓取网页 ${args.url || ''}`;
case 'run_command': return `执行命令 ${String(args.command || '').slice(0, 60)}`;
case 'git': return `Git ${args.action || ''}`;
case 'edit_file': return `编辑文件 ${args.path || ''}`;
case 'delete_file': return `删除 ${args.path || ''}`;
case 'create_directory': return `创建目录 ${args.path || ''}`;
case 'memory_search': return `搜索记忆 "${args.query || ''}"`;
case 'memory_add': return `添加记忆`;
default: return t.name;
}
}
/** 提取关键参数名 */
function extractKeyArgs(t: ToolCallRecord): string {
const args = t.arguments || {};
const keys: string[] = [];
if (args.path) keys.push('path');
if (args.query) keys.push('query');
if (args.url) keys.push('url');
if (args.command) keys.push('command');
if (args.action) keys.push('action');
if (args.branch) keys.push('branch');
return keys.join(', ');
}
/** 从用户消息提取关键词(简单分词) */
function extractKeywords(text: string): string {
// 移除常见停用词
const stopWords = new Set(['我', '你', '的', '了', '是', '在', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这', '请', '帮', '帮我', '可以', '怎么', '什么', '如何', 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'to', 'of', 'and', 'in', 'that', 'have', 'for', 'it', 'with', 'as', 'this', 'from', 'by', 'on', 'not', 'but', 'what', 'all', 'can', 'had', 'how', 'do']);
const words = text.toLowerCase()
.split(/[\s,,。!?、;:""''()\[\]【】<>《》]+/)
.filter(w => w.length > 1 && !stopWords.has(w));
// 取前 8 个关键词
return [...new Set(words)].slice(0, 8).join(',');
}
/** 生成技能名称 */
function generateSkillName(chain: ToolChainStep[], userMessage: string): string {
const firstTool = chain[0].name;
const lastTool = chain[chain.length - 1].name;
// 基于工具链模式命名
const patterns: Record<string, string> = {
'web_search → web_fetch': '联网搜索并抓取详情',
'list_directory → read_file': '浏览目录并读取文件',
'search_files → read_file': '搜索并读取文件',
'search_files → edit_file': '搜索并编辑文件',
'search_files → write_file': '搜索并写入文件',
'run_command → read_file': '执行命令并读取输出',
'web_search → web_fetch → write_file': '搜索、抓取并保存',
};
const chainKey = chain.map(s => s.name).join(' → ');
if (patterns[chainKey]) return patterns[chainKey];
// 基于用户消息前 20 字
const msgPreview = userMessage.slice(0, 20).replace(/[\r\n]/g, ' ');
return `${firstTool} → ... → ${lastTool}${msgPreview}`;
}
/** 生成技能描述 */
function generateSkillDescription(chain: ToolChainStep[], sessionTitle: string): string {
const steps = chain.map((s, i) => `${i + 1}. ${s.description}`).join('');
return `从会话「${sessionTitle}」中提取。步骤:${steps}`;
}