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:
thzxx
2026-06-22 18:13:01 +08:00
parent 6099c2ad21
commit 20cbfbd169
19 changed files with 41 additions and 738 deletions
+11 -21
View File
@@ -14,7 +14,7 @@
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-v0.11.10-E8734A?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/version-v0.11.11-E8734A?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
@@ -35,10 +35,9 @@
| | 功能 | 说明 |
|:---:|:---|:---|
| 🤖 | **ReAct Agent Loop** | 始终开启的唯一对话模式。Thought → Action → Observation → Reflection 完整循环,最大 85 轮(可配置),自动重试 2 次,工具去重,并行/链式执行 |
| 🔧 | **44 个内置工具** | 文件系统(16个) · 命令执行 · 联网搜索 · 浏览器控制(9个) · Git · 记忆 · 会话 · 子代理 · 系统工具 |
| 🔧 | **42 个内置工具** | 文件系统(16个) · 命令执行 · 联网搜索 · 浏览器控制(9个) · Git · 记忆 · 会话 · 子代理 · 系统工具 |
| 🧠 | **智能记忆系统** | 三类记忆(fact / preference / rule),FTS5 全文搜索 + 向量语义搜索,写入前安全扫描,容量 500 条,90 天衰减 |
| 📋 | **自定义文件** | SOUL.md(人格,不可压缩)+ AGENT.md(行为准则)+ USER.md(用户画像),工作空间优先,内置默认 fallback |
| 🎯 | **技能自动生成 v1.1** | 工具调用链自动提取 → 语义匹配 → 参数自优化 → 未使用衰减 → 技能链合并,越用越精准 |
| 🌐 | **MCP 协议扩展** | JSON-RPC 2.0 over stdio,动态工具发现,Shadowing 防护 |
| 🔍 | **联网搜索(双模式)** | SearXNG 元搜索引擎 JSON API70+引擎聚合)/ 四引擎 HTML 解析(Bing+百度+搜狗+360),双模式可切换;web_fetch 支持反爬+UA切换+浏览器回退 |
| 🌏 | **浏览器控制** | 打开网页 · 截图 · 执行 JS · 提取内容 · 点击 · 输入 · 滚动 · 关闭 |
@@ -138,12 +137,10 @@
</details>
<details>
<summary><strong>📊 技能 / 会话 / 子代理(5 个)</strong></summary>
<summary><strong>📊 会话 / 子代理(3 个)</strong></summary>
| 工具 | 功能 |
|------|------|
| `skill_list` | 列出已生成的技能 |
| `skill_view` | 查看技能详情 |
| `session_list` | 列出历史会话 |
| `session_read` | 读取会话内容 |
| `spawn_task` | 委派子代理(独立上下文 + 超时保护) |
@@ -173,11 +170,9 @@
记忆检索 (FTS5 + 向量语义搜索) → 上下文注入
技能匹配 (语义匹配 + 参数自优化 + 衰减 + 链合并)
Ollama API (流式响应,num_ctx 自动检测)
Tool Registry (44 内置 + MCP 动态)
Tool Registry (42 内置 + MCP 动态)
观察结果 → 反思 → 循环 / 最终回答
```
@@ -204,7 +199,7 @@ main.ts (入口)
### 🗄️ 数据库
SQLite (sql.js WASM)7 张表,WAL 模式 + FTS5 全文搜索:
SQLite (sql.js WASM)6 张表,WAL 模式 + FTS5 全文搜索:
| 表 | 用途 |
|---|---|
@@ -214,7 +209,6 @@ SQLite (sql.js WASM)7 张表,WAL 模式 + FTS5 全文搜索:
| `memories` | Agent 记忆(FTS5 + 向量嵌入) |
| `settings` | 设置(JSON 序列化) |
| `traces` | ReAct 执行轨迹 |
| `skills` | 自动生成的技能 |
## 🔒 安全机制
@@ -245,7 +239,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
产出:`release/Metona Ollama Setup v0.11.10.exe`
产出:`release/Metona Ollama Setup v0.11.11.exe`
## 🛠️ 常用命令
@@ -276,10 +270,9 @@ npm run dist # 构建 Windows 安装包
| | Feature | Description |
|:---:|:---|:---|
| 🤖 | **ReAct Agent Loop** | Always-on, only chat mode. Full Thought → Action → Observation → Reflection cycle, up to 85 iterations (configurable), auto-retry 2 times, tool dedup, parallel/chain execution |
| 🔧 | **44 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Skills · Sessions · Sub-agent · System |
| 🔧 | **42 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Sessions · Sub-agent · System |
| 🧠 | **Smart Memory System** | Three types (fact / preference / rule), FTS5 + vector semantic search, pre-write security scan, 500 capacity, 90-day decay |
| 📋 | **Custom Files** | SOUL.md (persona, never compressed) + AGENT.md (behavior rules) + USER.md (user profile), workspace-first with built-in defaults |
| 🎯 | **Auto Skill Generation v1.1** | Tool chain extraction → semantic matching → parameter self-optimization → decay → chain merging |
| 🌐 | **MCP Protocol Extension** | JSON-RPC 2.0 over stdio, dynamic tool discovery, Shadowing protection |
| 🔍 | **Web Search (dual-mode)** | SearXNG meta-search JSON API (70+ engines) / quad-engine HTML parsing (Bing+Baidu+Sogou+360), switchable; web_fetch with auto-retry+mobile UA+SPA browser fallback |
| 🌏 | **Browser Control** | Open pages · Screenshot · JS execution · Content extraction · Click · Type · Scroll · Close |
@@ -378,12 +371,10 @@ npm run dist # 构建 Windows 安装包
</details>
<details>
<summary><strong>📊 Skills / Sessions / Sub-agent (5)</strong></summary>
<summary><strong>📊 Sessions / Sub-agent (3)</strong></summary>
| Tool | Function |
|------|------|
| `skill_list` | List generated skills |
| `skill_view` | View skill details |
| `session_list` | List session history |
| `session_read` | Read session content |
| `spawn_task` | Delegate sub-agent (isolated context + timeout) |
@@ -413,7 +404,7 @@ User message → workspace SOUL.md (never compressed) → AGENT.md → USER.md
Ollama API (Streaming Response)
Tool Registry (44 Built-in + MCP Dynamic)
Tool Registry (42 Built-in + MCP Dynamic)
Observation → Reflection → Loop / Final Answer
```
@@ -440,7 +431,7 @@ main.ts (Entry)
### 🗄️ Database
SQLite (sql.js WASM), 7 tables, WAL mode + FTS5 full-text search:
SQLite (sql.js WASM), 6 tables, WAL mode + FTS5 full-text search:
| Table | Purpose |
|---|---|
@@ -450,7 +441,6 @@ SQLite (sql.js WASM), 7 tables, WAL mode + FTS5 full-text search:
| `memories` | Agent memories (FTS5 + vector embedding) |
| `settings` | Settings (JSON serialization) |
| `traces` | ReAct execution traces |
| `skills` | Auto-generated skills |
## 🔒 Security
@@ -481,7 +471,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
Output: `release/Metona Ollama Setup v0.11.10.exe`
Output: `release/Metona Ollama Setup v0.11.11.exe`
## 🛠️ Common Commands
+3 -3
View File
@@ -91,7 +91,7 @@ npm run dist # NSIS 安装包
| 文件 | 说明 |
|------|------|
| `release/Metona Ollama Setup v0.11.10.exe` | NSIS 安装包(可选目录、创建快捷方式) |
| `release/Metona Ollama Setup v0.11.11.exe` | NSIS 安装包(可选目录、创建快捷方式) |
> 仅提供 NSIS 安装包。
@@ -103,7 +103,7 @@ npm run dist # NSIS 安装包
TOKEN="your_access_token"
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases?access_token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"tag_name":"v0.11.10","name":"v0.11.10","body":"Release notes","target_commitish":"master"}'
-d '{"tag_name":"v0.11.11","name":"v0.11.11","body":"Release notes","target_commitish":"master"}'
```
### 上传附件
@@ -112,7 +112,7 @@ curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/release
TOKEN="your_access_token"
RELEASE_ID="<release_id>"
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases/$RELEASE_ID/attach_files?access_token=$TOKEN" \
-H "Content-Type: multipart/form-data" -F "file=@release/Metona Ollama Setup v0.11.10.exe"
-H "Content-Type: multipart/form-data" -F "file=@release/Metona Ollama Setup v0.11.11.exe"
```
## 故障排查
+6 -8
View File
@@ -1,6 +1,6 @@
# Metona Ollama Desktop — 开发规范
> 版本: v0.11.10 | 更新: 2026-06-05 | 维护: 项目团队
> 版本: v0.11.11 | 更新: 2026-06-05 | 维护: 项目团队
---
@@ -43,7 +43,7 @@ src/
│ ├── tray.ts # 系统托盘
│ ├── utils.ts # 工具函数
│ └── db/
│ └── sqlite.ts # SQLite 数据库层(7 张表 + FTS5
│ └── sqlite.ts # SQLite 数据库层(6 张表 + FTS5
├── renderer/ # 渲染进程
│ ├── main.ts # 入口、全局初始化
│ ├── types.d.ts # 完整类型定义
@@ -53,12 +53,11 @@ src/
│ ├── components/ # 14 个 UI 组件
│ ├── services/
│ │ ├── agent-engine.ts # ReAct Agent Loop 引擎
│ │ ├── tool-registry.ts # 工具注册与调度(39 个内置工具 + MCP 动态)
│ │ ├── tool-registry.ts # 工具注册与调度(42 个内置工具 + MCP 动态)
│ │ ├── memory-manager.ts # 记忆管理核心
│ │ ├── vector-memory.ts # 记忆向量索引(IVF)
│ │ ├── vector-store.ts # 向量存储 + IVF 索引
│ │ ├── context-manager.ts # 上下文窗口管理
│ │ ├── skill-manager.ts # 技能自动生成(Level 0)
│ │ ├── sub-agent.ts # 子代理委派
│ │ ├── mcp-client.ts # MCP 渲染端客户端
│ │ ├── log-service.ts # 结构化日志
@@ -155,11 +154,11 @@ import {
### 5.1 五大子系统
```
① Agent 系统 → agent-engine.ts + tool-registry.ts39 内置工具 + MCP 动态)
① Agent 系统 → agent-engine.ts + tool-registry.ts42 内置工具 + MCP 动态)
② 记忆系统 → memory-manager.ts + vector-memory.ts
③ 向量存储 → vector-store.tsIVF 索引)
④ 工作空间 → workspace.ts (主进程) + workspace-panel.ts (渲染进程)
⑤ 数据层 → db/sqlite.tsSQLite, 7 张表, FTS5
⑤ 数据层 → db/sqlite.tsSQLite, 6 张表, FTS5
```
### 5.2 ReAct Agent Loop
@@ -177,7 +176,7 @@ import {
### 5.3 SQLite 数据库
7 张表,WAL 模式 + NORMAL 同步:
6 张表,WAL 模式 + NORMAL 同步:
| 表 | 用途 | 关键特性 |
|---|---|---|
@@ -187,7 +186,6 @@ import {
| `memories` | Agent 记忆 | FTS5 全文搜索,向量嵌入,容量 500 上限 |
| `settings` | 设置 | JSON 序列化 |
| `traces` | ReAct 执行轨迹 | Agent 可观测性 |
| `skills` | 自动生成的可复用技能 | 成功/失败次数、平均时长 |
### 5.4 联网搜索联动
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "metona-ollama-desktop",
"version": "0.11.10",
"version": "0.11.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metona-ollama-desktop",
"version": "0.11.10",
"version": "0.11.11",
"license": "MIT",
"dependencies": {
"ffmpeg-static": "^5.2.0",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "metona-ollama-desktop",
"version": "0.11.10",
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
"version": "0.11.11",
"description": "Metona Ollama - TypeScript + Electron \u684c\u9762 AI \u804a\u5929\u5ba2\u6237\u7aef",
"main": "dist/main/main.js",
"author": "thzxx",
"license": "MIT",
+3 -10
View File
@@ -4,7 +4,7 @@
## 项目简介
Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,采用 TypeScript + Electron 构建。所有 AI 推理均在本地完成,数据不离开本机。支持 ReAct Agent Loop、44 个内置工具、智能记忆系统、MCP 协议扩展、四引擎联网搜索、浏览器控制等能力。
Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,采用 TypeScript + Electron 构建。所有 AI 推理均在本地完成,数据不离开本机。支持 ReAct Agent Loop、42 个内置工具、智能记忆系统、MCP 协议扩展、四引擎联网搜索、浏览器控制等能力。
---
@@ -18,7 +18,7 @@ Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,
- 链式工具调用:web_search → web_fetch、list_directory → read_file 等模式
- 文本解析兜底:当模型未通过 tool_calls 返回时,从文本中提取 Action/Input
### 🔧 44 个内置工具
### 🔧 42 个内置工具
| 分类 | 数量 | 工具 |
|------|------|------|
@@ -28,7 +28,6 @@ Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,
| 浏览器控制 | 8 | browser_open, browser_screenshot, browser_evaluate, browser_extract, browser_click, browser_type, browser_scroll, browser_close |
| Git | 1 | gitinit/clone/add/commit/push/pull/diff/log/status/branch/checkout/merge/stash/reset/tag/remote |
| 记忆管理 | 4 | memory_search, memory_add, memory_replace, memory_remove |
| 技能系统 | 2 | skill_list, skill_view(渐进式加载) |
| 会话管理 | 2 | session_list, session_read |
| 子代理 | 1 | spawn_task(独立上下文 + 超时保护) |
| 系统工具 | 6 | datetime / calculator / random / uuid / json_format / hash |
@@ -62,11 +61,6 @@ Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,
- Token 消耗趋势图(输入/输出分色显示)
- 2 秒自动刷新
### 📊 技能自动生成系统
- 从成功的工具调用链中自动提取可复用技能
- Level 0 简短摘要(~100 tokens),渐进式加载
- 技能匹配与上下文注入
### 🔒 安全机制
- 文件路径安全检查(checkPathAllowed
- 命令执行安全检查(checkCommandAllowed
@@ -91,7 +85,7 @@ Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,
```
### 数据层
- **SQLite (sql.js WASM)**7 张表(sessions, messages, tool_calls, memories, settings, traces, skills
- **SQLite (sql.js WASM)**6 张表(sessions, messages, tool_calls, memories, settings, traces
- **FTS5 全文搜索**memories 表支持全文检索
- **WAL 模式 + NORMAL 同步**:零原生依赖,无需 electron-rebuild
@@ -116,7 +110,6 @@ Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,
## 最近改进
- **`/compress` 上下文压缩**:支持手动触发 LLM 摘要压缩,长对话不再丢失上下文
- **Skill 渐进式加载**:技能摘要(Level 0)仅 ~100 tokens,按需加载完整工具链
- **Token 全局统计**:仪表盘新增跨会话累计 Token 分析视图
- **图表重写**:Token 消耗趋势图重写 — 网格线 + 正确高度计算 + 柱子底部对齐
- **Agent Loop 稳定性**:修复 abortController 在自动压缩前未定义的严重 bug
-90
View File
@@ -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 {
-27
View File
@@ -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
View File
@@ -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
View File
@@ -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: {
+2 -2
View File
@@ -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
View File
@@ -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 个):git17 个子操作,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 个):git17 个子操作,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>🔌 MCPModel 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>支持认证 KeyHTTP 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>
-1
View File
@@ -13,7 +13,6 @@
- **Git 操作**:完整的版本控制——查看状态、日志、差异,暂存、提交、推送、拉取,分支管理、合并、标签、远程仓库操作
- **记忆管理**:跨会话记住用户的重要信息(事实、偏好、规则),搜索历史记忆,添加/修改/删除记忆条目
- **会话管理**:列出和读取历史会话记录,跨会话引用之前的对话内容
- **技能系统**:查看从过往成功任务中自动提取的可复用技能,按需加载完整操作链
- **子代理委派**:将独立子任务委派给子代理并行执行,完成后汇总结果
- **系统工具**:获取精确时间、数学计算、随机生成、UUID 生成、JSON 格式化、哈希计算
+1 -20
View File
@@ -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;
-1
View File
@@ -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}`); }
-480
View File
@@ -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}%` }
};
}
-1
View File
@@ -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',
]);
/** 从总工具列表中筛选子代理可用的 */
+4 -42
View File
@@ -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: '关闭浏览器'
};
+1 -7
View File
@@ -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;