v0.11.11: 彻底移除技能系统,工具 44→42,SQLite 7→6 张表
- 删除 src/renderer/services/skill-manager.ts (480行) - sqlite.ts: 删除 skills 表/索引/迁移/SkillRow接口/CRUD - ipc.ts: 删除 6 个 skills IPC handler - preload.ts/types.d.ts: 删除 skills bridge 方法/类型 - agent-engine.ts: 删除技能匹配上下文注入/自动提取 - tool-registry.ts: 删除 skill_list/skill_view 工具定义/执行 - log-service.ts/sub-agent.ts/workspace-panel.ts: 清理引用 - index.html: 帮助面板/工具卡片更新,44→42 - README/release-notes/DEVELOPMENT/AGENT: 文档全量同步
This commit is contained in:
@@ -205,22 +205,6 @@ export async function initDatabase(): Promise<SQL.Database> {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_traces_session ON traces(session_id, created_at);
|
||||
|
||||
-- v4.2 技能表(自动从成功工具调用链提取)
|
||||
CREATE TABLE IF NOT EXISTS skills (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
summary TEXT DEFAULT '',
|
||||
trigger_keywords TEXT,
|
||||
tool_chain TEXT NOT NULL,
|
||||
success_count INTEGER DEFAULT 0,
|
||||
fail_count INTEGER DEFAULT 0,
|
||||
avg_duration_ms INTEGER DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_skills_updated ON skills(updated_at DESC);
|
||||
`);
|
||||
|
||||
// 兼容迁移:为已有 messages 表补充 attachments 列(文件/视频等附件 JSON)
|
||||
@@ -228,8 +212,6 @@ export async function initDatabase(): Promise<SQL.Database> {
|
||||
|
||||
// 兼容迁移:为已有 messages 表补充 prompt_eval_count 列
|
||||
|
||||
// 兼容迁移:为已有 skills 表补充 summary 列
|
||||
try { db.run("ALTER TABLE skills ADD COLUMN summary TEXT DEFAULT ''"); } catch { /* 列已存在,忽略 */ }
|
||||
|
||||
// 尝试创建 FTS5 全文搜索(可选,sql.js 默认 WASM 可能不包含 FTS5)
|
||||
try {
|
||||
@@ -314,22 +296,6 @@ export interface ToolCallRow {
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
// v4.2
|
||||
export interface SkillRow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
summary: string;
|
||||
trigger_keywords: string | null;
|
||||
tool_chain: string;
|
||||
success_count: number;
|
||||
fail_count: number;
|
||||
avg_duration_ms: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
last_used_at: number | null;
|
||||
}
|
||||
|
||||
export interface TraceRow {
|
||||
id: string;
|
||||
session_id: string;
|
||||
@@ -527,62 +493,6 @@ export function getTracesBySession(sessionId: string): TraceRow[] {
|
||||
return queryAll(getDb(), 'SELECT * FROM traces WHERE session_id = ? ORDER BY step_index ASC', [sessionId]) as unknown as TraceRow[];
|
||||
}
|
||||
|
||||
// ─── Skills CRUD (v4.2) ───
|
||||
|
||||
export function saveSkill(skill: SkillRow): string {
|
||||
runExec(getDb(), `INSERT OR REPLACE INTO skills (id, name, description, summary, trigger_keywords, tool_chain, success_count, fail_count, avg_duration_ms, created_at, updated_at, last_used_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[skill.id, skill.name, skill.description, skill.summary || '', skill.trigger_keywords, skill.tool_chain,
|
||||
skill.success_count, skill.fail_count, skill.avg_duration_ms,
|
||||
skill.created_at, skill.updated_at, skill.last_used_at]
|
||||
);
|
||||
persist();
|
||||
return skill.id;
|
||||
}
|
||||
|
||||
|
||||
export function getAllSkills(): SkillRow[] {
|
||||
return queryAll(getDb(), 'SELECT * FROM skills ORDER BY success_count DESC, updated_at DESC') as unknown as SkillRow[];
|
||||
}
|
||||
|
||||
export function deleteSkill(id: string): void {
|
||||
runExec(getDb(), 'DELETE FROM skills WHERE id = ?', [id]);
|
||||
persist();
|
||||
}
|
||||
|
||||
export function clearAllSkills(): void {
|
||||
getDb().run('DELETE FROM skills');
|
||||
persist();
|
||||
}
|
||||
|
||||
export function searchSkills(keywords: string, limit = 5): SkillRow[] {
|
||||
const likeQ = `%${keywords.replace(/[^\w\u4e00-\u9fff]/g, '%')}%`;
|
||||
return queryAll(getDb(), `
|
||||
SELECT * FROM skills
|
||||
WHERE trigger_keywords LIKE ? OR name LIKE ? OR description LIKE ?
|
||||
ORDER BY success_count DESC, updated_at DESC
|
||||
LIMIT ?
|
||||
`, [likeQ, likeQ, likeQ, limit]) as unknown as SkillRow[];
|
||||
}
|
||||
|
||||
export function incrementSkillUsage(id: string, success: boolean, durationMs: number): void {
|
||||
const d = getDb();
|
||||
const skill = queryOne(d, 'SELECT success_count, fail_count, avg_duration_ms FROM skills WHERE id = ?', [id]) as any;
|
||||
if (!skill) return;
|
||||
|
||||
const newSuccess = success ? skill.success_count + 1 : skill.success_count;
|
||||
const newFail = success ? skill.fail_count : skill.fail_count + 1;
|
||||
const totalUses = newSuccess + newFail;
|
||||
// 指数移动平均
|
||||
const alpha = 0.3;
|
||||
const newAvg = Math.round(alpha * durationMs + (1 - alpha) * (skill.avg_duration_ms || durationMs));
|
||||
|
||||
runExec(d, `UPDATE skills SET success_count = ?, fail_count = ?, avg_duration_ms = ?, updated_at = ?, last_used_at = ? WHERE id = ?`,
|
||||
[newSuccess, newFail, newAvg, Date.now(), Date.now(), id]
|
||||
);
|
||||
persist();
|
||||
}
|
||||
|
||||
// ─── Token 全局统计 ───
|
||||
|
||||
export interface SessionTokenStat {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
saveSetting, getSetting,
|
||||
saveToolCall, getToolCallsBySession,
|
||||
saveTrace, getTracesBySession,
|
||||
saveSkill, getAllSkills, deleteSkill, clearAllSkills, searchSkills, incrementSkillUsage,
|
||||
exportAllSessions, importSessions,
|
||||
getAllSessionsTokenStats
|
||||
} from './db/sqlite.js';
|
||||
@@ -433,32 +432,6 @@ export async function setupIPC(): Promise<void> {
|
||||
catch (err) { sendLog('error', '导入失败', (err as Error).message); return { imported: 0, skipped: 0 }; }
|
||||
});
|
||||
|
||||
// ── Skills (v4.2) ──
|
||||
ipcMain.handle('db:saveSkill', (_, skill) => {
|
||||
try { return { success: true, id: saveSkill(skill) }; }
|
||||
catch (err) { return { success: false, error: (err as Error).message }; }
|
||||
});
|
||||
ipcMain.handle('db:getAllSkills', () => {
|
||||
try { return getAllSkills(); }
|
||||
catch { return []; }
|
||||
});
|
||||
ipcMain.handle('db:deleteSkill', (_, id) => {
|
||||
try { deleteSkill(id); return { success: true }; }
|
||||
catch (err) { return { success: false, error: (err as Error).message }; }
|
||||
});
|
||||
ipcMain.handle('db:clearAllSkills', () => {
|
||||
try { clearAllSkills(); return { success: true }; }
|
||||
catch (err) { return { success: false, error: (err as Error).message }; }
|
||||
});
|
||||
ipcMain.handle('db:searchSkills', (_, query: string, limit?: number) => {
|
||||
try { return searchSkills(query, limit ?? 5); }
|
||||
catch { return []; }
|
||||
});
|
||||
ipcMain.handle('db:incrementSkillUsage', (_, id: string, success: boolean, durationMs: number) => {
|
||||
try { incrementSkillUsage(id, success, durationMs); return { success: true }; }
|
||||
catch (err) { return { success: false, error: (err as Error).message }; }
|
||||
});
|
||||
|
||||
// ── Token 全局统计 ──
|
||||
ipcMain.handle('db:getAllTokenStats', () => {
|
||||
try { return getAllSessionsTokenStats(); }
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ export function createMenu(): void {
|
||||
dialog.showMessageBox(mainWindow!, {
|
||||
type: 'info',
|
||||
title: '关于 Metona Ollama',
|
||||
message: 'Metona Ollama Desktop v0.11.10',
|
||||
message: 'Metona Ollama Desktop v0.11.11',
|
||||
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
|
||||
icon: getIconPath()
|
||||
});
|
||||
|
||||
+1
-7
@@ -77,13 +77,7 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
|
||||
getTraces: (sessionId: string) => ipcRenderer.invoke('db:getTraces', sessionId),
|
||||
exportSessions: () => ipcRenderer.invoke('db:exportSessions'),
|
||||
importSessions: (data: unknown) => ipcRenderer.invoke('db:importSessions', data),
|
||||
// v4.2 Skills
|
||||
saveSkill: (skill: unknown) => ipcRenderer.invoke('db:saveSkill', skill),
|
||||
getAllSkills: () => ipcRenderer.invoke('db:getAllSkills'),
|
||||
deleteSkill: (id: string) => ipcRenderer.invoke('db:deleteSkill', id),
|
||||
clearAllSkills: () => ipcRenderer.invoke('db:clearAllSkills'),
|
||||
searchSkills: (query: string, limit?: number) => ipcRenderer.invoke('db:searchSkills', query, limit),
|
||||
incrementSkillUsage: (id: string, success: boolean, durationMs: number) => ipcRenderer.invoke('db:incrementSkillUsage', id, success, durationMs),
|
||||
|
||||
getAllTokenStats: () => ipcRenderer.invoke('db:getAllTokenStats'),
|
||||
},
|
||||
workspace: {
|
||||
|
||||
@@ -964,7 +964,7 @@ function getToolDisplayName(name: string): string {
|
||||
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||
memory_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
|
||||
skill_list: '技能列表', skill_view: '查看技能',
|
||||
|
||||
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
|
||||
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
||||
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
||||
@@ -981,7 +981,7 @@ function getToolIcon(name: string): string {
|
||||
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||
memory_search: '🧠', memory_add: '💾', memory_replace: '✏️', memory_remove: '🗑️',
|
||||
skill_list: '🎯', skill_view: '👁️',
|
||||
|
||||
session_list: '📋', session_read: '📖', spawn_task: '🤖',
|
||||
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
||||
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
||||
|
||||
+4
-13
@@ -28,7 +28,7 @@
|
||||
<div class="header-left">
|
||||
<span class="logo">🦙</span>
|
||||
<span class="app-title">Metona Ollama</span>
|
||||
<span class="app-version">v0.11.10</span>
|
||||
<span class="app-version">v0.11.11</span>
|
||||
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
@@ -435,13 +435,12 @@
|
||||
<div class="modal-body">
|
||||
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools)</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
|
||||
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 下方 Think 按钮切换,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>上下文长度自动检测</strong> — 切换模型时自动从模型元数据获取实际支持的上下文长度(如 Qwen3.6 27B = 262,144 tokens),无需手动配置</li><li><strong>多模态</strong> — 上传图片或视频(≤10MB),模型需支持 Vision。图片自动压缩,视频 1fps 提取帧序列带时序标注</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,自动剥离注释并按上下文预算智能截断</li><li><strong>AI 回复顶部</strong> — 每条 AI 回复上方展示 📋 系统提示词折叠卡片,可点击查看实际发送给模型的完整上下文</li></ul></div>
|
||||
<div class="help-section"><h4>🔧 Tool Calling(始终开启)</h4><ul><li>所有消息均通过 <strong>Agent Loop 自主调用本地工具</strong>,像一个本地 Agent,无普通聊天模式</li><li><strong>44 个工具</strong>,分为 10 类:<ul><li><strong>文件系统</strong>(16 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files / compress</li><li><strong>命令执行</strong>(1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式,可配置超时)</li><li><strong>联网搜索</strong>(2 个):web_search(支持 SearXNG 元搜索引擎 JSON API / 四引擎 HTML 解析,双模式可切换)/ web_fetch(反爬headers+UA自动切换+自动回退浏览器渲染)</li><li><strong>Git</strong>(1 个):git(17 个子操作,push/pull/clone 内置60-120s超时保护)</li><li><strong>浏览器控制</strong>(9 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_wait / browser_close</li><li><strong>记忆 & 会话 & Skill</strong>(8 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read / skill_list / skill_view</li><li><strong>子代理</strong>(1 个):spawn_task</li><li><strong>系统工具</strong>(6 个):datetime / calculator / random / uuid / json_format / hash</li></ul></li><li>read_file 支持 binary模式读取/base64解码/字节分页/2000行默认+截断hint自动续读</li><li>write_file 支持 base64写二进制文件/mode(overwrite+append)合并append_file功能/10MB</li><li>edit_file 支持 use_regex 正则替换</li><li>search_files 支持正则表达式搜索(use_regex=true)</li><li>browser_screenshot 支持全页面截图+元素截图</li><li>browser_extract 支持CSS选择器提取特定区域</li><li>new browser_wait 等待元素出现或定时等待</li><li>仅 <code>run_command</code> 支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code>、<code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code>、<code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,上下文使用率>80%时自动缩减到3轮</li><li>独立工具自动<strong>并行执行</strong>(16个只读工具加入并行白名单),有依赖关系的工具串行执行</li></ul></div>
|
||||
<div class="help-section"><h4>🔧 Tool Calling(始终开启)</h4><ul><li>所有消息均通过 <strong>Agent Loop 自主调用本地工具</strong>,像一个本地 Agent,无普通聊天模式</li><li><strong>42 个工具</strong>,分为 9 类:<ul><li><strong>文件系统</strong>(16 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files / compress</li><li><strong>命令执行</strong>(1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式,可配置超时)</li><li><strong>联网搜索</strong>(2 个):web_search(支持 SearXNG 元搜索引擎 JSON API / 四引擎 HTML 解析,双模式可切换)/ web_fetch(反爬headers+UA自动切换+自动回退浏览器渲染)</li><li><strong>Git</strong>(1 个):git(17 个子操作,push/pull/clone 内置60-120s超时保护)</li><li><strong>浏览器控制</strong>(9 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_wait / browser_close</li><li><strong>记忆 & 会话</strong>(6 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read</li><li><strong>子代理</strong>(1 个):spawn_task</li><li><strong>系统工具</strong>(6 个):datetime / calculator / random / uuid / json_format / hash</li></ul></li><li>read_file 支持 binary模式读取/base64解码/字节分页/2000行默认+截断hint自动续读</li><li>write_file 支持 base64写二进制文件/mode(overwrite+append)合并append_file功能/10MB</li><li>edit_file 支持 use_regex 正则替换</li><li>search_files 支持正则表达式搜索(use_regex=true)</li><li>browser_screenshot 支持全页面截图+元素截图</li><li>browser_extract 支持CSS选择器提取特定区域</li><li>new browser_wait 等待元素出现或定时等待</li><li>仅 <code>run_command</code> 支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code>、<code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code>、<code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,上下文使用率>80%时自动缩减到3轮</li><li>独立工具自动<strong>并行执行</strong>(16个只读工具加入并行白名单),有依赖关系的工具串行执行</li></ul></div>
|
||||
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li><strong>增量提取</strong>:长对话中每 20 轮自动触发轻量级记忆提取,不等到对话结束</li><li>对话结束时自动触发完整记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 memory 工具主动管理记忆</li><li>记忆写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
||||
<div class="help-section"><h4>🤖 Agent Loop v0.11.10 增强</h4><ul><li><strong>🎬 视频上传</strong>:支持上传 .mp4/.avi/.mov/.mkv/.webm 等视频(≤10MB),自动 1fps 提取帧序列(带时间戳),多模态模型原生理解视频时序关系</li><li><strong>智能上下文压缩</strong>:滑动窗口 + LLM结构化JSON摘要,超过50%上下文窗口自动触发</li><li><strong>流式调用超时保护</strong>:可配置超时(默认300s),Ollama 卡死不再永久阻塞</li><li><strong>HTTP/MCP 超时可配</strong>:设置面板可分别调整 HTTP(默认30s)和 MCP(默认60s)超时</li><li><strong>Final Answer 智能检测</strong>:多模式匹配(Final Answer/最终答案/最终回答/总结),避免漏检或误触</li><li><strong>Token 感知迭代预算</strong>:上下文使用率>80%时自动缩减剩余轮次到3轮</li><li><strong>工具缓存 TTL</strong>:搜索5分钟/网页10分钟/文件30分钟,过期自动刷新</li><li><strong>自动子任务拆解</strong>:检测"同时/分别/以及"等并行关键词,自动 spawn 子代理并行处理</li><li><strong>旧工具结果自动截断</strong>:超过10轮的工具结果自动截断到500字符,控制上下文膨胀</li></ul></div>
|
||||
<div class="help-section"><h4>🤖 Agent Loop v0.11.11 增强</h4><ul><li><strong>🎬 视频上传</strong>:支持上传 .mp4/.avi/.mov/.mkv/.webm 等视频(≤10MB),自动 1fps 提取帧序列(带时间戳),多模态模型原生理解视频时序关系</li><li><strong>智能上下文压缩</strong>:滑动窗口 + LLM结构化JSON摘要,超过50%上下文窗口自动触发</li><li><strong>流式调用超时保护</strong>:可配置超时(默认300s),Ollama 卡死不再永久阻塞</li><li><strong>HTTP/MCP 超时可配</strong>:设置面板可分别调整 HTTP(默认30s)和 MCP(默认60s)超时</li><li><strong>Final Answer 智能检测</strong>:多模式匹配(Final Answer/最终答案/最终回答/总结),避免漏检或误触</li><li><strong>Token 感知迭代预算</strong>:上下文使用率>80%时自动缩减剩余轮次到3轮</li><li><strong>工具缓存 TTL</strong>:搜索5分钟/网页10分钟/文件30分钟,过期自动刷新</li><li><strong>自动子任务拆解</strong>:检测"同时/分别/以及"等并行关键词,自动 spawn 子代理并行处理</li><li><strong>旧工具结果自动截断</strong>:超过10轮的工具结果自动截断到500字符,控制上下文膨胀</li></ul></div>
|
||||
<div class="help-section"><h4>🔌 MCP(Model Context Protocol)</h4><ul><li>支持连接外部 MCP Server,动态扩展工具能力</li><li>设置面板可添加/启用/禁用/删除 MCP 服务器</li><li>MCP 工具以 <code>mcp_{server}_{tool}</code> 前缀注册,与内置工具统一调度</li><li>启动时自动连接已启用的 MCP 服务器</li></ul></div>
|
||||
<div class="help-section"><h4>🔍 SearXNG 元搜索引擎</h4><ul><li>点击顶部 🔍 按钮打开配置面板,可接入自部署的 SearXNG 实例</li><li><strong>JSON 模式</strong>:调用 SearXNG JSON API,聚合 70+ 引擎结果,结构化解析</li><li><strong>HTML 模式</strong>:获取原始搜索结果页面,交由 AI 自行分析提取信息</li><li>支持认证 Key(HTTP Header Authorization),保护私有实例</li><li>启用后替代内置四引擎方案;关闭即回退,无缝切换</li><li>所有参数(引擎、语言、安全搜索、时间范围等)均可独立配置</li></ul></div>
|
||||
<div class="help-section"><h4>📋 自定义文件(SOUL.md / AGENT.md / USER.md)</h4><ul><li>在工作空间目录创建以下文件即可自定义 AI 行为,修改后下一轮对话立即生效</li><li><strong>SOUL.md</strong> — AI 身份、性格、行为准则(<strong>永远不可被压缩</strong>,注入为最高优先级系统提示词)</li><li><strong>AGENT.md</strong> — 工具调用规则、链式调用模式、核心约束(内置精简默认版,可通过工作空间覆盖)</li><li><strong>USER.md</strong> — 用户画像:技术栈、偏好、习惯等个人信息,AI 在对话中自动参考</li><li>可在 AI 回复顶部的 📋 系统提示词卡片中查看实际注入的完整上下文</li><li>删除工作空间中的文件即可恢复为内置默认版本</li></ul></div>
|
||||
<div class="help-section"><h4>🎯 技能自动生成</h4><ul><li>AI 完成包含 <strong>2+ 工具调用</strong>的复杂任务后,自动从执行轨迹中提取可复用技能</li><li><strong>语义匹配</strong>:配置嵌入模型后,根据用户消息的语义自动匹配最相关技能</li><li><strong>参数自优化</strong>:同一技能链多次执行后,自动合并高频参数提示,越用越精准</li><li><strong>技能衰减</strong>:30 天未使用的技能自动降权,90 天几乎忽略</li><li><strong>链合并</strong>:相同前缀(前 2 步相同)的多个技能自动合并为抽象技能</li><li>AI 可通过 <code>skill_list</code>(渐进式摘要)和 <code>skill_view</code>(完整工具链)查看技能</li><li>技能存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
||||
<div class="help-section"><h4>📊 Token 实时监控</h4><ul><li>点击顶部 📊 按钮打开 Token 监控仪表盘</li><li><strong>全局统计</strong> — 跨会话累计 Token 消耗,柱状图按会话展示趋势</li><li><strong>当前会话</strong> — 实时显示本轮对话的 Token 消耗,按轮次展示明细</li><li>每 2 秒自动刷新数据,支持输入/输出分色显示</li></ul></div>
|
||||
<div class="help-section"><h4>🖥️ 布局说明</h4><ul><li><strong>左侧面板</strong> — 执行日志,实时显示应用运行日志(连接、模型加载、工具调用等)</li><li><strong>中间区域</strong> — 聊天消息,顶部 Header + 模型栏,底部输入框</li><li><strong>右侧面板</strong> — 工作空间(常驻显示),包含 3 个页签:<ul><li><strong>💻 命令行 Tab</strong> — 终端界面,实时流式输出,支持长时间运行(无超时),单一终端进程</li><li><strong>🔧 工具 Tab</strong> — 展示本轮对话的工具调用卡片,含状态统计(总数 ✅ 成功 ❌ 失败),AI 执行命令时自动切到此页签</li><li><strong>📁 文件 Tab</strong> — 浏览工作空间目录,点击文件预览内容(带行号),支持上级目录导航</li></ul></li><li>工作空间目录可在设置中修改</li></ul></div>
|
||||
<div class="help-section"><h4>🕐 历史记录</h4><ul><li>所有会话自动保存到本地 SQLite</li><li>点击顶部 🕐 按钮查看、搜索、恢复历史会话</li><li>支持导出 JSON / <code>.metona</code> 加密备份</li></ul></div>
|
||||
@@ -454,7 +453,7 @@
|
||||
<div class="modal-overlay" id="toolsModal" style="display:none;">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>🔧 工具面板(44 个)</h3>
|
||||
<h3>🔧 工具面板(42 个)</h3>
|
||||
<button class="icon-btn" id="btnCloseTools">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
@@ -580,14 +579,6 @@
|
||||
<div class="tool-card-header"><span class="tool-card-icon">🤖</span><span class="tool-card-name">spawn_task</span><span class="tool-card-badge auto">自动</span></div>
|
||||
<div class="tool-card-desc">生成子代理独立执行任务,完成后回传结果</div>
|
||||
</div>
|
||||
<div class="tool-card">
|
||||
<div class="tool-card-header"><span class="tool-card-icon">📚</span><span class="tool-card-name">skill_list</span><span class="tool-card-badge auto">自动</span></div>
|
||||
<div class="tool-card-desc">列出所有自动生成的技能(名称、描述、成功率)</div>
|
||||
</div>
|
||||
<div class="tool-card">
|
||||
<div class="tool-card-header"><span class="tool-card-icon">📖</span><span class="tool-card-name">skill_view</span><span class="tool-card-badge auto">自动</span></div>
|
||||
<div class="tool-card-desc">查看技能详情(完整工具链、参数提示、使用统计)</div>
|
||||
</div>
|
||||
<div class="tool-card">
|
||||
<div class="tool-card-header"><span class="tool-card-icon">🌐</span><span class="tool-card-name">browser_open</span><span class="tool-card-badge auto">自动</span></div>
|
||||
<div class="tool-card-desc">打开URL,支持等待特定CSS元素出现(SPA页面)</div>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
- **Git 操作**:完整的版本控制——查看状态、日志、差异,暂存、提交、推送、拉取,分支管理、合并、标签、远程仓库操作
|
||||
- **记忆管理**:跨会话记住用户的重要信息(事实、偏好、规则),搜索历史记忆,添加/修改/删除记忆条目
|
||||
- **会话管理**:列出和读取历史会话记录,跨会话引用之前的对话内容
|
||||
- **技能系统**:查看从过往成功任务中自动提取的可复用技能,按需加载完整操作链
|
||||
- **子代理委派**:将独立子任务委派给子代理并行执行,完成后汇总结果
|
||||
- **系统工具**:获取精确时间、数学计算、随机生成、UUID 生成、JSON 格式化、哈希计算
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
needsConfirmation
|
||||
} from './tool-registry.js';
|
||||
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
|
||||
import { extractSkillsFromToolRecords, matchSkills, buildSkillContext } from './skill-manager.js';
|
||||
import { showToast } from '../components/toast.js';
|
||||
import { logInfo, logWarn, logSuccess, logError, logToolStart, logToolResult, logAgentLoop, logModelResponse, logStreamProgress, resetStreamProgress } from './log-service.js';
|
||||
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
|
||||
@@ -80,7 +79,7 @@ const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch']); // web_fetch 可能依赖 w
|
||||
const ALWAYS_PARALLEL = new Set([
|
||||
'read_file', 'list_directory', 'search_files', 'get_file_info', 'tree',
|
||||
'web_search', 'browser_screenshot', 'browser_extract', 'browser_evaluate',
|
||||
'memory_search', 'session_list', 'session_read', 'skill_list', 'skill_view',
|
||||
'memory_search', 'session_list', 'session_read',
|
||||
'diff_files', 'git',
|
||||
'datetime', 'calculator',
|
||||
'random', 'uuid', 'json_format', 'hash',
|
||||
@@ -93,7 +92,6 @@ const VALID_TOOL_NAMES = new Set([
|
||||
'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files',
|
||||
'replace_in_files', 'read_multiple_files', 'git', 'compress',
|
||||
'memory_search', 'memory_add', 'memory_replace', 'memory_remove', 'session_list', 'session_read',
|
||||
'skill_list', 'skill_view',
|
||||
'datetime', 'calculator',
|
||||
'random', 'uuid', 'json_format', 'hash'
|
||||
]);
|
||||
@@ -517,17 +515,6 @@ Shell: ${osInfo.shell}
|
||||
- 如果是 Linux/macOS,使用 Bash 命令(如 ls、cat、grep,路径用 /)
|
||||
- 严禁在 Windows 上执行 Linux 命令,严禁在 Linux 上执行 Windows 命令。`);
|
||||
|
||||
// v4.2 注入匹配的技能上下文
|
||||
if (useTools && userContent) {
|
||||
const matchedSkills = await matchSkills(userContent, 3);
|
||||
if (matchedSkills.length > 0) {
|
||||
const skillContext = buildSkillContext(matchedSkills);
|
||||
if (skillContext) {
|
||||
systemPromptParts.push(skillContext);
|
||||
logInfo(`技能匹配: ${matchedSkills.length} 个技能已注入`, matchedSkills.map(s => s.name).join(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组合 system prompt
|
||||
|
||||
@@ -915,12 +902,6 @@ Shell: ${osInfo.shell}
|
||||
} catch { /* 不阻塞 */ }
|
||||
}
|
||||
|
||||
// v4.2 自动提取技能
|
||||
if (allToolRecords.length >= 2) {
|
||||
try {
|
||||
await extractSkillsFromToolRecords(allToolRecords, userContent, (currentSession as ChatSession)?.title || '');
|
||||
} catch { /* 不阻塞 */ }
|
||||
}
|
||||
|
||||
callbacks.onDone(content || '(模型未返回内容)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||
return;
|
||||
|
||||
@@ -220,7 +220,6 @@ export function logConnection(status: string, detail?: string): void { addLog(st
|
||||
export function logModel(name: string, action: string): void { addLog('info', `模型: ${action}`, name); }
|
||||
export function logSession(action: string, detail?: string): void { addLog('info', `会话: ${action}`, detail); }
|
||||
export function logMemory(action: string, detail?: string): void { addLog('debug', `记忆: ${action}`, detail); }
|
||||
export function logSkill(action: string, detail?: string): void { addLog('debug', `技能: ${action}`, detail); }
|
||||
export function logRAG(action: string, detail?: string): void { addLog('info', `向量: ${action}`, detail); }
|
||||
export function logSetting(key: string, value: string): void { addLog('debug', `设置: ${key}`, value); }
|
||||
export function logInit(msg: string): void { addLog('info', `初始化: ${msg}`); }
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
/** Level 0 简短摘要(~100 tokens),用于索引展示 */
|
||||
summary: 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.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 = await bridge.db.getAllSkills() as Skill[];
|
||||
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) {
|
||||
// v1.1: 重复链 → 更新参数提示(合并新知)+ 增加计数
|
||||
try {
|
||||
const existingChain: ToolChainStep[] = JSON.parse(duplicate.tool_chain);
|
||||
let updated = false;
|
||||
for (let i = 0; i < Math.min(chain.length, existingChain.length); i++) {
|
||||
const newArgs = chain[i].args_hint.split(',').map(a => a.trim()).filter(Boolean);
|
||||
const oldArgs = existingChain[i].args_hint.split(',').map(a => a.trim()).filter(Boolean);
|
||||
const merged = [...new Set([...oldArgs, ...newArgs])];
|
||||
const mergedHint = merged.join(', ');
|
||||
if (mergedHint !== existingChain[i].args_hint) {
|
||||
existingChain[i].args_hint = mergedHint;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
duplicate.tool_chain = JSON.stringify(existingChain);
|
||||
duplicate.success_count++;
|
||||
duplicate.updated_at = Date.now();
|
||||
await bridge.db.saveSkill(duplicate);
|
||||
logInfo(`技能参数优化: ${duplicate.name}`, `合并了新的参数提示`);
|
||||
return 1;
|
||||
}
|
||||
} catch { /* 更新失败不阻塞 */ }
|
||||
logInfo(`技能已存在,跳过提取: ${duplicate.name}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 从用户消息中提取触发关键词
|
||||
const keywords = extractKeywords(userMessage);
|
||||
|
||||
// 生成技能名称和描述
|
||||
const name = generateSkillName(chain, userMessage);
|
||||
const description = generateSkillDescription(chain, sessionTitle);
|
||||
|
||||
const summary = generateSkillSummary(chain, userMessage);
|
||||
|
||||
const skill: Skill = {
|
||||
id: `skill_${generateId()}`,
|
||||
name,
|
||||
description,
|
||||
summary,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配用户消息与已有技能。
|
||||
* v1.1: 关键词 + 语义匹配融合,未使用技能衰减。
|
||||
*/
|
||||
export async function matchSkills(userMessage: string, limit = 3): Promise<Skill[]> {
|
||||
const bridge = window.metonaDesktop;
|
||||
if (!bridge?.db?.getAllSkills) return [];
|
||||
|
||||
try {
|
||||
const allSkills = await bridge.db.getAllSkills() as any as Skill[];
|
||||
if (allSkills.length === 0) return [];
|
||||
|
||||
const messageLower = userMessage.toLowerCase();
|
||||
const messageWords = new Set(messageLower.split(/[\s,,。!?、;:""''()\[\]【】]+/).filter(w => w.length > 1));
|
||||
|
||||
// ── 语义匹配(如果 embedding 模型可用)──
|
||||
let userEmbedding: number[] | null = null;
|
||||
try {
|
||||
const memEnabled = state.get<boolean>('memoryEnabled', false);
|
||||
const embedModel = state.get<string>('embeddingModel', '');
|
||||
if (memEnabled && embedModel) {
|
||||
const api = state.get<any>(KEYS.API);
|
||||
if (api?.embed) {
|
||||
const result = await api.embed(embedModel, userMessage);
|
||||
userEmbedding = result.embedding || result.embeddings?.[0] || null;
|
||||
}
|
||||
}
|
||||
} catch { /* 语义匹配失败回退关键词 */ }
|
||||
|
||||
const now = Date.now();
|
||||
const THIRTY_DAYS = 30 * 24 * 3600 * 1000;
|
||||
|
||||
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.3;
|
||||
}
|
||||
}
|
||||
|
||||
// 名称和描述匹配
|
||||
const nameWords = skill.name.toLowerCase().split(/[\s→\-]+/);
|
||||
for (const w of nameWords) {
|
||||
if (w.length > 1 && messageWords.has(w)) score += 0.15;
|
||||
}
|
||||
|
||||
// 语义匹配:余弦相似度加权(主导分数)
|
||||
if (userEmbedding && skill.summary) {
|
||||
const skillEmbed = getCachedSkillEmbedding(skill.id);
|
||||
if (skillEmbed) {
|
||||
const sim = cosineSimilarity(userEmbedding, skillEmbed);
|
||||
score += sim * 0.4; // 语义匹配主导
|
||||
}
|
||||
}
|
||||
|
||||
// 成功率加权
|
||||
const total = skill.success_count + skill.fail_count;
|
||||
if (total > 0) score *= (0.5 + 0.5 * (skill.success_count / total));
|
||||
|
||||
// ── v1.1 未使用技能衰减 ──
|
||||
if (skill.last_used_at) {
|
||||
const daysSinceUse = (now - skill.last_used_at) / (24 * 3600 * 1000);
|
||||
if (daysSinceUse > 30) score *= 0.5; // 30天未用降权50%
|
||||
if (daysSinceUse > 90) score *= 0.3; // 90天未用几乎忽略
|
||||
}
|
||||
|
||||
return { skill, score };
|
||||
});
|
||||
|
||||
// ── v1.1 技能链合并:相同前缀的链抽象为一个技能 ──
|
||||
const merged = mergeSimilarSkills(scored);
|
||||
|
||||
return merged
|
||||
.filter(s => s.score >= MATCH_THRESHOLD)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit)
|
||||
.map(s => s.skill);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── 向量计算工具 ──
|
||||
|
||||
/** 技能 embedding 缓存(LRU 简单实现) */
|
||||
const _skillEmbedCache = new Map<string, { embedding: number[]; time: number }>();
|
||||
|
||||
function getCachedSkillEmbedding(skillId: string): number[] | null {
|
||||
const cached = _skillEmbedCache.get(skillId);
|
||||
if (cached && Date.now() - cached.time < 300_000) return cached.embedding; // 5分钟有效
|
||||
return null;
|
||||
}
|
||||
|
||||
export function cacheSkillEmbedding(skillId: string, embedding: number[]): void {
|
||||
_skillEmbedCache.set(skillId, { embedding, time: Date.now() });
|
||||
// 限制缓存大小
|
||||
if (_skillEmbedCache.size > 200) {
|
||||
const oldest = [..._skillEmbedCache.entries()].sort((a, b) => a[1].time - b[1].time)[0];
|
||||
if (oldest) _skillEmbedCache.delete(oldest[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function cosineSimilarity(a: number[], b: number[]): number {
|
||||
if (!a || !b || a.length !== b.length) return 0;
|
||||
let dot = 0, normA = 0, normB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dot += a[i] * b[i];
|
||||
normA += a[i] * a[i];
|
||||
normB += b[i] * b[i];
|
||||
}
|
||||
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
||||
return denom === 0 ? 0 : dot / denom;
|
||||
}
|
||||
|
||||
// ── 技能链合并 ──
|
||||
|
||||
interface ScoredSkill { skill: Skill; score: number; }
|
||||
|
||||
/**
|
||||
* v1.1 技能链合并:两个以上技能有相同前缀(前 2 步相同),
|
||||
* 合并为一个抽象技能,分数取最高。
|
||||
*/
|
||||
function mergeSimilarSkills(scored: ScoredSkill[]): ScoredSkill[] {
|
||||
if (scored.length < 2) return scored;
|
||||
|
||||
const result: ScoredSkill[] = [];
|
||||
const used = new Set<number>();
|
||||
|
||||
for (let i = 0; i < scored.length; i++) {
|
||||
if (used.has(i)) continue;
|
||||
const group: ScoredSkill[] = [scored[i]];
|
||||
|
||||
// 找同前缀的
|
||||
for (let j = i + 1; j < scored.length; j++) {
|
||||
if (used.has(j)) continue;
|
||||
if (getChainPrefix(scored[i].skill) === getChainPrefix(scored[j].skill)) {
|
||||
group.push(scored[j]);
|
||||
used.add(j);
|
||||
}
|
||||
}
|
||||
|
||||
if (group.length >= 2) {
|
||||
// 合并为一个抽象技能
|
||||
const best = group.reduce((a, b) => a.score > b.score ? a : b);
|
||||
const chainNames = group.map(g => {
|
||||
try { return (JSON.parse(g.skill.tool_chain) as ToolChainStep[]).map(s => s.name).join('→'); }
|
||||
catch { return g.skill.name; }
|
||||
});
|
||||
const mergedSkill: Skill = {
|
||||
...best.skill,
|
||||
name: `${getChainPrefix(best.skill)} → ...`,
|
||||
description: `合并了 ${group.length} 个相似技能: ${chainNames.join(' | ')}`,
|
||||
summary: best.skill.summary,
|
||||
};
|
||||
result.push({ skill: mergedSkill, score: best.score * 1.1 }); // 合并后略加分
|
||||
used.add(i);
|
||||
} else {
|
||||
result.push(scored[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 获取技能链的前 2 步前缀(用于相似度判断) */
|
||||
function getChainPrefix(skill: Skill): string {
|
||||
try {
|
||||
const chain: ToolChainStep[] = JSON.parse(skill.tool_chain);
|
||||
return chain.slice(0, 2).map(s => s.name).join('→');
|
||||
} catch {
|
||||
return skill.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从匹配的技能构建 system prompt 上下文(Level 0:仅名称 + 描述索引)
|
||||
* 完整工具链通过 skill_view 工具按需加载(Level 1)
|
||||
*/
|
||||
export function buildSkillContext(skills: Skill[]): string {
|
||||
if (skills.length === 0) return '';
|
||||
|
||||
const parts = skills.map(skill => {
|
||||
const total = skill.success_count + skill.fail_count;
|
||||
const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100;
|
||||
return `• ${skill.name} — ${skill.description}(成功率 ${rate}%)`;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (parts.length === 0) return '';
|
||||
|
||||
return `【可用技能 — 以下技能曾成功完成过类似任务】\n${parts.join('\n')}\n\n提示:使用 skill_view 工具查看某个技能的完整工具链和参数提示。`;
|
||||
}
|
||||
|
||||
// ── 辅助函数 ──
|
||||
|
||||
/** 生成工具步骤描述 */
|
||||
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 `添加记忆`;
|
||||
case 'spawn_task': return `子代理委派`;
|
||||
case 'browser_open': return `打开网页 ${args.url || ''}`;
|
||||
case 'browser_screenshot': return `浏览器截图`;
|
||||
case 'browser_evaluate': return `执行浏览器 JS`;
|
||||
case 'browser_extract': return `提取页面内容`;
|
||||
case 'browser_click': return `点击元素 ${args.selector || ''}`;
|
||||
case 'browser_type': return `输入文本 ${args.selector || ''}`;
|
||||
case 'browser_scroll': return `滚动页面`;
|
||||
case 'browser_close': 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}`;
|
||||
}
|
||||
|
||||
/** 生成技能简短摘要(Level 0,~100 tokens) */
|
||||
function generateSkillSummary(chain: ToolChainStep[], userMessage: string): string {
|
||||
const toolNames = chain.map(s => s.name).join(' → ');
|
||||
const msgPreview = userMessage.slice(0, 30).replace(/[\r\n]/g, ' ');
|
||||
return `工具链: ${toolNames}。场景: ${msgPreview}`;
|
||||
}
|
||||
|
||||
// ── 渐进式技能加载 ──
|
||||
|
||||
/** 列出所有技能(渐进式 Level 0:只返回名称、描述摘要、成功率) */
|
||||
export async function listSkills(): Promise<Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
summary: string;
|
||||
success_rate: string;
|
||||
chain_preview: string;
|
||||
}>> {
|
||||
const bridge = window.metonaDesktop;
|
||||
if (!bridge?.db?.getAllSkills) return [];
|
||||
const allSkills: Skill[] = await bridge.db.getAllSkills() as any;
|
||||
return (allSkills as any[]).map((skill: any) => {
|
||||
const total = skill.success_count + skill.fail_count;
|
||||
const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100;
|
||||
let chainPreview = '';
|
||||
try {
|
||||
const chain: ToolChainStep[] = JSON.parse(skill.tool_chain);
|
||||
chainPreview = chain.map(s => s.name).join(' → ');
|
||||
} catch { /* */ }
|
||||
return {
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
summary: skill.summary || skill.description,
|
||||
success_rate: `${rate}%`,
|
||||
chain_preview: chainPreview
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看技能详情(渐进式 Level 1:返回完整工具链和参数提示) */
|
||||
export async function viewSkill(skillName: string): Promise<{
|
||||
success: boolean;
|
||||
skill?: Skill & { chain: ToolChainStep[]; success_rate: string };
|
||||
error?: string;
|
||||
}> {
|
||||
const bridge = window.metonaDesktop;
|
||||
if (!bridge?.db?.getAllSkills) return { success: false, error: '数据库不可用' };
|
||||
const allSkills: Skill[] = await bridge.db.getAllSkills() as any;
|
||||
const skill = allSkills.find(s =>
|
||||
s.name.toLowerCase() === skillName.toLowerCase() ||
|
||||
s.name.toLowerCase().includes(skillName.toLowerCase())
|
||||
);
|
||||
if (!skill) return { success: false, error: `未找到技能: ${skillName}` };
|
||||
let chain: ToolChainStep[] = [];
|
||||
try { chain = JSON.parse(skill.tool_chain); } catch { /* */ }
|
||||
const total = skill.success_count + skill.fail_count;
|
||||
const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100;
|
||||
return {
|
||||
success: true,
|
||||
skill: { ...skill, chain, success_rate: `${rate}%` }
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,6 @@ const SUB_AGENT_TOOL_WHITELIST = new Set([
|
||||
'browser_extract', 'browser_screenshot',
|
||||
'memory_search',
|
||||
'session_list', 'session_read',
|
||||
'skill_list', 'skill_view',
|
||||
]);
|
||||
|
||||
/** 从总工具列表中筛选子代理可用的 */
|
||||
|
||||
@@ -446,31 +446,6 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'skill_list',
|
||||
description: 'List all available auto-generated skills. Shows name, description, and success rate. Use skill_view to see full details of a specific skill.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'skill_view',
|
||||
description: 'View full details of a specific skill including its tool chain, parameter hints, and usage statistics.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: { type: 'string', description: 'The skill name (or partial name) to view.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// ══════════════════════════════════════════════
|
||||
// v4.3 新增工具:子代理委派
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -478,7 +453,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'spawn_task',
|
||||
description: 'Spawn a sub-agent to independently execute a task using read-only tools (file reading, web search, browser viewing, memory/session/skill queries). Use this to parallelize independent research or analysis sub-tasks. The model is configured in Settings and cannot be overridden per call.',
|
||||
description: 'Spawn a sub-agent to independently execute a task using read-only tools (file reading, web search, browser viewing, memory/session queries). Use this to parallelize independent research or analysis sub-tasks. The model is configured in Settings and cannot be overridden per call.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['task'],
|
||||
@@ -740,7 +715,7 @@ let enabledTools: Set<string> = new Set([
|
||||
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
|
||||
'read_multiple_files', 'git', 'compress',
|
||||
'memory_search', 'memory_add', 'memory_replace', 'memory_remove',
|
||||
'session_list', 'session_read', 'skill_list', 'skill_view', 'spawn_task',
|
||||
'session_list', 'session_read', 'spawn_task',
|
||||
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
|
||||
'browser_click', 'browser_type', 'browser_scroll', 'browser_close',
|
||||
'datetime', 'calculator',
|
||||
@@ -903,19 +878,6 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
||||
logToolResult('spawn_task', result.success, result.success ? `完成, ${(result as any).loops} 轮` : result.error);
|
||||
return result;
|
||||
}
|
||||
if (toolName === 'skill_list') {
|
||||
const { listSkills } = await import('./skill-manager.js');
|
||||
const skills = await listSkills();
|
||||
logToolResult('skill_list', true, `${skills.length} 个技能`);
|
||||
return { success: true, skills, total: skills.length };
|
||||
}
|
||||
if (toolName === 'skill_view') {
|
||||
const { viewSkill } = await import('./skill-manager.js');
|
||||
const name = String(args.name || '');
|
||||
const result = await viewSkill(name);
|
||||
logToolResult('skill_view', result.success, result.success ? result.skill?.name : result.error);
|
||||
return result;
|
||||
}
|
||||
|
||||
// run_command 走 workspace IPC
|
||||
if (toolName === 'run_command') {
|
||||
@@ -955,7 +917,7 @@ export function getToolIcon(name: string): string {
|
||||
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||
memory_search: '🧠', memory_add: '💾', memory_replace: '🔄', memory_remove: '🗑️',
|
||||
session_list: '📋', session_read: '📖', skill_list: '📚', skill_view: '📖', spawn_task: '🤖',
|
||||
session_list: '📋', session_read: '📖', spawn_task: '🤖',
|
||||
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
||||
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
||||
};
|
||||
@@ -972,7 +934,7 @@ export function formatToolName(name: string): string {
|
||||
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||
memory_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
|
||||
session_list: '会话列表', session_read: '读取会话', skill_list: '技能列表', skill_view: '技能详情', spawn_task: '子代理委派',
|
||||
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
|
||||
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
||||
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
||||
};
|
||||
|
||||
Vendored
+1
-7
@@ -462,13 +462,7 @@ export interface DBAPI {
|
||||
getTraces: (sessionId: string) => Promise<TraceEntry[]>;
|
||||
exportSessions: () => Promise<unknown>;
|
||||
importSessions: (data: unknown) => Promise<{ imported: number; skipped: number }>;
|
||||
// v4.2 Skills
|
||||
saveSkill: (skill: unknown) => Promise<{ success: boolean; id?: string; error?: string }>;
|
||||
getAllSkills: () => Promise<unknown[]>;
|
||||
deleteSkill: (id: string) => Promise<{ success: boolean; error?: string }>;
|
||||
clearAllSkills: () => Promise<{ success: boolean; error?: string }>;
|
||||
searchSkills: (query: string, limit?: number) => Promise<unknown[]>;
|
||||
incrementSkillUsage: (id: string, success: boolean, durationMs: number) => Promise<{ success: boolean; error?: string }>;
|
||||
|
||||
getAllTokenStats(): Promise<{
|
||||
sessions: Array<{
|
||||
session_id: string;
|
||||
|
||||
Reference in New Issue
Block a user