feat: v5.1.0 - 记忆replace/remove、Skill渐进式加载、安全扫描、预算可配置、工具截断配置
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
全离线运行 · 数据本地存储 · 零外部依赖
|
全离线运行 · 数据本地存储 · 零外部依赖
|
||||||
|
|
||||||
[](https://gitee.com/thzxx/metona-ollama-desktop/releases)
|
[](https://gitee.com/thzxx/metona-ollama-desktop/releases)
|
||||||
[](https://gitee.com/thzxx/metona-ollama-desktop/releases)
|
[](https://gitee.com/thzxx/metona-ollama-desktop/releases)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://www.electronjs.org/)
|
[](https://www.electronjs.org/)
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔧 Tool Calling — 34 个内置工具
|
### 🔧 Tool Calling — 38 个内置工具
|
||||||
|
|
||||||
AI 在对话中**主动调用**本地工具完成任务,所有操作在用户可视化监督下执行。采用 **ReAct Agent Loop**(Thought → Action → Observation → Reflection),最大 15 轮,10 分钟超时。
|
AI 在对话中**主动调用**本地工具完成任务,所有操作在用户可视化监督下执行。采用 **ReAct Agent Loop**(Thought → Action → Observation → Reflection),最大 15 轮,10 分钟超时。
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ src/
|
|||||||
|
|
||||||
| 文件 | 类型 |
|
| 文件 | 类型 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `Metona Ollama Setup 5.0.1.exe` | NSIS 安装包(可选目录、创建快捷方式) |
|
| `Metona Ollama Setup 5.1.0.exe` | NSIS 安装包(可选目录、创建快捷方式) |
|
||||||
|
|
||||||
> ⚠️ 未签名版本,首次运行 Windows 可能弹出安全警告,点击「仍要运行」即可。
|
> ⚠️ 未签名版本,首次运行 Windows 可能弹出安全警告,点击「仍要运行」即可。
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -14,7 +14,7 @@
|
|||||||
# 1. 克隆并安装依赖
|
# 1. 克隆并安装依赖
|
||||||
git clone https://gitee.com/thzxx/metona-ollama-desktop.git
|
git clone https://gitee.com/thzxx/metona-ollama-desktop.git
|
||||||
cd metona-ollama-desktop
|
cd metona-ollama-desktop
|
||||||
git checkout metona-ollama-desktop-v5.0.1
|
git checkout metona-ollama-desktop-v5.1.0
|
||||||
npm config set registry https://registry.npmmirror.com
|
npm config set registry https://registry.npmmirror.com
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
@@ -176,6 +176,14 @@ electron-builder 会自动处理所有依赖下载(Electron、NSIS、winCodeSi
|
|||||||
|
|
||||||
## 构建日志
|
## 构建日志
|
||||||
|
|
||||||
|
### v5.1.0(2026-04-18) — metona-ollama-desktop 仓库
|
||||||
|
|
||||||
|
- ✨ 新增 4 个工具:memory_replace、memory_remove、skill_list、skill_view(总数 38 个)
|
||||||
|
- ✨ 记忆安全扫描(prompt injection / 敏感信息 / 不可见字符检测)
|
||||||
|
- 🔧 Agent Loop 迭代预算可配置化(maxTurns)
|
||||||
|
- 🔧 工具结果截断按工具独立配置
|
||||||
|
- 📝 版本号更新至 5.1.0
|
||||||
|
|
||||||
### v5.0.1(2026-04-18) — metona-ollama-desktop 仓库
|
### v5.0.1(2026-04-18) — metona-ollama-desktop 仓库
|
||||||
|
|
||||||
- 🔧 修复 DuckDuckGo 搜索超时过长(15s → 8s),添加 Google 作为第三搜索引擎兜底
|
- 🔧 修复 DuckDuckGo 搜索超时过长(15s → 8s),添加 Google 作为第三搜索引擎兜底
|
||||||
|
|||||||
+28
-1
@@ -3,7 +3,7 @@
|
|||||||
## 分支结构
|
## 分支结构
|
||||||
|
|
||||||
**桌面版主线**(`master`):
|
**桌面版主线**(`master`):
|
||||||
- `metona-ollama-desktop-v3.2.6` → `metona-ollama-desktop-v3.3.4` → `metona-ollama-desktop-v4.0.0` → `metona-ollama-desktop-v5.0.0` → `metona-ollama-desktop-v5.0.1`(当前发布分支)
|
- `metona-ollama-desktop-v3.2.6` → `metona-ollama-desktop-v3.3.4` → `metona-ollama-desktop-v4.0.0` → `metona-ollama-desktop-v5.0.0` → `metona-ollama-desktop-v5.0.1` → `metona-ollama-desktop-v5.1.0`(当前发布分支)
|
||||||
|
|
||||||
**Web 版历史**已独立为 [metona-ollama-web](https://gitee.com/thzxx/metona-ollama-web) 仓库(v1.1.0 ~ v4.0.3)。
|
**Web 版历史**已独立为 [metona-ollama-web](https://gitee.com/thzxx/metona-ollama-web) 仓库(v1.1.0 ~ v4.0.3)。
|
||||||
|
|
||||||
@@ -11,6 +11,33 @@
|
|||||||
|
|
||||||
## 桌面版(Desktop)
|
## 桌面版(Desktop)
|
||||||
|
|
||||||
|
### Desktop v5.1.0 — Hermes 借鉴 & 记忆/Skill 系统增强
|
||||||
|
|
||||||
|
- ✨ **记忆 replace/remove 操作**(借鉴 Hermes Agent)
|
||||||
|
- 新增 `memory_replace` 工具:子串匹配 old_text,替换为 new_content
|
||||||
|
- 新增 `memory_remove` 工具:子串匹配 old_text,删除对应记忆
|
||||||
|
- 两种目标:memory(Agent 笔记)和 user(用户画像)
|
||||||
|
- 唯一匹配约束:匹配多条时返回错误,要求更精确的子串
|
||||||
|
- ✨ **Skill 渐进式加载**(借鉴 Hermes Agent)
|
||||||
|
- 新增 `skill_list` 工具(Level 0):返回技能名称、描述、成功率、工具链预览
|
||||||
|
- 新增 `skill_view` 工具(Level 1):返回完整工具链、参数提示、使用统计
|
||||||
|
- Agent 先看列表,需要时再加载详情,节省 token
|
||||||
|
- ✨ **记忆安全扫描**
|
||||||
|
- 记忆写入前自动检查 prompt injection 模式(ignore previous / you are now a 等)
|
||||||
|
- 检测敏感信息(SSH 私钥、API Key、信用卡号)
|
||||||
|
- 检测不可见 Unicode 字符
|
||||||
|
- 拦截时返回明确错误信息
|
||||||
|
- 🔧 **迭代预算可配置化**
|
||||||
|
- Agent Loop 最大轮数从硬编码 15 改为 state 读取(`maxTurns`,默认 15)
|
||||||
|
- 用户可通过设置面板调整
|
||||||
|
- 🔧 **工具结果截断按工具配置**
|
||||||
|
- 新增 `TOOL_MAX_RESULT_SIZE` 常量,每个工具独立配置最大返回字符数
|
||||||
|
- web_fetch: 20000, web_search: 3000, read_file: 15000, run_command: 10000 等
|
||||||
|
- 🔧 **更新 AGENT_SYSTEM_PROMPT**
|
||||||
|
- 工具数量 34 → 38
|
||||||
|
- TOOL_USAGE_GUIDE 新增记忆管理和技能查看规则
|
||||||
|
- 📝 **版本号更新至 5.1.0**(7 个文件同步更新)
|
||||||
|
|
||||||
### Desktop v5.0.1 — 搜索引擎优化 & Agent 去重修复
|
### Desktop v5.0.1 — 搜索引擎优化 & Agent 去重修复
|
||||||
|
|
||||||
- 🔧 **修复 DuckDuckGo 搜索超时过长**
|
- 🔧 **修复 DuckDuckGo 搜索超时过长**
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Metona Ollama Desktop — 开发规范
|
# Metona Ollama Desktop — 开发规范
|
||||||
|
|
||||||
> 版本: 5.0.1 | 更新: 2026-04-18 | 维护: 项目团队
|
> 版本: 5.1.0 | 更新: 2026-04-18 | 维护: 项目团队
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "5.0.1",
|
"version": "5.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "5.0.1",
|
"version": "5.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sql.js": "^1.11.0"
|
"sql.js": "^1.11.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "5.0.1",
|
"version": "5.1.0",
|
||||||
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
||||||
"main": "dist/main/main.js",
|
"main": "dist/main/main.js",
|
||||||
"author": "thzxx",
|
"author": "thzxx",
|
||||||
|
|||||||
+20
-4
@@ -27,7 +27,7 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="logo">🦙</span>
|
<span class="logo">🦙</span>
|
||||||
<span class="app-title">Metona Ollama</span>
|
<span class="app-title">Metona Ollama</span>
|
||||||
<span class="app-version">v5.0.1</span>
|
<span class="app-version">v5.1.0</span>
|
||||||
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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"/>
|
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||||
@@ -413,8 +413,8 @@
|
|||||||
<div class="modal-body">
|
<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><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> — 设置中开启,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>多模态</strong> — 上传图片,模型需支持 Vision 能力</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,内容以代码块发送给模型</li><li><strong>上下文长度</strong> — 设置中调整 <code>num_ctx</code>,越大记忆越长,消耗显存越多</li></ul></div>
|
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 设置中开启,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>多模态</strong> — 上传图片,模型需支持 Vision 能力</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,内容以代码块发送给模型</li><li><strong>上下文长度</strong> — 设置中调整 <code>num_ctx</code>,越大记忆越长,消耗显存越多</li></ul></div>
|
||||||
<div class="help-section"><h4>🔧 Tool Calling(AI 自主操作)</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>29 个工具</strong>,分为 6 类:<ul><li><strong>文件操作</strong>(14 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / append_file / edit_file / get_file_info / tree / diff_files / replace_in_files / read_multiple_files</li><li><strong>网络工具</strong>(3 个):web_search(联网搜索)/ web_fetch(网页抓取)/ download_file</li><li><strong>浏览器控制</strong>(8 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_close</li><li><strong>系统工具</strong>(3 个):run_command / git / compress</li><li><strong>记忆/会话</strong>(4 个):memory_search / memory_add / session_list / session_read</li><li><strong>子代理</strong>(1 个):spawn_task</li></ul></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>15 轮</strong>工具调用,总超时 10 分钟,也可随时中断</li><li>独立工具自动并行执行,有依赖关系的工具串行执行</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral)</li></ul></div>
|
<div class="help-section"><h4>🔧 Tool Calling(AI 自主操作)</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>38 个工具</strong>,分为 7 类:<ul><li><strong>文件操作</strong>(14 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / append_file / edit_file / get_file_info / tree / diff_files / replace_in_files / read_multiple_files</li><li><strong>网络工具</strong>(3 个):web_search(联网搜索)/ web_fetch(网页抓取)/ download_file</li><li><strong>浏览器控制</strong>(8 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_close</li><li><strong>系统工具</strong>(3 个):run_command / git / compress</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></ul></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>15 轮</strong>工具调用,总超时 10 分钟,也可随时中断</li><li>独立工具自动并行执行,有依赖关系的工具串行执行</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral)</li></ul></div>
|
||||||
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li>对话满 <strong>6 条消息</strong>后自动触发记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 <code>memory_search</code> / <code>memory_add</code> 工具主动管理记忆</li><li>AI 可通过 <code>session_list</code> / <code>session_read</code> 查阅历史会话</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li>对话满 <strong>6 条消息</strong>后自动触发记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 <code>memory_search</code> / <code>memory_add</code> / <code>memory_replace</code> / <code>memory_remove</code> 工具主动管理记忆</li><li>AI 可通过 <code>session_list</code> / <code>session_read</code> 查阅历史会话</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
||||||
<div class="help-section"><h4>🎭 人格模式</h4><ul><li>设置中可切换 6 种预设人格:通用助手 / 编程助手 / 写作助手 / 数据分析 / 翻译助手 / 代码审查</li><li>不同人格有独立的 system prompt 和温度偏好</li><li>通用助手不做额外约束,其他人格会调整行为风格</li></ul></div>
|
<div class="help-section"><h4>🎭 人格模式</h4><ul><li>设置中可切换 6 种预设人格:通用助手 / 编程助手 / 写作助手 / 数据分析 / 翻译助手 / 代码审查</li><li>不同人格有独立的 system prompt 和温度偏好</li><li>通用助手不做额外约束,其他人格会调整行为风格</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>🔌 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>⏰ 定时任务(Cron)</h4><ul><li>支持一次性(at)和周期性(cron 表达式)任务调度</li><li>任务触发后将描述填入输入框,可直接发送给 AI 执行</li><li>设置面板可添加/管理/删除定时任务</li></ul></div>
|
<div class="help-section"><h4>⏰ 定时任务(Cron)</h4><ul><li>支持一次性(at)和周期性(cron 表达式)任务调度</li><li>任务触发后将描述填入输入框,可直接发送给 AI 执行</li><li>设置面板可添加/管理/删除定时任务</li></ul></div>
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
<div class="modal-overlay" id="toolsModal" style="display:none;">
|
<div class="modal-overlay" id="toolsModal" style="display:none;">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>🔧 工具面板(29 个)</h3>
|
<h3>🔧 工具面板(38 个)</h3>
|
||||||
<button class="icon-btn" id="btnCloseTools">
|
<button class="icon-btn" id="btnCloseTools">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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"/>
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
@@ -548,6 +548,14 @@
|
|||||||
<div class="tool-card-header"><span class="tool-card-icon">💾</span><span class="tool-card-name">memory_add</span><span class="tool-card-badge auto">自动</span></div>
|
<div class="tool-card-header"><span class="tool-card-icon">💾</span><span class="tool-card-name">memory_add</span><span class="tool-card-badge auto">自动</span></div>
|
||||||
<div class="tool-card-desc">添加记忆条目(事实/偏好/规则),跨会话积累</div>
|
<div class="tool-card-desc">添加记忆条目(事实/偏好/规则),跨会话积累</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tool-card">
|
||||||
|
<div class="tool-card-header"><span class="tool-card-icon">🔄</span><span class="tool-card-name">memory_replace</span><span class="tool-card-badge auto">自动</span></div>
|
||||||
|
<div class="tool-card-desc">替换已有记忆内容(子串匹配 old_text,替换为 new_content)</div>
|
||||||
|
</div>
|
||||||
|
<div class="tool-card">
|
||||||
|
<div class="tool-card-header"><span class="tool-card-icon">🗑️</span><span class="tool-card-name">memory_remove</span><span class="tool-card-badge auto">自动</span></div>
|
||||||
|
<div class="tool-card-desc">删除记忆条目(子串匹配 old_text)</div>
|
||||||
|
</div>
|
||||||
<div class="tool-card">
|
<div class="tool-card">
|
||||||
<div class="tool-card-header"><span class="tool-card-icon">📋</span><span class="tool-card-name">session_list</span><span class="tool-card-badge auto">自动</span></div>
|
<div class="tool-card-header"><span class="tool-card-icon">📋</span><span class="tool-card-name">session_list</span><span class="tool-card-badge auto">自动</span></div>
|
||||||
<div class="tool-card-desc">列出历史会话,支持按标题搜索</div>
|
<div class="tool-card-desc">列出历史会话,支持按标题搜索</div>
|
||||||
@@ -560,6 +568,14 @@
|
|||||||
<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-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 class="tool-card-desc">生成子代理独立执行任务,完成后回传结果</div>
|
||||||
</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">
|
||||||
<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-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">在 Agent 浏览器中打开 URL</div>
|
<div class="tool-card-desc">在 Agent 浏览器中打开 URL</div>
|
||||||
|
|||||||
@@ -28,16 +28,30 @@ import type {
|
|||||||
TraceEntry
|
TraceEntry
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
|
||||||
const MAX_LOOPS = 15;
|
|
||||||
const MAX_LOOP_TIME = 600000; // 10 分钟总超时
|
const MAX_LOOP_TIME = 600000; // 10 分钟总超时
|
||||||
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
|
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
|
||||||
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
|
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
|
||||||
const MAX_RETRIES = 2; // 工具错误自动重试次数
|
const MAX_RETRIES = 2; // 工具错误自动重试次数
|
||||||
|
|
||||||
|
/** 每个工具返回给模型的最大字符数 */
|
||||||
|
const TOOL_MAX_RESULT_SIZE: Record<string, number> = {
|
||||||
|
web_fetch: 20000, // 网页内容通常较长
|
||||||
|
web_search: 3000, // 搜索结果已精简
|
||||||
|
read_file: 15000, // 文件内容
|
||||||
|
read_multiple_files: 10000,
|
||||||
|
list_directory: 5000,
|
||||||
|
search_files: 5000,
|
||||||
|
run_command: 10000, // 命令输出
|
||||||
|
git: 5000,
|
||||||
|
session_read: 15000,
|
||||||
|
browser_extract: 10000,
|
||||||
|
browser_evaluate: 8000,
|
||||||
|
};
|
||||||
|
|
||||||
/** v4.1: 工具并行执行 — 依赖检测用 */
|
/** v4.1: 工具并行执行 — 依赖检测用 */
|
||||||
const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch', 'edit_file', 'append_file', 'move_file', 'delete_file']);
|
const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch', 'edit_file', 'append_file', 'move_file', 'delete_file']);
|
||||||
|
|
||||||
const AGENT_SYSTEM_PROMPT = `你是一个具备工具调用能力的 AI 助手。你有 34 个工具可以调用,包括文件操作、联网搜索、网页抓取、命令执行、浏览器控制、记忆管理、MCP 工具等。
|
const AGENT_SYSTEM_PROMPT = `你是一个具备工具调用能力的 AI 助手。你有 38 个工具可以调用,包括文件操作、联网搜索、网页抓取、命令执行、浏览器控制、记忆管理、MCP 工具等。
|
||||||
|
|
||||||
## 核心规则
|
## 核心规则
|
||||||
|
|
||||||
@@ -73,7 +87,9 @@ const TOOL_USAGE_GUIDE = `【工具链规则(强制执行)】
|
|||||||
7. 不要重复调用:已经成功调用过的工具+参数组合不要再调用。
|
7. 不要重复调用:已经成功调用过的工具+参数组合不要再调用。
|
||||||
8. 记忆工具:可以用 memory_search 搜索过去记忆,用 memory_add 添加重要信息。
|
8. 记忆工具:可以用 memory_search 搜索过去记忆,用 memory_add 添加重要信息。
|
||||||
9. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。
|
9. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。
|
||||||
10. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。`;
|
10. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。
|
||||||
|
11. 管理记忆:可以用 memory_replace 更新已有记忆(子串匹配 old_text),用 memory_remove 删除不再需要的记忆。
|
||||||
|
12. 查看技能:可以用 skill_list 列出所有可用技能,用 skill_view 查看特定技能的详细工具链。`;
|
||||||
|
|
||||||
/** 工具名白名单:用于文本解析兜底时过滤非法工具名 */
|
/** 工具名白名单:用于文本解析兜底时过滤非法工具名 */
|
||||||
const VALID_TOOL_NAMES = new Set([
|
const VALID_TOOL_NAMES = new Set([
|
||||||
@@ -81,7 +97,8 @@ const VALID_TOOL_NAMES = new Set([
|
|||||||
'delete_file', 'run_command', 'move_file', 'copy_file', 'web_fetch', 'web_search',
|
'delete_file', 'run_command', 'move_file', 'copy_file', 'web_fetch', 'web_search',
|
||||||
'append_file', 'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files',
|
'append_file', 'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files',
|
||||||
'replace_in_files', 'read_multiple_files', 'git', 'compress',
|
'replace_in_files', 'read_multiple_files', 'git', 'compress',
|
||||||
'memory_search', 'memory_add', 'session_list', 'session_read'
|
'memory_search', 'memory_add', 'memory_replace', 'memory_remove', 'session_list', 'session_read',
|
||||||
|
'skill_list', 'skill_view'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,7 +251,8 @@ function formatToolResultForModel(toolName: string, result: ToolResult): string
|
|||||||
case 'web_fetch': {
|
case 'web_fetch': {
|
||||||
let content = (result.content as string) || '';
|
let content = (result.content as string) || '';
|
||||||
// 截断过长内容,避免撑爆上下文
|
// 截断过长内容,避免撑爆上下文
|
||||||
if (content.length > 15000) content = content.slice(0, 15000) + '\n... (已截断)';
|
const webFetchMax = TOOL_MAX_RESULT_SIZE['web_fetch'] || 20000;
|
||||||
|
if (content.length > webFetchMax) content = content.slice(0, webFetchMax) + '\n... (已截断)';
|
||||||
return JSON.stringify({ success: true, url: result.url, content });
|
return JSON.stringify({ success: true, url: result.url, content });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +333,11 @@ function formatToolResultForModel(toolName: string, result: ToolResult): string
|
|||||||
k === 'status' || k === 'length' || k === 'isDirectory') continue;
|
k === 'status' || k === 'length' || k === 'isDirectory') continue;
|
||||||
clean[k] = v;
|
clean[k] = v;
|
||||||
}
|
}
|
||||||
return JSON.stringify(clean);
|
let json = JSON.stringify(clean);
|
||||||
|
// 按工具配置截断
|
||||||
|
const maxLen = TOOL_MAX_RESULT_SIZE[toolName] || 15000;
|
||||||
|
if (json.length > maxLen) json = json.slice(0, maxLen) + '\n... (已截断)';
|
||||||
|
return json;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,6 +488,9 @@ export async function runAgentLoop(
|
|||||||
|
|
||||||
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`);
|
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`);
|
||||||
|
|
||||||
|
// 迭代预算:从 state 读取,默认 15
|
||||||
|
const maxLoops = state.get<number>('maxTurns', 15);
|
||||||
|
|
||||||
let loopCount = 0;
|
let loopCount = 0;
|
||||||
const allToolRecords: ToolCallRecord[] = [];
|
const allToolRecords: ToolCallRecord[] = [];
|
||||||
const loopStartTime = Date.now();
|
const loopStartTime = Date.now();
|
||||||
@@ -481,7 +506,7 @@ export async function runAgentLoop(
|
|||||||
return { eval_count: totalEvalCount || undefined, total_duration: totalDuration };
|
return { eval_count: totalEvalCount || undefined, total_duration: totalDuration };
|
||||||
};
|
};
|
||||||
|
|
||||||
while (loopCount < MAX_LOOPS) {
|
while (loopCount < maxLoops) {
|
||||||
loopCount++;
|
loopCount++;
|
||||||
|
|
||||||
// 全局超时检查
|
// 全局超时检查
|
||||||
@@ -498,7 +523,7 @@ export async function runAgentLoop(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logAgentLoop(loopCount, MAX_LOOPS);
|
logAgentLoop(loopCount, maxLoops);
|
||||||
|
|
||||||
let thinking = '';
|
let thinking = '';
|
||||||
content = '';
|
content = '';
|
||||||
|
|||||||
@@ -248,6 +248,13 @@ export async function addMemory(data: {
|
|||||||
}): Promise<MemoryEntry> {
|
}): Promise<MemoryEntry> {
|
||||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||||
|
|
||||||
|
// 安全扫描:检测 prompt injection 和敏感信息
|
||||||
|
const securityCheck = scanMemorySecurity(data.content);
|
||||||
|
if (!securityCheck.safe) {
|
||||||
|
logWarn('记忆安全扫描拦截', securityCheck.reason);
|
||||||
|
throw new Error(`记忆内容被安全规则拦截: ${securityCheck.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 检查重复(内容相似度 > 80% 则跳过)
|
// 检查重复(内容相似度 > 80% 则跳过)
|
||||||
const existing = memoryCache.find(e => {
|
const existing = memoryCache.find(e => {
|
||||||
if (e.type !== data.type) return false;
|
if (e.type !== data.type) return false;
|
||||||
@@ -293,6 +300,112 @@ export async function addMemory(data: {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 安全扫描 ──
|
||||||
|
|
||||||
|
/** 记忆内容安全扫描 */
|
||||||
|
function scanMemorySecurity(content: string): { safe: boolean; reason: string } {
|
||||||
|
// Prompt injection 模式
|
||||||
|
const injectionPatterns = [
|
||||||
|
/ignore\s+(all\s+)?previous/i,
|
||||||
|
/forget\s+(all\s+)?instructions/i,
|
||||||
|
/you\s+are\s+now\s+a/i,
|
||||||
|
/new\s+system\s*prompt/i,
|
||||||
|
/override\s+(your|the)\s+/i,
|
||||||
|
/disregard\s+(all|any|previous)/i,
|
||||||
|
];
|
||||||
|
for (const p of injectionPatterns) {
|
||||||
|
if (p.test(content)) return { safe: false, reason: '疑似 prompt injection 攻击' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 敏感信息模式
|
||||||
|
const secretPatterns = [
|
||||||
|
/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/,
|
||||||
|
/sk-[a-zA-Z0-9]{20,}/,
|
||||||
|
/ghp_[a-zA-Z0-9]{36}/,
|
||||||
|
/AKIA[A-Z0-9]{16}/,
|
||||||
|
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/, // 信用卡号
|
||||||
|
];
|
||||||
|
for (const p of secretPatterns) {
|
||||||
|
if (p.test(content)) return { safe: false, reason: '疑似包含敏感信息(密钥/密码/信用卡号)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不可见字符
|
||||||
|
if (/[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/.test(content)) {
|
||||||
|
return { safe: false, reason: '包含不可见 Unicode 字符' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { safe: true, reason: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 记忆替换(根据 old_text 子串匹配)──
|
||||||
|
|
||||||
|
export async function replaceMemoryByContent(
|
||||||
|
target: 'memory' | 'user',
|
||||||
|
oldText: string,
|
||||||
|
newContent: string
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
if (!oldText || oldText.length < 2) {
|
||||||
|
return { success: false, message: 'old_text 不能为空且至少 2 个字符' };
|
||||||
|
}
|
||||||
|
if (!newContent || newContent.length < 2) {
|
||||||
|
return { success: false, message: 'new_content 不能为空且至少 2 个字符' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全扫描
|
||||||
|
const securityCheck = scanMemorySecurity(newContent);
|
||||||
|
if (!securityCheck.safe) {
|
||||||
|
return { success: false, message: `新内容被安全规则拦截: ${securityCheck.reason}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配
|
||||||
|
const matches = memoryCache.filter(e => {
|
||||||
|
if (target === 'user') {
|
||||||
|
return (e.type === 'preference' || e.type === 'fact') && e.content.includes(oldText);
|
||||||
|
}
|
||||||
|
return e.content.includes(oldText);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return { success: false, message: `未找到包含 "${oldText.slice(0, 50)}" 的记忆` };
|
||||||
|
}
|
||||||
|
if (matches.length > 1) {
|
||||||
|
return { success: false, message: `匹配到 ${matches.length} 条记忆,请使用更精确的 old_text` };
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateMemory(matches[0].id, { content: newContent.trim() });
|
||||||
|
logMemory('替换记忆', `${matches[0].id}: ${oldText.slice(0, 30)} → ${newContent.slice(0, 30)}`);
|
||||||
|
return { success: true, message: `已替换记忆: ${matches[0].id}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 记忆删除(根据 old_text 子串匹配)──
|
||||||
|
|
||||||
|
export async function removeMemoryByContent(
|
||||||
|
target: 'memory' | 'user',
|
||||||
|
oldText: string
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
if (!oldText || oldText.length < 2) {
|
||||||
|
return { success: false, message: 'old_text 不能为空且至少 2 个字符' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = memoryCache.filter(e => {
|
||||||
|
if (target === 'user') {
|
||||||
|
return (e.type === 'preference' || e.type === 'fact') && e.content.includes(oldText);
|
||||||
|
}
|
||||||
|
return e.content.includes(oldText);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return { success: false, message: `未找到包含 "${oldText.slice(0, 50)}" 的记忆` };
|
||||||
|
}
|
||||||
|
if (matches.length > 1) {
|
||||||
|
return { success: false, message: `匹配到 ${matches.length} 条记忆,请使用更精确的 old_text` };
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteMemory(matches[0].id);
|
||||||
|
logMemory('删除记忆', `${matches[0].id}: ${oldText.slice(0, 50)}`);
|
||||||
|
return { success: true, message: `已删除记忆: ${matches[0].id}` };
|
||||||
|
}
|
||||||
|
|
||||||
// ── 记忆更新 ──
|
// ── 记忆更新 ──
|
||||||
|
|
||||||
export async function updateMemory(id: string, updates: Partial<MemoryEntry>): Promise<void> {
|
export async function updateMemory(id: string, updates: Partial<MemoryEntry>): Promise<void> {
|
||||||
|
|||||||
@@ -268,3 +268,56 @@ function generateSkillDescription(chain: ToolChainStep[], sessionTitle: string):
|
|||||||
const steps = chain.map((s, i) => `${i + 1}. ${s.description}`).join(';');
|
const steps = chain.map((s, i) => `${i + 1}. ${s.description}`).join(';');
|
||||||
return `从会话「${sessionTitle}」中提取。步骤:${steps}`;
|
return `从会话「${sessionTitle}」中提取。步骤:${steps}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 渐进式技能加载 ──
|
||||||
|
|
||||||
|
/** 列出所有技能(渐进式 Level 0:只返回名称、描述、成功率) */
|
||||||
|
export async function listSkills(): Promise<Array<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
success_rate: string;
|
||||||
|
chain_preview: string;
|
||||||
|
}>> {
|
||||||
|
const bridge = (window as any).metonaDesktop;
|
||||||
|
if (!bridge?.db?.getAllSkills) return [];
|
||||||
|
const allSkills: Skill[] = await bridge.db.getAllSkills();
|
||||||
|
return allSkills.map(skill => {
|
||||||
|
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,
|
||||||
|
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 as any).metonaDesktop;
|
||||||
|
if (!bridge?.db?.getAllSkills) return { success: false, error: '数据库不可用' };
|
||||||
|
const allSkills: Skill[] = await bridge.db.getAllSkills();
|
||||||
|
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}%` }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -388,6 +388,37 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'memory_replace',
|
||||||
|
description: 'Replace an existing memory entry with updated content. Uses substring matching to find the entry to replace.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['target', 'old_text', 'new_content'],
|
||||||
|
properties: {
|
||||||
|
target: { type: 'string', enum: ['memory', 'user'], description: 'Target store: memory (agent notes) or user (user profile).' },
|
||||||
|
old_text: { type: 'string', description: 'Unique substring that identifies the entry to replace.' },
|
||||||
|
new_content: { type: 'string', description: 'The new content to replace the old entry with.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'memory_remove',
|
||||||
|
description: 'Remove a memory entry. Uses substring matching to find the entry to remove.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['target', 'old_text'],
|
||||||
|
properties: {
|
||||||
|
target: { type: 'string', enum: ['memory', 'user'], description: 'Target store: memory (agent notes) or user (user profile).' },
|
||||||
|
old_text: { type: 'string', description: 'Unique substring that identifies the entry to remove.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
@@ -417,6 +448,31 @@ 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 新增工具:子代理委派
|
// v4.3 新增工具:子代理委派
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
@@ -562,7 +618,8 @@ let enabledTools: Set<string> = new Set([
|
|||||||
'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file',
|
'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file',
|
||||||
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
|
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
|
||||||
'read_multiple_files', 'git', 'compress',
|
'read_multiple_files', 'git', 'compress',
|
||||||
'memory_search', 'memory_add', 'session_list', 'session_read', 'spawn_task',
|
'memory_search', 'memory_add', 'memory_replace', 'memory_remove',
|
||||||
|
'session_list', 'session_read', 'skill_list', 'skill_view', 'spawn_task',
|
||||||
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
|
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
|
||||||
'browser_click', 'browser_type', 'browser_scroll', 'browser_close'
|
'browser_click', 'browser_type', 'browser_scroll', 'browser_close'
|
||||||
]);
|
]);
|
||||||
@@ -637,6 +694,23 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
|||||||
logToolResult('memory_add', true, `${type}: ${content.slice(0, 50)}`);
|
logToolResult('memory_add', true, `${type}: ${content.slice(0, 50)}`);
|
||||||
return { success: true, id: entry.id, type: entry.type, content: entry.content };
|
return { success: true, id: entry.id, type: entry.type, content: entry.content };
|
||||||
}
|
}
|
||||||
|
if (toolName === 'memory_replace') {
|
||||||
|
const { replaceMemoryByContent } = await import('./memory-manager.js');
|
||||||
|
const target = String(args.target || 'memory') as 'memory' | 'user';
|
||||||
|
const oldText = String(args.old_text || '');
|
||||||
|
const newContent = String(args.new_content || '');
|
||||||
|
const result = await replaceMemoryByContent(target, oldText, newContent);
|
||||||
|
logToolResult('memory_replace', result.success, result.message);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (toolName === 'memory_remove') {
|
||||||
|
const { removeMemoryByContent } = await import('./memory-manager.js');
|
||||||
|
const target = String(args.target || 'memory') as 'memory' | 'user';
|
||||||
|
const oldText = String(args.old_text || '');
|
||||||
|
const result = await removeMemoryByContent(target, oldText);
|
||||||
|
logToolResult('memory_remove', result.success, result.message);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
if (toolName === 'session_list') {
|
if (toolName === 'session_list') {
|
||||||
const bridge = window.metonaDesktop;
|
const bridge = window.metonaDesktop;
|
||||||
if (!bridge?.db) return { success: false, error: '桌面 API 不可用' };
|
if (!bridge?.db) return { success: false, error: '桌面 API 不可用' };
|
||||||
@@ -688,6 +762,19 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
|||||||
logToolResult('spawn_task', result.success, result.success ? `完成, ${(result as any).loops} 轮` : result.error);
|
logToolResult('spawn_task', result.success, result.success ? `完成, ${(result as any).loops} 轮` : result.error);
|
||||||
return result;
|
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
|
// run_command 走 workspace IPC
|
||||||
if (toolName === 'run_command') {
|
if (toolName === 'run_command') {
|
||||||
@@ -731,7 +818,8 @@ export function getToolIcon(name: string): string {
|
|||||||
edit_file: '✂️', get_file_info: 'ℹ️', tree: '🌳', download_file: '⬇️',
|
edit_file: '✂️', get_file_info: 'ℹ️', tree: '🌳', download_file: '⬇️',
|
||||||
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
||||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||||
memory_search: '🧠', memory_add: '💾', session_list: '📋', session_read: '📖', spawn_task: '🤖',
|
memory_search: '🧠', memory_add: '💾', memory_replace: '🔄', memory_remove: '🗑️',
|
||||||
|
session_list: '📋', session_read: '📖', skill_list: '📚', skill_view: '📖', spawn_task: '🤖',
|
||||||
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
||||||
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
||||||
};
|
};
|
||||||
@@ -747,7 +835,8 @@ export function formatToolName(name: string): string {
|
|||||||
get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
|
get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
|
||||||
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
||||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||||
memory_search: '搜索记忆', memory_add: '添加记忆', session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
|
memory_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
|
||||||
|
session_list: '会话列表', session_read: '读取会话', skill_list: '技能列表', skill_view: '技能详情', spawn_task: '子代理委派',
|
||||||
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
||||||
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user