diff --git a/README.md b/README.md index 4a344e1..dae5e4c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

- version + version electron typescript license @@ -35,12 +35,12 @@ | | 功能 | 说明 | |:---:|:---|:---| | 🤖 | **ReAct Agent Loop** | 始终开启的唯一对话模式。Thought → Action → Observation → Reflection 完整循环,最大 85 轮(可配置),自动重试 2 次,工具去重,并行/链式执行 | -| 🔧 | **38 个内置工具** | 文件系统(16个) · 命令执行 · 联网搜索 · 浏览器控制(9个) · Git · 记忆 · 会话 · 子代理 | +| 🔧 | **44 个内置工具** | 文件系统(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 防护 | -| 🔍 | **四引擎联网搜索** | Bing + 百度 + DuckDuckGo + Google 并行聚合,智能排序+时间过滤+摘要增强,web_fetch支持反爬+UA切换+浏览器回退 | +| 🔍 | **四引擎联网搜索** | Bing + 百度 + 搜狗 + 360搜索 并行聚合,智能排序+时间过滤+摘要增强,web_fetch支持反爬+UA切换+浏览器回退 | | 🌏 | **浏览器控制** | 打开网页 · 截图 · 执行 JS · 提取内容 · 点击 · 输入 · 滚动 · 关闭 | | 🖥️ | **工作空间面板** | 终端(实时流式输出)+ 文件浏览器,命令安全检查 | | 🔢 | **上下文长度自动检测** | 切换模型时自动从 model_info 获取实际支持的上下文长度,无需手动配置 | @@ -92,7 +92,7 @@ | 工具 | 功能 | |------|------| -| `web_search` | 四引擎并行搜索(Bing+百度+DDG+Google),智能排序+时间过滤+摘要增强,5分钟缓存 | +| `web_search` | 四引擎并行搜索(Bing+百度+搜狗+360),智能排序+时间过滤+摘要增强,5分钟缓存 | | `web_fetch` | 网页内容抓取,自动重试(指数退避),移动端UA切换,SPA页面自动升级到浏览器渲染 | @@ -150,6 +150,20 @@ +

+🕐 系统工具(2 个) + +| 工具 | 功能 | +|------|------| +| `datetime` | 获取系统精确时间(ISO/Unix/日期/时间/完整) | +| `calculator` | 安全数学计算(+ - * / ** % (),递归下降解析器) | +| `random` | 随机生成(整数/浮点/抽取/随机字符串) | +| `uuid` | 生成 UUID v4 唯一标识符 | +| `json_format` | JSON 格式化 + 语法验证 + 键排序 | +| `hash` | 哈希计算(MD5/SHA1/SHA256/SHA384/SHA512) | + +
+ ## 🏗️ 架构 ``` @@ -231,7 +245,7 @@ npm start ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist ``` -产出:`release/Metona Ollama Setup v0.11.0.exe` +产出:`release/Metona Ollama Setup v0.11.1.exe` ## 🛠️ 常用命令 @@ -262,12 +276,12 @@ 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 | -| 🔧 | **39 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Skills · Sessions · Sub-agent | +| 🔧 | **44 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Skills · 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 | -| 🔍 | **Quad-engine Web Search** | Bing + Baidu + DuckDuckGo + Google parallel, 5min cache, URL reachability check, web_fetch with auto-retry+mobile UA+SPA browser fallback | +| 🔍 | **Quad-engine Web Search** | Bing + Baidu + Sogou + 360 parallel, 5min cache, URL reachability check, web_fetch with auto-retry+mobile UA+SPA browser fallback | | 🌏 | **Browser Control** | Open pages · Screenshot · JS execution · Content extraction · Click · Type · Scroll · Close | | 🖥️ | **Workspace Panel** | Terminal (real-time streaming) + file browser, command security checks | | 🔢 | **Auto Context Length Detection** | Reads actual context_length from model metadata on model switch — no manual config needed | @@ -376,6 +390,20 @@ npm run dist # 构建 Windows 安装包 +
+🕐 System Tools (2) + +| Tool | Function | +|------|------| +| `datetime` | Get precise system time (ISO/Unix/date/time/full) | +| `calculator` | Safe math evaluation (+ - * / ** % (), recursive descent parser) | +| `random` | Random generation (int/float/pick/string) | +| `uuid` | Generate UUID v4 unique identifier | +| `json_format` | JSON format + validate + sort keys | +| `hash` | Cryptographic hash (MD5/SHA1/SHA256/SHA384/SHA512) | + +
+ ## 🏗️ Architecture ``` @@ -385,7 +413,7 @@ User message → workspace SOUL.md (never compressed) → AGENT.md → USER.md ↓ Ollama API (Streaming Response) ↓ - Tool Registry (39 Built-in + MCP Dynamic) + Tool Registry (44 Built-in + MCP Dynamic) ↓ Observation → Reflection → Loop / Final Answer ``` @@ -453,7 +481,7 @@ npm start ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist ``` -Output: `release/Metona Ollama Setup v0.11.0.exe` +Output: `release/Metona Ollama Setup v0.11.1.exe` ## 🛠️ Common Commands diff --git a/docs/BUILD.md b/docs/BUILD.md index babfe92..eab3897 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -91,7 +91,7 @@ npm run dist # NSIS 安装包 | 文件 | 说明 | |------|------| -| `release/Metona Ollama Setup v0.11.0.exe` | NSIS 安装包(可选目录、创建快捷方式) | +| `release/Metona Ollama Setup v0.11.1.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.0","name":"v0.11.0","body":"Release notes","target_commitish":"master"}' + -d '{"tag_name":"v0.11.1","name":"v0.11.1","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="" 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.0.exe" + -H "Content-Type: multipart/form-data" -F "file=@release/Metona Ollama Setup v0.11.1.exe" ``` ## 故障排查 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 97b8db5..dcced9a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,6 +1,6 @@ # Metona Ollama Desktop — 开发规范 -> 版本: v0.11.0 | 更新: 2026-06-05 | 维护: 项目团队 +> 版本: v0.11.1 | 更新: 2026-06-05 | 维护: 项目团队 --- diff --git a/package-lock.json b/package-lock.json index 5c2ea4c..a0403c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "metona-ollama-desktop", - "version": "0.11.0", + "version": "0.11.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metona-ollama-desktop", - "version": "0.11.0", + "version": "0.11.1", "license": "MIT", "dependencies": { "sql.js": "^1.11.0" @@ -63,9 +63,9 @@ "license": "MIT" }, "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -140,9 +140,9 @@ } }, "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -213,9 +213,9 @@ } }, "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -280,9 +280,9 @@ } }, "node_modules/@electron/rebuild/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -293,9 +293,9 @@ } }, "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -342,9 +342,9 @@ "license": "MIT" }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -352,9 +352,9 @@ } }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, "license": "MIT", "dependencies": { @@ -367,9 +367,9 @@ } }, "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -855,7 +855,7 @@ "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.5", + "version": "5.1.2", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, @@ -962,9 +962,9 @@ } }, "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -999,9 +999,9 @@ } }, "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -1038,9 +1038,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", "cpu": [ "arm" ], @@ -1052,9 +1052,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", "cpu": [ "arm64" ], @@ -1066,9 +1066,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", "cpu": [ "arm64" ], @@ -1080,9 +1080,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", "cpu": [ "x64" ], @@ -1094,9 +1094,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", "cpu": [ "arm64" ], @@ -1108,9 +1108,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", "cpu": [ "x64" ], @@ -1122,13 +1122,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1136,13 +1139,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1150,13 +1156,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1164,13 +1173,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1178,13 +1190,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1192,13 +1207,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1206,13 +1224,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1220,13 +1241,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1234,13 +1258,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1248,13 +1275,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1262,13 +1292,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1276,13 +1309,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", - "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1290,13 +1326,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1304,9 +1343,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", "cpu": [ "x64" ], @@ -1318,9 +1357,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", "cpu": [ "arm64" ], @@ -1332,9 +1371,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", "cpu": [ "arm64" ], @@ -1346,9 +1385,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", "cpu": [ "ia32" ], @@ -1360,9 +1399,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", "cpu": [ "x64" ], @@ -1374,9 +1413,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", "cpu": [ "x64" ], @@ -1414,9 +1453,9 @@ } }, "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", "dev": true, "license": "MIT", "engines": { @@ -1447,9 +1486,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1488,9 +1527,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "version": "20.19.43", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { @@ -1539,13 +1578,13 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.9.10", + "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", "dev": true, "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.6" } }, "node_modules/7zip-bin": { @@ -1600,9 +1639,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1723,9 +1762,9 @@ } }, "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1736,9 +1775,9 @@ } }, "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -1826,7 +1865,7 @@ } }, "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.5", + "version": "5.1.2", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, @@ -1992,9 +2031,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -2101,9 +2140,9 @@ } }, "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2161,9 +2200,9 @@ "license": "MIT" }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -2505,9 +2544,9 @@ "license": "MIT" }, "node_modules/config-file-ts/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -2792,9 +2831,9 @@ "license": "MIT" }, "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -2849,9 +2888,9 @@ } }, "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3041,9 +3080,9 @@ } }, "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "peer": true, @@ -3081,9 +3120,9 @@ } }, "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3135,9 +3174,9 @@ } }, "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3223,9 +3262,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -3403,9 +3442,9 @@ "license": "MIT" }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -3656,9 +3695,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -3699,9 +3738,9 @@ } }, "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "optional": true, @@ -3837,9 +3876,9 @@ "license": "ISC" }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -4020,9 +4059,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "engines": { @@ -4145,10 +4184,20 @@ } }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4251,7 +4300,7 @@ } }, "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.5", + "version": "5.1.2", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, @@ -4398,7 +4447,7 @@ } }, "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "5.0.1", + "version": "5.0.0", "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, @@ -4660,9 +4709,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -4689,9 +4738,9 @@ } }, "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "version": "3.92.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4702,9 +4751,9 @@ } }, "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -4733,9 +4782,9 @@ } }, "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -4772,9 +4821,9 @@ } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -4863,7 +4912,7 @@ } }, "node_modules/onetime": { - "version": "5.1.5", + "version": "5.1.2", "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, @@ -5035,13 +5084,13 @@ "license": "ISC" }, "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", "dev": true, "license": "MIT", "dependencies": { - "@xmldom/xmldom": "^0.8.8", + "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, @@ -5050,9 +5099,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -5070,7 +5119,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5199,9 +5248,9 @@ "peer": true }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "peer": true, @@ -5332,13 +5381,13 @@ } }, "node_modules/rollup": { - "version": "4.60.1", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", - "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -5348,31 +5397,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.1", - "@rollup/rollup-android-arm64": "4.60.1", - "@rollup/rollup-darwin-arm64": "4.60.1", - "@rollup/rollup-darwin-x64": "4.60.1", - "@rollup/rollup-freebsd-arm64": "4.60.1", - "@rollup/rollup-freebsd-x64": "4.60.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", - "@rollup/rollup-linux-arm-musleabihf": "4.60.1", - "@rollup/rollup-linux-arm64-gnu": "4.60.1", - "@rollup/rollup-linux-arm64-musl": "4.60.1", - "@rollup/rollup-linux-loong64-gnu": "4.60.1", - "@rollup/rollup-linux-loong64-musl": "4.60.1", - "@rollup/rollup-linux-ppc64-gnu": "4.60.1", - "@rollup/rollup-linux-ppc64-musl": "4.60.1", - "@rollup/rollup-linux-riscv64-gnu": "4.60.1", - "@rollup/rollup-linux-riscv64-musl": "4.60.1", - "@rollup/rollup-linux-s390x-gnu": "4.60.1", - "@rollup/rollup-linux-x64-gnu": "4.60.1", - "@rollup/rollup-linux-x64-musl": "4.60.1", - "@rollup/rollup-openbsd-x64": "4.60.1", - "@rollup/rollup-openharmony-arm64": "4.60.1", - "@rollup/rollup-win32-arm64-msvc": "4.60.1", - "@rollup/rollup-win32-ia32-msvc": "4.60.1", - "@rollup/rollup-win32-x64-gnu": "4.60.1", - "@rollup/rollup-win32-x64-msvc": "4.60.1", + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" } }, @@ -5510,9 +5559,9 @@ } }, "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -5550,13 +5599,13 @@ } }, "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "version": "2.8.9", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -5791,7 +5840,7 @@ } }, "node_modules/tar/node_modules/minipass": { - "version": "5.0.1", + "version": "5.0.0", "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, @@ -5827,9 +5876,9 @@ } }, "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5850,9 +5899,9 @@ } }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 7b87a91..a0d584b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metona-ollama-desktop", - "version": "0.11.0", + "version": "0.11.1", "description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端", "main": "dist/main/main.js", "author": "thzxx", @@ -67,7 +67,6 @@ "vite": "^5.4.0" }, "dependencies": { - "sql.js": "^1.11.0", - "https-proxy-agent": "^7.0.0" + "sql.js": "^1.11.0" } } diff --git a/release-notes.md b/release-notes.md index e8eaa99..39b79ec 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,7 +4,7 @@ ## 项目简介 -Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,采用 TypeScript + Electron 构建。所有 AI 推理均在本地完成,数据不离开本机。支持 ReAct Agent Loop、39 个内置工具、智能记忆系统、MCP 协议扩展、四引擎联网搜索、浏览器控制等能力。 +Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端,采用 TypeScript + Electron 构建。所有 AI 推理均在本地完成,数据不离开本机。支持 ReAct Agent Loop、44 个内置工具、智能记忆系统、MCP 协议扩展、四引擎联网搜索、浏览器控制等能力。 --- @@ -18,19 +18,20 @@ Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端, - 链式工具调用:web_search → web_fetch、list_directory → read_file 等模式 - 文本解析兜底:当模型未通过 tool_calls 返回时,从文本中提取 Action/Input -### 🔧 39 个内置工具 +### 🔧 44 个内置工具 | 分类 | 数量 | 工具 | |------|------|------| | 文件系统 | 17 | read_file, write_file, list_directory, search_files, create_directory, delete_file, move_file, copy_file, append_file, edit_file, get_file_info, tree, download_file, diff_files, replace_in_files, read_multiple_files, compress | | 命令执行 | 1 | run_command(实时流式输出,工作空间终端集成) | -| 联网搜索 | 2 | web_search(Bing + 百度 + DuckDuckGo + Google 四引擎), web_fetch | +| 联网搜索 | 2 | web_search(Bing + 百度 + 搜狗 + 360搜索 四引擎), web_fetch | | 浏览器控制 | 8 | browser_open, browser_screenshot, browser_evaluate, browser_extract, browser_click, browser_type, browser_scroll, browser_close | | Git | 1 | git(init/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 | ### 🧠 智能记忆系统 - **三类记忆**:fact(事实)、preference(偏好)、rule(规则,始终注入上下文) @@ -46,7 +47,7 @@ Metona Ollama Desktop 是一个基于 **Ollama** 的本地 AI 桌面客户端, - 60 秒请求超时保护 ### 🔍 四引擎联网搜索 -- Bing + 百度 + DuckDuckGo + Google 四引擎聚合 +- Bing + 百度 + 搜狗 + 360搜索 四引擎聚合 - web_search 返回标题、URL、摘要(默认 15 条) - web_fetch 支持网页内容抓取(默认不截断) - 工具链规则:搜索后自动引导抓取详情 diff --git a/search-proxy/README.md b/search-proxy/README.md deleted file mode 100644 index 94a51c4..0000000 --- a/search-proxy/README.md +++ /dev/null @@ -1,203 +0,0 @@ -# metona-search-proxy - -部署在可直连 Google/Bing/DuckDuckGo/Baidu 的云服务器上,通过 HTTP API 提供搜索引擎代理。 - -``` -本应用(web_search) → HTTP → metona-search-proxy(:7899) → Google/Bing/DDG/Baidu → JSON -``` - ---- - -## 前置要求 - -| 依赖 | 说明 | -|------|------| -| **Go** | 1.21+(编译用,运行不需要) | -| **服务器** | 能直连 Google/Bing/DDG(海外云服务器/VPS) | -| **系统** | Linux (Ubuntu/Debian/CentOS) / macOS | - -### 安装 Go(服务器上没有的话) - -```bash -# Ubuntu/Debian -sudo apt update && sudo apt install -y golang-go - -# CentOS/RHEL -sudo yum install -y golang -``` - ---- - -## 编译 - -```bash -cd /opt -git clone https://gitee.com/thzxx/metona-ollama-desktop.git -cd metona-ollama-desktop/search-proxy -go build -o metona-search-proxy . -``` - -> 如果没有 `go.mod`(或报 `go.mod file not found`): -> ```bash -> GO111MODULE=off go build -o metona-search-proxy . -> ``` - -### 本地交叉编译(编译好再上传) - -```bash -GOOS=linux GOARCH=amd64 go build -o metona-search-proxy . -scp metona-search-proxy user@your-server:/opt/ -``` - ---- - -## 全局命令 - -```bash -sudo cp metona-search-proxy /usr/local/bin/ -sudo chmod +x /usr/local/bin/metona-search-proxy -``` - -之后任何目录都可以直接用: - -```bash -metona-search-proxy --version -metona-search-proxy -status -``` - ---- - -## CLI 命令 - -### 服务管理 - -```bash -metona-search-proxy -start # 后台启动 -metona-search-proxy -stop # 停止 -metona-search-proxy -status # 查看状态 -metona-search-proxy -restart # 重启 -``` - -### 运行参数 - -```bash -metona-search-proxy [选项] - -选项: - -port int 监听端口 (默认 7899) - -cache int 缓存时间/分钟 (默认 5) - -rate int 限流速率 req/s (默认 30) - -burst int 限流突发容量 (默认 60) - -timeout int 搜索引擎超时/秒 (默认 12) - -version 显示版本号 - -start 后台启动 - -stop 停止后台实例 - -status 查看运行状态 - -restart 重启后台实例 -``` - -### 示例 - -```bash -# 查看状态 -metona-search-proxy -status -# ● 运行中 PID: 12345 端口: 7899 -# 健康检查: http://localhost:7899/health - -# 前台运行(调试用,Ctrl+C 停止) -metona-search-proxy - -# 后台启动(默认端口 7899) -metona-search-proxy -start -# 已后台启动 (PID: 12345) - -# 后台启动(自定义端口和缓存) -metona-search-proxy -start -port 6789 -cache 10 -rate 50 - -# 停止 -metona-search-proxy -stop -# 已停止 (PID: 12345) -``` - ---- - -## 部署方式(二选一) - -### 方式一:systemd 服务(推荐—生产环境) - -```bash -# 1. 安装二进制 -sudo mkdir -p /opt/metona-search-proxy -sudo cp metona-search-proxy /opt/metona-search-proxy/ -sudo chmod +x /opt/metona-search-proxy/metona-search-proxy - -# 2. 创建服务 -sudo cp metona-search-proxy.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable --now metona-search-proxy - -# 3. 管理 -sudo systemctl status metona-search-proxy # 状态 -sudo systemctl stop metona-search-proxy # 停止 -sudo journalctl -u metona-search-proxy -f # 日志 -``` - -> ⚠️ 使用 systemd 后不要再用 `-start/-stop` CLI 命令,会冲突。 - -### 方式二:CLI 管理(轻量—个人开发) - -```bash -# 安装全局命令 -sudo cp metona-search-proxy /usr/local/bin/ - -# 启动 -metona-search-proxy -start - -# 查看状态 -metona-search-proxy -status - -# 停止 -metona-search-proxy -stop - -# 设置开机自启(加到 crontab) -(crontab -l 2>/dev/null; echo "@reboot metona-search-proxy -start") | crontab - -``` - -> ⚠️ 使用 CLI 管理前确保 systemd 服务已停止:`sudo systemctl stop metona-search-proxy` - ---- - -## 测试 - -```bash -curl http://localhost:7899/health -curl "http://localhost:7899/search?q=hello&engine=google&n=5" -curl "http://localhost:7899/search?q=天气&engine=all&n=10" -``` - ---- - -## API - -``` -GET /search?q=关键词&engine=all&n=10 - -engine: google | bing | ddg | baidu | all (默认) -n: 1-20,默认10 -缓存: 5分钟(可配) -限流: 30 req/s(可配) - -GET /health -``` - ---- - -## 本应用配置 - -Metona Ollama Desktop → 设置 → 代理 → 填入: - -``` -http://服务器IP:7899 -``` - -重启应用即可。 diff --git a/search-proxy/main.go b/search-proxy/main.go deleted file mode 100644 index c547813..0000000 --- a/search-proxy/main.go +++ /dev/null @@ -1,784 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "io" - "log" - "net" - "net/http" - "net/url" - "os" - "os/exec" - "os/signal" - "regexp" - "runtime" - "strconv" - "strings" - "sync" - "sync/atomic" - "syscall" - "time" -) - -const version = "2.2.0" - -// ── CLI ── -var ( - cliPort = flag.Int("port", 7899, "监听端口") - cliCache = flag.Int("cache", 5, "缓存时间(分钟)") - cliRate = flag.Int("rate", 30, "限流速率(req/s)") - cliBurst = flag.Int("burst", 60, "限流突发容量") - cliTimeout = flag.Int("timeout", 12, "搜索引擎请求超时(秒)") - cliVersion = flag.Bool("version", false, "显示版本") - cliStop = flag.Bool("stop", false, "停止后台运行的实例") - cliStart = flag.Bool("start", false, "后台启动(nohup模式)") - cliStatus = flag.Bool("status", false, "查看运行状态") - cliRestart = flag.Bool("restart", false, "重启后台实例") -) - -var pidFile = "/tmp/metona-search-proxy.pid" - -// ── 统计 ── -type statsData struct { - ReqTotal atomic.Int64 - ReqRate atomic.Int64 // 被限流的 - CacheHit atomic.Int64 - EngineOK [4]atomic.Int64 // google/bing/baidu/ddg 成功 - EngineFail [4]atomic.Int64 // google/bing/baidu/ddg 失败 - StartTime time.Time -} - -var stats = &statsData{StartTime: time.Now()} - -// ── 限流器 ── -type RateLimiter struct { - tokens atomic.Int64 - capacity int64 - rate int64 - lastFill atomic.Int64 -} - -func newRateLimiter(rate, cap int64) *RateLimiter { - rl := &RateLimiter{rate: rate, capacity: cap} - rl.tokens.Store(cap) - rl.lastFill.Store(time.Now().UnixNano()) - return rl -} - -func (rl *RateLimiter) Allow() bool { - now := time.Now().UnixNano() - last := rl.lastFill.Load() - elapsed := float64(now-last) / 1e9 - newTokens := int64(elapsed * float64(rl.rate)) - if newTokens > 0 { - rl.lastFill.Store(now) - for { - current := rl.tokens.Load() - next := current + newTokens - if next > rl.capacity { - next = rl.capacity - } - if rl.tokens.CompareAndSwap(current, next) { - break - } - } - } - for { - current := rl.tokens.Load() - if current <= 0 { - return false - } - if rl.tokens.CompareAndSwap(current, current-1) { - return true - } - } -} - -// ── 缓存 ── -var cache sync.Map - -type cacheEntry struct { - Results []Result - ExpiresAt int64 -} - -var cacheSize atomic.Int64 - -func cleanCacheLoop(interval time.Duration) { - for { - time.Sleep(interval) - now := time.Now().Unix() - count := 0 - cache.Range(func(key, value any) bool { - ce := value.(cacheEntry) - if now >= ce.ExpiresAt { - cache.Delete(key) - count++ - } - return true - }) - if count > 0 { - log.Printf("cache cleaned: %d entries removed", count) - } - } -} - -// ── 正则预编译(全局,避免每次搜索重新编译)── -var ( - reBingBlock = regexp.MustCompile(`
  • ]*>([\s\S]*?)
  • `) - reBingTitle = regexp.MustCompile(`]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)`) - reBingSnip = regexp.MustCompile(`]*>([\s\S]*?)

    `) - - reDDGLink = regexp.MustCompile(`]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)`) - - reBaiduBlock = regexp.MustCompile(`
    ]*>([\s\S]*?)
    \s*(?:
    ]*>[\s\S]*?]*href="([^"]*)"[^>]*>([\s\S]*?)`) - reBaiduSnip = regexp.MustCompile(`
    ]*>([\s\S]*?)
    `) - reBaiduDURL = regexp.MustCompile(`data-url="(https?://[^"]*)"`) - - reHTMLTags = regexp.MustCompile(`<[^>]+>`) -) - -type Result struct { - Title string `json:"title"` - URL string `json:"url"` - Snippet string `json:"snippet"` - Engine string `json:"engine"` -} - -type SearchResp struct { - Success bool `json:"success"` - Query string `json:"query"` - Total int `json:"total"` - Results []Result `json:"results"` - Engine string `json:"engine,omitempty"` - Error string `json:"error,omitempty"` - FromCache bool `json:"from_cache,omitempty"` -} - -// ── 全局 ── -var ( - httpClient *http.Client - ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" - acceptLg = "zh-CN,zh;q=0.9,en;q=0.8" - limiter *RateLimiter - cacheTTL int64 -) - -// ── PID 管理 ── - -func writePID(port int) { - pid := os.Getpid() - os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n%d", pid, port)), 0644) -} - -func readPID() (pid int, port int, ok bool) { - data, err := os.ReadFile(pidFile) - if err != nil { - return 0, 0, false - } - parts := strings.Split(strings.TrimSpace(string(data)), "\n") - if len(parts) < 2 { - return 0, 0, false - } - pid, _ = strconv.Atoi(parts[0]) - port, _ = strconv.Atoi(parts[1]) - if pid < 1 || port < 1 { - return 0, 0, false - } - proc, err := os.FindProcess(pid) - if err != nil { - return 0, 0, false - } - if runtime.GOOS == "windows" { - return pid, port, true - } - err = proc.Signal(syscall.Signal(0)) - return pid, port, err == nil -} - -func stopProcess(pid int) bool { - proc, err := os.FindProcess(pid) - if err != nil { - return false - } - if runtime.GOOS == "windows" { - return proc.Kill() == nil - } - err = proc.Signal(syscall.SIGTERM) - if err != nil { - return false - } - for i := 0; i < 50; i++ { - if err := proc.Signal(syscall.Signal(0)); err != nil { - os.Remove(pidFile) - return true - } - time.Sleep(100 * time.Millisecond) - } - proc.Signal(syscall.SIGKILL) - os.Remove(pidFile) - return true -} - -func checkPort(port int) bool { - ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - return true - } - ln.Close() - return false -} - -func doStart() { - if pid, _, ok := readPID(); ok { - fmt.Printf("已在运行 (PID: %d)\n", pid) - os.Exit(1) - } - if checkPort(*cliPort) { - fmt.Printf("端口 %d 已被占用\n", *cliPort) - os.Exit(1) - } - args := []string{} - for _, a := range os.Args[1:] { - if a != "-start" && a != "--start" { - args = append(args, a) - } - } - cmd := exec.Command(os.Args[0], args...) - cmd.Stdout = nil - cmd.Stderr = nil - cmd.Start() - fmt.Printf("已后台启动 (PID: %d)\n", cmd.Process.Pid) -} - -func doStop() { - pid, _, ok := readPID() - if !ok { - fmt.Println("未找到运行中的实例") - os.Exit(1) - } - if stopProcess(pid) { - fmt.Printf("已停止 (PID: %d)\n", pid) - } else { - fmt.Printf("停止失败 (PID: %d)\n", pid) - os.Exit(1) - } -} - -func doStatus() { - if pid, port, ok := readPID(); ok && checkPort(port) { - fmt.Printf("● 运行中 PID: %d 端口: %d\n", pid, port) - fmt.Printf(" 健康检查: http://localhost:%d/health\n", port) - fmt.Printf(" 统计数据: http://localhost:%d/stats\n", port) - fmt.Printf(" PID 文件: %s\n", pidFile) - } else { - fmt.Println("○ 未运行") - os.Remove(pidFile) - } -} - -func doRestart() { - if pid, _, ok := readPID(); ok { - stopProcess(pid) - time.Sleep(500 * time.Millisecond) - } - doStart() -} - -// ── recovery ── - -func recoverWrap(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer func() { - if err := recover(); err != nil { - log.Printf("PANIC: %v", err) - writeJSON(w, http.StatusInternalServerError, SearchResp{Error: "internal error"}) - } - }() - h(w, r) - } -} - -// ── 引擎实现 ── - -var engineNames = []string{"google", "bing", "baidu", "ddg"} - -func doGet(u string) (*http.Response, error) { - req, _ := http.NewRequest("GET", u, nil) - req.Header.Set("User-Agent", ua) - req.Header.Set("Accept-Language", acceptLg) - return httpClient.Do(req) -} - -func searchGoogle(q string, n int) []Result { - // Google 直连被反爬,改为 StartPage(背后是 Google 结果,无需 JS) - u := fmt.Sprintf("https://www.startpage.com/sp/search?query=%s&num=%d&lang=zh-CN", url.QueryEscape(q), n) - resp, err := doGet(u) - if err != nil || resp.StatusCode != 200 { - if resp != nil { - codeLog("google", resp.StatusCode) - resp.Body.Close() - } else { - codeLog("google", 0) - } - return nil - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - html := string(body) - - // StartPage 结果解析 - reBlock := regexp.MustCompile(`]*class="[^"]*result[^"]*"[^>]*>([\s\S]*?)\s*`) - reTitle := regexp.MustCompile(`]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)`) - reSnip := regexp.MustCompile(`]*class="[^"]*description[^"]*"[^>]*>([\s\S]*?)

    `) - - var out []Result - for _, m := range reBlock.FindAllStringSubmatch(html, -1) { - if len(out) >= n { - break - } - tm := reTitle.FindStringSubmatch(m[1]) - if tm == nil || strings.Contains(tm[1], "startpage.com") { - continue - } - title := clean(tm[2]) - if title == "" { - continue - } - snip := "" - if sm := reSnip.FindStringSubmatch(m[1]); sm != nil { - snip = clean(sm[1]) - } - out = append(out, Result{Title: title, URL: tm[1], Snippet: snip, Engine: "google"}) - } - return out -} - -func searchBing(q string, n int) []Result { - u := fmt.Sprintf("https://www.bing.com/search?q=%s&count=%d", url.QueryEscape(q), n) - resp, err := doGet(u) - if err != nil || resp.StatusCode != 200 { - if resp != nil { - resp.Body.Close() - codeLog("bing", resp.StatusCode) - } else { - codeLog("bing", 0) - } - return nil - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - html := string(body) - - var out []Result - for _, m := range reBingBlock.FindAllStringSubmatch(html, -1) { - if len(out) >= n { - break - } - tm := reBingTitle.FindStringSubmatch(m[1]) - if tm == nil { - continue - } - title := clean(tm[2]) - if title == "" { - continue - } - snip := "" - if sm := reBingSnip.FindStringSubmatch(m[1]); sm != nil { - snip = clean(sm[1]) - } - out = append(out, Result{Title: title, URL: tm[1], Snippet: snip, Engine: "bing"}) - } - return out -} - -func searchDDG(q string, n int) []Result { - u := fmt.Sprintf("https://lite.duckduckgo.com/lite/?q=%s", url.QueryEscape(q)) - resp, err := doGet(u) - if err != nil || resp.StatusCode != 200 { - if resp != nil { - resp.Body.Close() - codeLog("ddg", resp.StatusCode) - } else { - codeLog("ddg", 0) - } - return nil - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - html := string(body) - - var out []Result - for _, m := range reDDGLink.FindAllStringSubmatch(html, -1) { - if len(out) >= n { - break - } - link := decodeEntities(m[1]) - title := clean(m[2]) - if title == "" || strings.Contains(link, "duckduckgo.com") { - continue - } - out = append(out, Result{Title: title, URL: link, Snippet: "", Engine: "ddg"}) - } - return out -} - -func searchBaidu(q string, n int) []Result { - u := fmt.Sprintf("https://www.baidu.com/s?wd=%s&rn=%d", url.QueryEscape(q), n) - resp, err := doGet(u) - if err != nil || resp.StatusCode != 200 { - if resp != nil { - resp.Body.Close() - codeLog("baidu", resp.StatusCode) - } else { - codeLog("baidu", 0) - } - return nil - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - html := string(body) - - var out []Result - for _, m := range reBaiduBlock.FindAllStringSubmatch(html, -1) { - if len(out) >= n { - break - } - tm := reBaiduTitle.FindStringSubmatch(m[1]) - if tm == nil { - continue - } - link := tm[1] - if !strings.HasPrefix(link, "http") { - if dm := reBaiduDURL.FindStringSubmatch(m[1]); dm != nil { - link = dm[1] - } - } - title := clean(tm[2]) - if title == "" { - continue - } - snip := "" - if sm := reBaiduSnip.FindStringSubmatch(m[1]); sm != nil { - snip = clean(sm[1]) - } - out = append(out, Result{Title: title, URL: link, Snippet: snip, Engine: "baidu"}) - } - return out -} - -func codeLog(engine string, code int) { - if code == 0 { - log.Printf("%s: network error", engine) - return - } - switch code { - case 429: - log.Printf("%s: 429 rate limited", engine) - case 503: - log.Printf("%s: 503 unavailable", engine) - case 403: - log.Printf("%s: 403 blocked (可能需要验证码)", engine) - default: - log.Printf("%s: HTTP %d", engine, code) - } -} - -// ── 引擎索引 ── -var engineIdx = map[string]int{"google": 0, "bing": 1, "baidu": 2, "ddg": 3} - -// ── 工具 ── - -func clean(s string) string { - s = reHTMLTags.ReplaceAllString(s, "") - s = decodeEntities(s) - return strings.TrimSpace(strings.ReplaceAll(s, "\n", " ")) -} - -func decodeEntities(s string) string { - r := strings.NewReplacer( - " ", " ", "<", "<", ">", ">", "&", "&", - """, `"`, "'", "'", "'", "'", - "…", "…", "—", "—", "–", "–", - ) - return r.Replace(s) -} - -func recordEngine(eng string, ok bool) { - if idx, exists := engineIdx[eng]; exists { - if ok { - stats.EngineOK[idx].Add(1) - } else { - stats.EngineFail[idx].Add(1) - } - } -} - -func parallelSearch(q string, n int) []Result { - ch := make(chan []Result, 4) - engines := []func(string, int) []Result{searchGoogle, searchBing, searchBaidu, searchDDG} - for i, fn := range engines { - go func(idx int, f func(string, int) []Result) { - r := f(q, n) - if r == nil { - r = []Result{} - } - recordEngine(engineNames[idx], len(r) > 0) - ch <- r - }(i, fn) - } - seen := map[string]bool{} - var out []Result - for range engines { - for _, r := range <-ch { - key := strings.TrimRight(r.URL, "/") - if !seen[key] { - seen[key] = true - out = append(out, r) - } - } - } - if len(out) > n { - out = out[:n] - } - return out -} - -// ── Handler ── - -func handleSearch(w http.ResponseWriter, r *http.Request) { - stats.ReqTotal.Add(1) - if !limiter.Allow() { - stats.ReqRate.Add(1) - writeJSON(w, http.StatusTooManyRequests, SearchResp{Error: "rate limited"}) - return - } - - q := strings.TrimSpace(r.URL.Query().Get("q")) - if q == "" { - writeJSON(w, http.StatusBadRequest, SearchResp{Error: "missing q"}) - return - } - - engine := r.URL.Query().Get("engine") - if engine == "" { - engine = "all" - } - - n := 10 - fmt.Sscanf(r.URL.Query().Get("n"), "%d", &n) - if n < 1 { - n = 10 - } - if n > 20 { - n = 20 - } - - ck := fmt.Sprintf("%s|%s|%d", q, engine, n) - if v, ok := cache.Load(ck); ok { - ce := v.(cacheEntry) - if time.Now().Unix() < ce.ExpiresAt { - stats.CacheHit.Add(1) - writeJSON(w, http.StatusOK, SearchResp{ - Success: true, Query: q, Engine: engine, - Total: len(ce.Results), Results: ce.Results, FromCache: true, - }) - return - } - cache.Delete(ck) - } - - var results []Result - switch engine { - case "google": - results = searchGoogle(q, n) - recordEngine("google", len(results) > 0) - case "bing": - results = searchBing(q, n) - recordEngine("bing", len(results) > 0) - case "ddg": - results = searchDDG(q, n) - recordEngine("ddg", len(results) > 0) - case "baidu": - results = searchBaidu(q, n) - recordEngine("baidu", len(results) > 0) - case "all": - results = parallelSearch(q, n) - default: - writeJSON(w, http.StatusBadRequest, SearchResp{Error: "unknown engine: " + engine}) - return - } - - cache.Store(ck, cacheEntry{Results: results, ExpiresAt: time.Now().Add(time.Duration(cacheTTL) * time.Minute).Unix()}) - writeJSON(w, http.StatusOK, SearchResp{ - Success: true, Query: q, Engine: engine, - Total: len(results), Results: results, - }) -} - -func handleHealth(w http.ResponseWriter, r *http.Request) { - writeJSON(w, http.StatusOK, map[string]interface{}{ - "status": "ok", - "version": version, - "engines": []string{"google", "bing", "ddg", "baidu", "all"}, - "uptime": time.Now().UTC().Format(time.RFC3339), - }) -} - -func handleDebug(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query().Get("q") - if q == "" { - q = "hello" - } - engine := r.URL.Query().Get("engine") - if engine == "" { - engine = "google" - } - u := "" - switch engine { - case "google": - u = fmt.Sprintf("https://www.google.com/search?q=%s&num=3&hl=zh-CN", url.QueryEscape(q)) - case "bing": - u = fmt.Sprintf("https://www.bing.com/search?q=%s&count=3", url.QueryEscape(q)) - case "ddg": - u = fmt.Sprintf("https://lite.duckduckgo.com/lite/?q=%s", url.QueryEscape(q)) - case "baidu": - u = fmt.Sprintf("https://www.baidu.com/s?wd=%s&rn=3", url.QueryEscape(q)) - } - resp, err := doGet(u) - if err != nil { - writeJSON(w, http.StatusOK, map[string]string{"error": err.Error()}) - return - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - // 只返回前 2000 字符,方便查看结构 - html := string(body) - if len(html) > 2000 { - html = html[:2000] + "\n... truncated" - } - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Write([]byte(fmt.Sprintf("Status: %d\nEngine: %s\nURL: %s\n\n", resp.StatusCode, engine, u))) - w.Write([]byte(html)) -} - -func handleStats(w http.ResponseWriter, r *http.Request) { - uptime := time.Since(stats.StartTime).Round(time.Second).String() - total := stats.ReqTotal.Load() - cacheHit := stats.CacheHit.Load() - rate := stats.ReqRate.Load() - - cacheRate := 0.0 - if total > 0 { - cacheRate = float64(cacheHit) / float64(total) * 100 - } - - writeJSON(w, http.StatusOK, map[string]interface{}{ - "uptime": uptime, - "req_total": total, - "req_rated": rate, - "cache_hit": cacheHit, - "cache_rate": fmt.Sprintf("%.1f%%", cacheRate), - "engines": map[string]interface{}{ - "google": map[string]int64{"ok": stats.EngineOK[0].Load(), "fail": stats.EngineFail[0].Load()}, - "bing": map[string]int64{"ok": stats.EngineOK[1].Load(), "fail": stats.EngineFail[1].Load()}, - "baidu": map[string]int64{"ok": stats.EngineOK[2].Load(), "fail": stats.EngineFail[2].Load()}, - "ddg": map[string]int64{"ok": stats.EngineOK[3].Load(), "fail": stats.EngineFail[3].Load()}, - }, - "version": version, - }) -} - -func writeJSON(w http.ResponseWriter, status int, v interface{}) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("Access-Control-Allow-Origin", "*") - w.WriteHeader(status) - json.NewEncoder(w).Encode(v) -} - -func main() { - flag.Parse() - - if *cliVersion { - fmt.Printf("metona-search-proxy v%s go/%s\n", version, runtime.Version()) - return - } - if *cliStop { - doStop() - return - } - if *cliStatus { - doStatus() - return - } - if *cliStart { - doStart() - return - } - if *cliRestart { - doRestart() - return - } - - port := os.Getenv("PORT") - if port == "" { - port = fmt.Sprintf("%d", *cliPort) - } - portNum, _ := strconv.Atoi(port) - - if checkPort(portNum) { - log.Fatalf("端口 %s 已被占用。使用 metona-search-proxy -status 查看。", port) - } - - httpClient = &http.Client{ - Timeout: time.Duration(*cliTimeout) * time.Second, - Transport: &http.Transport{ - MaxIdleConns: 100, - MaxIdleConnsPerHost: 20, - IdleConnTimeout: 90 * time.Second, - }, - } - limiter = newRateLimiter(int64(*cliRate), int64(*cliBurst)) - cacheTTL = int64(*cliCache) - - // 后台清理过期缓存 - go cleanCacheLoop(30 * time.Second) - - writePID(portNum) - defer os.Remove(pidFile) - - mux := http.NewServeMux() - mux.HandleFunc("/search", recoverWrap(handleSearch)) - mux.HandleFunc("/health", recoverWrap(handleHealth)) - mux.HandleFunc("/stats", recoverWrap(handleStats)) - mux.HandleFunc("/debug", recoverWrap(handleDebug)) - - srv := &http.Server{ - Addr: ":" + port, - Handler: mux, - ReadTimeout: 10 * time.Second, - WriteTimeout: 15 * time.Second, - IdleTimeout: 60 * time.Second, - } - - go func() { - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - <-sigCh - log.Println("shutting down...") - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - srv.Shutdown(ctx) - }() - - log.Printf("metona-search-proxy v%s :%s engines: google/bing/ddg/baidu/all cache: %dm rate: %d/s", - version, port, *cliCache, *cliRate) - if err := srv.ListenAndServe(); err != http.ErrServerClosed { - log.Fatal(err) - } - log.Println("stopped") -} diff --git a/search-proxy/metona-search-proxy.service b/search-proxy/metona-search-proxy.service deleted file mode 100644 index e85895d..0000000 --- a/search-proxy/metona-search-proxy.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Metona Search Proxy - Google/Bing/DDG/Baidu API -After=network.target - -[Service] -Type=simple -ExecStart=/opt/metona-search-proxy/metona-search-proxy -Restart=always -RestartSec=5 -Environment=PORT=7899 - -[Install] -WantedBy=multi-user.target diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b5421f7..88f2c3b 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -45,13 +45,19 @@ import { handleReplaceInFiles, handleReadMultipleFiles, handleGit, - handleCompress + handleCompress, + handleDateTime, + handleCalculator, + handleRandom, + handleUUID, + handleJsonFormat, + handleHash } from './tool-handlers.js'; import { browserOpen, browserScreenshot, browserEvaluate, browserExtract, browserClick, browserType, browserScroll, browserClose, browserWait } from './browser.js'; import { startServer, stopServer, stopAllServers, callTool, getAllTools, getServerStatuses, refreshTools, setMCPTimeout } from './mcp-manager.js'; import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js'; import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js'; -import { setHTTPTimeout, setProxyUrl } from './tool-handlers-system.js'; +import { setHTTPTimeout } from './tool-handlers-system.js'; /** 工具结果摘要(用于日志面板) */ function summarizeResult(toolName: string, result: Record): string { @@ -75,6 +81,12 @@ function summarizeResult(toolName: string, result: Record): str case 'read_multiple_files': return `${result.total} 个文件`; case 'git': return `${result.action} ✓`; case 'compress': return `${result.action} → ${result.archive || result.destination}`; + case 'datetime': return `${(result as any).date || (result as any).iso}`; + case 'calculator': return `${(result as any).expression} = ${(result as any).result}`; + case 'random': return `${(result as any).type === 'pick' ? '🎲 ' + String((result as any).result) : String((result as any).result)}`; + case 'uuid': return `${(result as any).result}`; + case 'json_format': return `${(result as any).keys || 0} keys, ${(result as any).formatted_size}B`; + case 'hash': return `${(result as any).algorithm}: ${(result as any).hash?.slice(0, 16)}...`; default: return '完成'; } } @@ -187,6 +199,12 @@ export async function setupIPC(): Promise { case 'read_multiple_files':result = await handleReadMultipleFiles(args as { paths: string[]; max_chars_per_file?: number }); break; case 'git': result = await handleGit(args as { action: string; path?: string; files?: string[]; message?: string; branch?: string; remote?: string; remote_url?: string; count?: number; all?: boolean; staged?: boolean; new_branch?: boolean; delete_branch?: boolean; force?: boolean; url?: string }); break; case 'compress': result = await handleCompress(args as { action: string; path: string; destination?: string; format?: string }); break; + case 'datetime': result = handleDateTime(args as { format?: string; timezone?: string }); break; + case 'calculator': result = handleCalculator(args as { expression: string }); break; + case 'random': result = handleRandom(args as { type?: string; min?: number; max?: number; count?: number; items?: string[]; length?: number }); break; + case 'uuid': result = handleUUID(args as { count?: number }); break; + case 'json_format': result = handleJsonFormat(args as { json: string; indent?: number; sort_keys?: boolean }); break; + case 'hash': result = handleHash(args as { text: string; algorithm?: string }); break; // v5.1 Browser 控制(增强版) case 'browser_open': result = await browserOpen(args.url as string, args.wait_selector as string | undefined); break; case 'browser_screenshot': result = await browserScreenshot({ full_page: args.full_page as boolean, selector: args.selector as string }); break; @@ -231,22 +249,6 @@ export async function setupIPC(): Promise { return { success: true }; }); - // ── 代理设置(web_search/web_fetch 走代理)── - ipcMain.handle('proxy:set', (_, url: string) => { - setProxyUrl(url); - if (url) { - process.env.HTTP_PROXY = url; - process.env.HTTPS_PROXY = url; - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - sendLog('info', `🌐 代理已设置`, url); - } else { - delete process.env.HTTP_PROXY; - delete process.env.HTTPS_PROXY; - delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; - sendLog('info', `🌐 代理已禁用`); - } - }); - // ── Workspace IPC ── // 获取工作空间目录 diff --git a/src/main/menu.ts b/src/main/menu.ts index f7cb765..eddb58f 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -101,7 +101,7 @@ export function createMenu(): void { dialog.showMessageBox(mainWindow!, { type: 'info', title: '关于 Metona Ollama', - message: 'Metona Ollama Desktop v0.11.0', + message: 'Metona Ollama Desktop v0.11.1', detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama', icon: getIconPath() }); diff --git a/src/main/preload.ts b/src/main/preload.ts index 70bac6f..6214d02 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -3,10 +3,20 @@ */ import { contextBridge, ipcRenderer } from 'electron'; +import * as os from 'os'; contextBridge.exposeInMainWorld('metonaDesktop', { isDesktop: true, info: () => ipcRenderer.invoke('app:info'), + sys: { + homeDir: os.homedir(), + tmpDir: os.tmpdir(), + shell: process.env.SHELL || process.env.ComSpec || (process.platform === 'win32' ? 'cmd.exe' : 'bash'), + arch: os.arch(), + platform: os.platform(), + hostname: os.hostname(), + username: os.userInfo().username, + }, dialog: { openFile: (options?: unknown) => ipcRenderer.invoke('dialog:openFile', options), saveFile: (options?: unknown) => ipcRenderer.invoke('dialog:saveFile', options) @@ -20,8 +30,7 @@ contextBridge.exposeInMainWorld('metonaDesktop', { execute: (toolName: string, args: Record) => ipcRenderer.invoke('tool:execute', toolName, args), getConfig: () => ipcRenderer.invoke('tool:getConfig'), setAllowedDirs: (dirs: string[]) => ipcRenderer.invoke('tool:setAllowedDirs', dirs), - setTimeouts: (timeouts: { http?: number; mcp?: number }) => ipcRenderer.invoke('tool:setTimeouts', timeouts), - setProxy: (url: string) => ipcRenderer.invoke('proxy:set', url) + setTimeouts: (timeouts: { http?: number; mcp?: number }) => ipcRenderer.invoke('tool:setTimeouts', timeouts) }, notify: (title: string, body: string) => ipcRenderer.invoke('notify', title, body), window: { diff --git a/src/main/tool-handlers-system.ts b/src/main/tool-handlers-system.ts index 3705d03..5dd31e5 100644 --- a/src/main/tool-handlers-system.ts +++ b/src/main/tool-handlers-system.ts @@ -10,33 +10,6 @@ import { mainWindow } from './main.js'; import { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js'; import { getWorkspaceDir } from './workspace.js'; -// ── 代理支持 ── -let _proxyUrl = ''; - -function getProxyAgent(): any { - if (!_proxyUrl) return null; - - // 尝试 https-proxy-agent(需 npm install) - try { - const { HttpsProxyAgent } = require('https-proxy-agent'); - const a = new HttpsProxyAgent(_proxyUrl); - return a; - } catch { /* 未安装 */ } - - // 尝试 undici(Node.js 内置) - try { - const { ProxyAgent } = require('undici'); - return new ProxyAgent({ uri: _proxyUrl }); - } catch { /* 不可用 */ } - - return null; -} - -/** 设置 HTTP 代理(供 IPC 调用) */ -export function setProxyUrl(url: string): void { - _proxyUrl = url; -} - /** 当前工具命令进程(用于用户手动终止) */ let _toolProc: ReturnType | null = null; @@ -207,42 +180,10 @@ function buildFetchHeaders(url: string, useMobileUA: boolean): Record): Promise { const _headers = headers || buildFetchHeaders(url, false); - // 有代理时用 https.request(兼容 https-proxy-agent) - const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || ''; - if (proxy) { - try { - const { HttpsProxyAgent } = require('https-proxy-agent'); - return new Promise((resolve) => { - const mod = require(url.startsWith('https:') ? 'https' : 'http'); - const u = new URL(url); - const req = mod.request({ - hostname: u.hostname, port: u.port || (url.startsWith('https:') ? 443 : 80), - path: u.pathname + u.search, method: 'GET', - headers: { ..._headers, Host: u.hostname }, - agent: new HttpsProxyAgent(proxy), timeout, rejectUnauthorized: false, - }, (res: any) => { - let body = ''; - res.on('data', (d: Buffer) => body += d.toString()); - res.on('end', () => resolve({ - ok: res.statusCode >= 200 && res.statusCode < 400, - status: res.statusCode, statusText: res.statusMessage, - headers: new Headers(res.headers as Record), - text: () => Promise.resolve(body), - arrayBuffer: () => Promise.resolve(Buffer.from(body).buffer), - } as unknown as Response)); - }); - req.on('timeout', () => req.destroy()); - req.on('error', (e: Error) => { sendLog('warn', `🌐 代理请求失败`, `${url.slice(0,80)} | ${e.message.slice(0,100)}`); resolve(null); }); - req.end(); - }); - } catch { /* 代理包不可用,回退到原生 fetch */ } - } - - // 无代理 / 代理包不可用 → 原生 fetch const controller = new AbortController(); const tid = setTimeout(() => controller.abort(), timeout); try { @@ -496,7 +437,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number; /** * Web Search — 联网搜索(并行多引擎 + LRU 缓存 + 智能排序 + 时间过滤 + 摘要增强) - * 同时请求 Bing / 百度 / DuckDuckGo / Google,智能排序后返回 + * 同时请求 Bing / 百度 / 搜狗 / 360搜索,智能排序后返回 */ export async function handleWebSearch(params: { query: string; max_results?: number; time_range?: string; enhance_snippets?: boolean }): Promise { try { @@ -512,13 +453,11 @@ export async function handleWebSearch(params: { query: string; max_results?: num // ── 时间范围 → URL 参数映射 ── let bingFreshness = ''; - let googleTbs = ''; if (timeRange) { const tr = timeRange.toLowerCase(); - if (tr === 'day' || tr === '1d' || tr === '一天') { bingFreshness = 'Day'; googleTbs = 'qdr:d'; } - else if (tr === 'week' || tr === '1w' || tr === '一周') { bingFreshness = 'Week'; googleTbs = 'qdr:w'; } - else if (tr === 'month' || tr === '1m' || tr === '一月') { bingFreshness = 'Month'; googleTbs = 'qdr:m'; } - else if (tr === 'year' || tr === '1y' || tr === '一年') { googleTbs = 'qdr:y'; } + if (tr === 'day' || tr === '1d' || tr === '一天') { bingFreshness = 'Day'; } + else if (tr === 'week' || tr === '1w' || tr === '一周') { bingFreshness = 'Week'; } + else if (tr === 'month' || tr === '1m' || tr === '一月') { bingFreshness = 'Month'; } } // ── 1. 检查缓存 ── @@ -552,22 +491,23 @@ export async function handleWebSearch(params: { query: string; max_results?: num } }, { - name: 'ddg', weight: 70, + name: 'sogou', weight: 75, fetcher: async () => { const resp = await fetchWithTimeout( - `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}` + `https://www.sogou.com/web?query=${encodeURIComponent(query)}` ); - if (!resp?.ok) throw new Error(`DDG ${resp?.status}`); - return parseDDGResults(await resp.text(), maxResults); + if (!resp?.ok) throw new Error(`搜狗 ${resp?.status}`); + return parseSogouResults(await resp.text(), maxResults); } }, { - name: 'google', weight: 85, + name: '360搜索', weight: 75, fetcher: async () => { - const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=${maxResults}${googleTbs ? '&tbs=' + googleTbs : ''}`; - const resp = await fetchWithTimeout(googleUrl); - if (!resp?.ok) throw new Error(`Google ${resp?.status}`); - return parseGoogleResults(await resp.text(), maxResults); + const resp = await fetchWithTimeout( + `https://www.so.com/s?q=${encodeURIComponent(query)}` + ); + if (!resp?.ok) throw new Error(`360 ${resp?.status}`); + return parse360Results(await resp.text(), maxResults); } }, ]; @@ -769,31 +709,32 @@ function parseBaiduResults(html: string, maxResults: number): Array<{ title: str return results; } -/** 解析 DuckDuckGo Lite HTML 搜索结果 */ -function parseDDGResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { +/** 解析搜狗 HTML 搜索结果 */ +function parseSogouResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { const results: Array<{ title: string; url: string; snippet: string }> = []; - // DDG Lite 结果: 内的 + - // 尝试匹配:title 后跟 显示URL - // 以及 snippet - const blockRegex = /]*class="[^"]*result-snippet[^"]*"[^>]*>([\s\S]*?)<\/tr>/gi; + // 搜狗结果块:
    + const blockRegex = /]*class="[^"]*(?:vrwrap|rb)[^"]*"[^>]*>([\s\S]*?)(?=]*class="[^"]*(?:vrwrap|rb)[^"]*"|]*id="pagebar)/gi; let blockMatch; while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) { const block = blockMatch[1]; - // 提取 URL - const linkMatch = block.match(/]*href="(https?:\/\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/); + // 提取标题和 URL:标题 + const linkMatch = block.match(/]*href="(https?:\/\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/i); if (!linkMatch) continue; const url = decodeHTML(linkMatch[1].trim()); const title = decodeHTML(linkMatch[2].replace(/<[^>]+>/g, '').trim()); - // 跳过 DuckDuckGo 自身链接 - if (url.includes('duckduckgo.com')) continue; + // 跳过搜狗自身链接 + if (url.includes('sogou.com') || url.includes('sogo.com')) continue; - // 提取摘要 - const snippetMatch = block.match(/]*class="[^"]*result-snippet[^"]*"[^>]*>([\s\S]*?)<\/td>/); + // 提取摘要:

    + const snippetMatch = + block.match(/]*class="[^"]*star-wiki[^"]*"[^>]*>([\s\S]*?)<\/p>/i) || + block.match(/]*class="[^"]*space-txt[^"]*"[^>]*>([\s\S]*?)<\/div>/i) || + block.match(/]*class="[^"]*str_info[^"]*"[^>]*>([\s\S]*?)<\/p>/i); const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''; if (title && url.startsWith('http')) { @@ -801,17 +742,40 @@ function parseDDGResults(html: string, maxResults: number): Array<{ title: strin } } - // 回退:尝试匹配更通用的 DDG 结果格式 - if (results.length === 0) { - const altRegex = /]*href="(https?:\/\/[^"]*)"[^>]*class="[^"]*result-link[^"]*"[^>]*>([\s\S]*?)<\/a>/gi; - let altMatch; - while ((altMatch = altRegex.exec(html)) !== null && results.length < maxResults) { - const url = decodeHTML(altMatch[1].trim()); - const title = decodeHTML(altMatch[2].replace(/<[^>]+>/g, '').trim()); - if (url.includes('duckduckgo.com')) continue; - if (title && url.startsWith('http')) { - results.push({ title, url, snippet: '' }); - } + return results; +} + +/** 解析 360 搜索 HTML 搜索结果 */ +function parse360Results(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { + const results: Array<{ title: string; url: string; snippet: string }> = []; + + // 360 结果块:

  • + const blockRegex = /<(?:li[^>]*class="[^"]*res-list[^"]*"|div[^>]*class="[^"]*result[^"]*")[^>]*>([\s\S]*?)(?=<(?:li[^>]*class="[^"]*res-list|div[^>]*class="[^"]*result)|<\/ul>|<\/div>\s*<\/div>\s*$)/gi; + let blockMatch; + + while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) { + const block = blockMatch[1]; + + // 提取标题和 URL + const linkMatch = block.match(/]*href="(https?:\/\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/i); + if (!linkMatch) continue; + + const url = decodeHTML(linkMatch[1].trim()); + const title = decodeHTML(linkMatch[2].replace(/<[^>]+>/g, '').trim()); + + // 跳过 360 自身链接 + if (url.includes('so.com') && !url.includes('www.so.com/link')) continue; + + // 提取摘要:

    + const snippetMatch = + block.match(/]*class="[^"]*res-desc[^"]*"[^>]*>([\s\S]*?)<\/p>/i) || + block.match(/]*class="[^"]*res-rich[^"]*"[^>]*>([\s\S]*?)<\/div>/i) || + block.match(/]*class="[^"]*res-summary[^"]*"[^>]*>([\s\S]*?)<\/p>/i) || + block.match(/]*>([\s\S]*?)<\/dd>/i); + const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''; + + if (title && url.startsWith('http')) { + results.push({ title, url, snippet }); } } @@ -846,38 +810,6 @@ function parseBingResults(html: string, maxResults: number): Array<{ title: stri return results; } -/** 解析 Google HTML 搜索结果 */ -function parseGoogleResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { - const results: Array<{ title: string; url: string; snippet: string }> = []; - - // Google 结果块:

    ...
    ,内含

    title

    和摘要 - const blockRegex = /
    ]*>([\s\S]*?)(?=
    \s*<\/div>\s*<\/div>)/gi; - let blockMatch; - - while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) { - const block = blockMatch[1]; - - const titleMatch = block.match(/]*href="(https?:\/\/[^"]*)"[^>]*>[\s\S]*?]*>([\s\S]*?)<\/h3>/); - if (!titleMatch) continue; - - const url = titleMatch[1].trim(); - const title = decodeHTML(titleMatch[2].replace(/<[^>]+>/g, '').trim()); - - // 摘要:尝试多种模式 - const snippetMatch = - block.match(/]*data-sncf="[^"]*"[^>]*>([\s\S]*?)<\/div>/) || - block.match(/]*class="[^"]*st[^"]*"[^>]*>([\s\S]*?)<\/span>/) || - block.match(/]*class="[^"]*VwiC3b[^"]*"[^>]*>([\s\S]*?)<\/div>/); - const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''; - - if (title && url.startsWith('http') && !url.includes('google.com')) { - results.push({ title, url, snippet }); - } - } - - return results; -} - /** 简单 HTML 实体解码(用于搜索结果解析) */ function decodeHTML(text: string): string { return decodeHTMLEntities(text); @@ -999,3 +931,289 @@ export async function handleCompress(params: { action: string; path: string; des return { success: false, error: (err as Error).message }; } } + +// ── datetime: 获取系统精确时间 ── +export function handleDateTime(params: { format?: string; timezone?: string }): ToolResult { + try { + const now = new Date(); + const tz = params.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; + const fmt = params.format || 'full'; + + const iso = now.toISOString(); + const unixSec = Math.floor(now.getTime() / 1000); + const unixMs = now.getTime(); + + // 中文日期(无前导零月份) + const y = now.getFullYear(); + const m = now.getMonth() + 1; + const d = now.getDate(); + const weekdays = ['日', '一', '二', '三', '四', '五', '六']; + const weekday = `星期${weekdays[now.getDay()]}`; + const dateStr = `${y}年${m}月${d}日 ${weekday}`; + + // 24小时制时间 + const h = now.getHours(); + const min = now.getMinutes(); + const sec = now.getSeconds(); + const time24 = `${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; + + // 时段 + const period = h < 6 ? '凌晨' : h < 12 ? '上午' : h < 14 ? '中午' : h < 18 ? '下午' : '晚上'; + + // 人性化时间(12小时制 + 时段) + const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h; + const timeFriendly = `${period} ${h12}:${String(min).padStart(2, '0')}`; + + // 完整人性化字符串 + const friendly = `${y}年${m}月${d}日 ${weekday} ${timeFriendly}`; + + let result: Record; + + switch (fmt) { + case 'iso': + result = { iso, timezone: tz }; + break; + case 'unix': + result = { unix_seconds: unixSec, unix_milliseconds: unixMs, timezone: tz }; + break; + case 'date': + result = { date: dateStr, weekday, timezone: tz }; + break; + case 'time': + result = { time_24h: time24, period, time_friendly: timeFriendly, timezone: tz }; + break; + default: // full + result = { + iso, + unix_seconds: unixSec, + unix_milliseconds: unixMs, + date: dateStr, + weekday, + time_24h: time24, + period, + time_friendly: timeFriendly, + friendly, + timezone: tz, + year: y, + month: m, + day: d, + hour: h, + minute: min, + second: sec, + millisecond: now.getMilliseconds(), + day_of_week: now.getDay(), + }; + } + + return { success: true, ...result }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} + +// ── calculator: 安全数学计算器(纯 JS 递归下降解析,无 eval) ── +export function handleCalculator(params: { expression: string }): ToolResult { + try { + const expr = params.expression; + if (!expr || expr.length > 500) { + return { success: false, error: '表达式为空或过长(最大500字符)' }; + } + const result = safeCalc(expr); + return { success: true, expression: expr, result }; + } catch (err) { + return { success: false, error: (err as Error).message, expression: params.expression }; + } +} + +function safeCalc(expr: string): number { + expr = expr.replace(/\s+/g, ''); + if (!/^[\d+\-*/().%^]+$/.test(expr)) { + throw new Error('表达式包含非法字符'); + } + let pos = 0; + + function parseExpression(): number { + let left = parseTerm(); + while (pos < expr.length) { + if (expr[pos] === '+') { pos++; left += parseTerm(); } + else if (expr[pos] === '-') { pos++; left -= parseTerm(); } + else break; + } + return left; + } + + function parseTerm(): number { + let left = parsePower(); + while (pos < expr.length) { + if (expr[pos] === '*') { pos++; left *= parsePower(); } + else if (expr[pos] === '/') { pos++; const d = parsePower(); if (d === 0) throw new Error('除数不能为零'); left /= d; } + else if (expr[pos] === '%') { pos++; left %= parsePower(); } + else break; + } + return left; + } + + function parsePower(): number { + let left = parseUnary(); + while (pos < expr.length && expr[pos] === '*' && pos + 1 < expr.length && expr[pos + 1] === '*') { + pos += 2; + left = Math.pow(left, parseUnary()); + } + return left; + } + + function parseUnary(): number { + if (expr[pos] === '-') { pos++; return -parseAtom(); } + if (expr[pos] === '+') { pos++; return parseAtom(); } + return parseAtom(); + } + + function parseAtom(): number { + if (expr[pos] === '(') { + pos++; + const val = parseExpression(); + if (pos >= expr.length || expr[pos] !== ')') throw new Error('缺少右括号'); + pos++; + return val; + } + const start = pos; + while (pos < expr.length && /[\d.]/.test(expr[pos])) pos++; + if (start === pos) throw new Error(`意外字符: ${expr[pos] || 'EOF'}`); + const num = parseFloat(expr.slice(start, pos)); + if (isNaN(num)) throw new Error(`无效数字: ${expr.slice(start, pos)}`); + return num; + } + + const result = parseExpression(); + if (pos < expr.length) throw new Error(`表达式末尾有意外字符: ${expr.slice(pos)}`); + if (!isFinite(result)) throw new Error('计算结果为无穷大'); + return result; +} + +// ── random: 随机数生成 ── +export function handleRandom(params: { type?: string; min?: number; max?: number; count?: number; items?: string[]; length?: number }): ToolResult { + try { + const type = params.type || 'int'; + + switch (type) { + case 'int': { + const min = params.min ?? 0; + const max = params.max ?? 100; + if (min > max) return { success: false, error: 'min 不能大于 max' }; + const count = Math.min(params.count ?? 1, 100); + if (count === 1) { + return { success: true, type: 'int', result: Math.floor(Math.random() * (max - min + 1)) + min, range: [min, max] }; + } + const results: number[] = []; + for (let i = 0; i < count; i++) results.push(Math.floor(Math.random() * (max - min + 1)) + min); + return { success: true, type: 'int', results, count, range: [min, max] }; + } + case 'float': { + const min = params.min ?? 0; + const max = params.max ?? 1; + if (min > max) return { success: false, error: 'min 不能大于 max' }; + const count = Math.min(params.count ?? 1, 100); + if (count === 1) { + return { success: true, type: 'float', result: Number((Math.random() * (max - min) + min).toFixed(6)), range: [min, max] }; + } + const results: number[] = []; + for (let i = 0; i < count; i++) results.push(Number((Math.random() * (max - min) + min).toFixed(6))); + return { success: true, type: 'float', results, count, range: [min, max] }; + } + case 'pick': { + const items = params.items; + if (!items || items.length === 0) return { success: false, error: 'pick 类型需要提供 items 数组' }; + const count = Math.min(params.count ?? 1, items.length); + // Fisher-Yates 部分洗牌 + const pool = [...items]; + const picked: string[] = []; + for (let i = 0; i < count; i++) { + const idx = Math.floor(Math.random() * pool.length); + picked.push(pool[idx]); + pool.splice(idx, 1); + } + return { success: true, type: 'pick', result: count === 1 ? picked[0] : picked, from: items.length, count }; + } + case 'string': { + const length = Math.min(params.length ?? 8, 256); + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let s = ''; + for (let i = 0; i < length; i++) s += chars.charAt(Math.floor(Math.random() * chars.length)); + // 使用 crypto 增强随机性 + const buf = require('crypto').randomBytes(Math.ceil(length * 0.75)); + let bIdx = 0; + let result = ''; + for (let i = 0; i < length; i++) { + const r = buf[bIdx++] || Math.floor(Math.random() * 256); + if (bIdx >= buf.length) bIdx = 0; + result += chars.charAt(r % chars.length); + } + return { success: true, type: 'string', result, length, charset: 'alphanumeric' }; + } + default: + return { success: false, error: `未知类型: ${type}。支持: int / float / pick / string` }; + } + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} + +// ── uuid: 生成 UUID v4 ── +export function handleUUID(params: { count?: number }): ToolResult { + try { + const crypto = require('crypto'); + const count = Math.min(params.count ?? 1, 20); + if (count === 1) { + return { success: true, result: crypto.randomUUID() }; + } + const results: string[] = []; + for (let i = 0; i < count; i++) results.push(crypto.randomUUID()); + return { success: true, results, count }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} + +// ── json_format: JSON 格式化 + 验证 ── +export function handleJsonFormat(params: { json: string; indent?: number; sort_keys?: boolean }): ToolResult { + try { + const parsed = JSON.parse(params.json); + const indent = params.indent ?? 2; + let formatted: string; + if (params.sort_keys) { + formatted = JSON.stringify(sortObjectKeys(parsed), null, indent); + } else { + formatted = JSON.stringify(parsed, null, indent); + } + const size = Buffer.byteLength(formatted, 'utf-8'); + return { success: true, formatted, original_size: params.json.length, formatted_size: size, keys: Object.keys(parsed).length }; + } catch (err) { + return { success: false, error: `JSON 解析失败: ${(err as Error).message}`, input_preview: params.json.slice(0, 200) }; + } + + function sortObjectKeys(obj: any): any { + if (Array.isArray(obj)) return obj.map(sortObjectKeys); + if (obj !== null && typeof obj === 'object') { + const sorted: Record = {}; + for (const k of Object.keys(obj).sort()) sorted[k] = sortObjectKeys(obj[k]); + return sorted; + } + return obj; + } +} + +// ── hash: 哈希计算 ── +export function handleHash(params: { text: string; algorithm?: string }): ToolResult { + try { + const crypto = require('crypto'); + const algo = (params.algorithm || 'sha256').toLowerCase().replace('-', ''); + const validAlgos = ['md5', 'sha1', 'sha256', 'sha384', 'sha512']; + if (!validAlgos.includes(algo)) { + return { success: false, error: `不支持的算法: ${algo}。支持: ${validAlgos.join(', ')}` }; + } + const hash = crypto.createHash(algo).update(params.text, 'utf-8').digest('hex'); + return { success: true, algorithm: algo, hash, input_length: params.text.length }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } +} diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index e261abb..3ba5833 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -24,7 +24,7 @@ export { handleReadMultipleFiles, } from './tool-handlers-fs.js'; -// 系统与网络操作(6 个) +// 系统与网络操作(8 个) export { handleRunCommand, killToolProcess, @@ -32,6 +32,12 @@ export { handleWebSearch, handleDownloadFile, handleCompress, + handleDateTime, + handleCalculator, + handleRandom, + handleUUID, + handleJsonFormat, + handleHash, } from './tool-handlers-system.js'; // Git 操作(1 个) diff --git a/src/renderer/components/input-area.ts b/src/renderer/components/input-area.ts index 02fac6d..8bc20ee 100644 --- a/src/renderer/components/input-area.ts +++ b/src/renderer/components/input-area.ts @@ -435,6 +435,7 @@ async function handleRetry(): Promise { } renderMessages(); await saveCurrentSession(); + if (loopStats?.prompt_eval_count) updateCtxRemain(loopStats.prompt_eval_count); } }); } catch (err) { @@ -594,6 +595,33 @@ function stopGeneration(): void { if (abortController) abortController.abort(); } +/** 更新剩余上下文显示 */ +function updateCtxRemain(promptEvalCount?: number): void { + const el = document.querySelector('#ctxRemain') as HTMLElement; + if (!el) return; + const total = state.get(KEYS.NUM_CTX, 0); + if (!total || !promptEvalCount || promptEvalCount <= 0) { + el.style.display = 'none'; + return; + } + const remain = total - promptEvalCount; + if (remain <= 0) { + el.style.display = 'inline-flex'; + el.textContent = '⚠ 上下文已满'; + el.style.color = 'var(--danger, #e74c3c)'; + } else { + el.style.display = 'inline-flex'; + el.style.color = ''; + const fmt = remain >= 1024 ? `${(remain / 1024).toFixed(0)}K` : String(remain); + el.textContent = `剩余 ${fmt}`; + } +} + +export function clearCtxRemain(): void { + const el = document.querySelector('#ctxRemain') as HTMLElement; + if (el) el.style.display = 'none'; +} + /** Token 预算比例:文件内容最多占用上下文窗口的比例 */ const FILE_TOKEN_BUDGET_RATIO = 0.3; @@ -1006,6 +1034,8 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio } renderMessages(); await saveCurrentSession(); + // 更新剩余上下文 + if (loopStats?.prompt_eval_count) updateCtxRemain(loopStats.prompt_eval_count); } }); } catch (err) { diff --git a/src/renderer/components/model-bar.ts b/src/renderer/components/model-bar.ts index 36a9635..a4571af 100644 --- a/src/renderer/components/model-bar.ts +++ b/src/renderer/components/model-bar.ts @@ -8,6 +8,7 @@ import { formatSize } from '../utils/utils.js'; import { OllamaAPI } from '../api/ollama.js'; import { ChatDB } from '../db/chat-db.js'; import { logModel, logDebug, logError, logWarn } from '../services/log-service.js'; +import { clearCtxRemain } from './input-area.js'; import type { OllamaModelDetail, ModelCaps } from '../types.js'; let modelSelectEl: HTMLSelectElement; @@ -159,11 +160,13 @@ async function filterEmbedModels(models: Array<{ name: string }>): Promise } async function checkModelCapability(modelName: string): Promise { + // 切换模型时清除旧上下文剩余显示 + clearCtxRemain(); + if (modelCapabilityCache.has(modelName)) { const cached = modelCapabilityCache.get(modelName)!; state.set(KEYS.NUM_CTX, cached.contextLength); - const displayEl = document.querySelector('#displayNumCtx'); - if (displayEl) displayEl.textContent = `${cached.contextLength.toLocaleString()} tokens`; + updateCtxTotal(cached.contextLength); applyCapability(modelName, cached); return; } @@ -200,9 +203,8 @@ async function checkModelCapability(modelName: string): Promise { const db = state.get(KEYS.DB); if (db) await db.saveSetting('numCtx', contextLength); - // 更新设置面板显示 - const displayEl = document.querySelector('#displayNumCtx'); - if (displayEl) displayEl.textContent = `${contextLength.toLocaleString()} tokens`; + // 更新模型栏上下文长度显示 + updateCtxTotal(contextLength); applyCapability(modelName, caps); } catch (err) { @@ -275,3 +277,16 @@ export function isToolCallingSupported(): boolean { const caps = modelCapabilityCache.get(model); return caps?.tools === true; } + +function formatCtx(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1024) return `${(n / 1024).toFixed(0)}K`; + return String(n); +} + +function updateCtxTotal(contextLength: number): void { + const el = document.querySelector('#ctxTotal') as HTMLElement; + if (!el) return; + el.style.display = 'inline-flex'; + el.textContent = formatCtx(contextLength); +} diff --git a/src/renderer/components/settings-modal.ts b/src/renderer/components/settings-modal.ts index 02d86d3..7ea83b1 100644 --- a/src/renderer/components/settings-modal.ts +++ b/src/renderer/components/settings-modal.ts @@ -39,18 +39,6 @@ export function initSettingsModal(): void { }, 500); document.querySelector('#inputServerUrl')!.addEventListener('input', saveServerUrl); - // 代理设置 - const inputProxyUrl = document.querySelector('#inputProxyUrl') as HTMLInputElement; - if (inputProxyUrl) { - const saveProxyUrl = debounce(async () => { - const db = state.get(KEYS.DB); - const url = inputProxyUrl.value.trim(); - if (db) await db.saveSetting('proxyUrl', url); - logSetting('代理', url || '已禁用'); - }, 500); - inputProxyUrl.addEventListener('input', saveProxyUrl); - } - const tempSlider = document.querySelector('#inputTemperature') as HTMLInputElement; const tempDisplay = document.querySelector('#tempValue')!; tempSlider.addEventListener('input', () => { diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index dc0b014..85c67df 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -834,7 +834,7 @@ function renderToolCalls(): void {
    AI 对话中自动调用工具,结果在此展示
    -
    共 38 个内置工具 · MCP 动态扩展
    +
    共 44 个内置工具 · MCP 动态扩展
    `; return; diff --git a/src/renderer/index.html b/src/renderer/index.html index 8ea0a6a..ba38dd9 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -28,7 +28,7 @@
    Metona Ollama - v0.11.0 + v0.11.1
    + +
    @@ -272,16 +274,6 @@
  • - - -

    留空则不使用代理。支持 HTTP 代理,例如 Clash(http://127.0.0.1:7890)。修改后需重启应用生效。

    -
    -
    - -
    - 24576 tokens -
    -

    选择模型时自动从模型元数据中获取实际上下文长度。

    低值更精确稳定,高值更有创意多样(0 = 确定性,2 = 最大随机)

    @@ -420,9 +412,9 @@ diff --git a/src/renderer/main.ts b/src/renderer/main.ts index 135e5f2..08e5a09 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -17,6 +17,7 @@ import { initHeader, checkConnection } from './components/header.js'; import { initModelBar, loadModels, setSelectedModel } from './components/model-bar.js'; import { initChatArea, renderMessages, clearMessages, enableAutoScroll } from './components/chat-area.js'; import { initInputArea } from './components/input-area.js'; +import { clearCtxRemain } from './components/input-area.js'; import { initSettingsModal, closeSettingsModal } from './components/settings-modal.js'; import { initHistoryModal, closeHistoryModal } from './components/history-modal.js'; import { initMemoryModal } from './components/memory-modal.js'; @@ -254,6 +255,9 @@ async function startNewSession(): Promise { state.set(KEYS.CURRENT_SESSION, session); logInfo('新建会话'); + // 清除剩余上下文显示 + clearCtxRemain(); + // ── 强制中断正在进行的生成 ── const abortController = state.get(KEYS.ABORT_CONTROLLER); if (abortController) { @@ -394,21 +398,6 @@ async function init(): Promise { (document.querySelector('#inputMCPTimeout') as HTMLInputElement).value = mcpTimeout >= 0 ? String(mcpTimeout) : ''; logInit(`超时设置: HTTP=${httpTimeout>=0?httpTimeout:30}s 流式=${streamTimeout>=0?streamTimeout:300}s MCP=${mcpTimeout>=0?mcpTimeout:60}s`); - // ── 代理设置 ── - const proxyUrl = await db.getSetting('proxyUrl', ''); - if (proxyUrl) { - try { - await fetch(proxyUrl, { method: 'HEAD', signal: AbortSignal.timeout(2000) }); - logInit(`代理: ${proxyUrl} ✓`); - } catch { logInit(`代理: ${proxyUrl} (无法连接,但已设置)`); } - state.set('proxyUrl', proxyUrl); - // 通知主进程设置环境变量 - if (bridge?.tool?.setProxy) { - await bridge.tool.setProxy(proxyUrl); - } - } - (document.querySelector('#inputProxyUrl') as HTMLInputElement).value = proxyUrl; - // ── Tool Calling 设置(永久开启,不可关闭)── state.set('toolCallingEnabled', true); const runCommandMode = await db.getSetting('runCommandMode', 'confirm') as 'auto' | 'confirm' | 'disabled'; diff --git a/src/renderer/public/AGENT.md b/src/renderer/public/AGENT.md index 6026cc7..aece801 100644 --- a/src/renderer/public/AGENT.md +++ b/src/renderer/public/AGENT.md @@ -1,26 +1,25 @@ -# AGENT.md — Agent 行为准则 +# 行为准则 -你是具备工具调用能力的 AI 助手。你有文件操作、命令执行、四引擎联网搜索(Bing+百度+DuckDuckGo+Google 并行)、网页抓取(支持自动重试+移动端UA+SPA浏览器渲染回退)、浏览器控制、Git、记忆管理等工具可用。 +你是具备工具调用能力的 AI 助手。你可以读写文件、执行命令、搜索网络、浏览网页、管理记忆等。 ## 核心规则 -1. **直接调用工具**:不要只说"我来帮你xxx"然后结束。说了要做就必须调用工具。 -2. **不要过早停止**:工具调用后如果信息还不完整,必须继续调用工具或给出下一步计划。不要在2-3轮后就给出不完整的回答。如果任务未完成,不要说"我已经完成了"。 -3. **多步任务逐步完成**:搜索→抓取→回答。抓取失败时直接用搜索结果,不要卡住。 -4. **需要最新信息时先搜索**:版本号、新闻、日期等必须用工具获取。**【严禁使用知识库日期】**——你不是活在一个特定日期,以搜索结果为唯一可信来源。 -5. **出错换方法重试**:最多 2 次,同一 URL 连续失败后换其他途径。 -6. **不重复调用**:相同参数的同一工具不重复执行。 -7. **善用搜索结果**:snippet 已有关键信息,优先从中提取答案。 +1. **直接行动**:不要只说"我来帮你xxx"然后结束。说了要做就必须调用工具。 +2. **不要过早停止**:工具调用后如果信息还不完整,继续调用工具或给出下一步计划。不要在信息不足时给出不完整的回答。 +3. **多步任务逐步完成**:搜索→获取详情→分析→回答。某个途径失败时换其他方法,不要卡住。 +4. **需要最新信息时优先搜索**:版本号、新闻、日期等以搜索结果为唯一可信来源,不要依赖训练数据中的日期。 +5. **出错换方法重试**:同一途径连续失败后换其他方法。 +6. **不重复调用**:相同参数的工具不重复执行。 +7. **善用已有信息**:优先从已获取的结果中提取答案,减少不必要的重复调用。 ## 链式调用模式 -- `web_search` → `web_fetch`(搜索后抓取详情) -- `list_directory` → `read_file`(浏览目录后读文件) -- `search_files` → `edit_file`(搜索后修改) +- 搜索 → 获取详情(先搜再深挖) +- 浏览目录 → 读取文件(先看结构再读内容) +- 搜索定位 → 精确修改(先找再改) ## 重要约束 -- **严禁猜 URL**:所有 URL 必须来自搜索结果。 -- **上传文件已在对话中**:用户上传的文件内容已在消息中,无需再用 read_file 读取。 -- **SPA 页面自动处理**:web_fetch 遇到内容过短的 SPA 页面会自动升级到浏览器渲染,无需手动用 browser_open。GitHub releases、npm 等页面可直接通过 web_fetch 获取。 +- **严禁编造 URL**:所有 URL 必须来自搜索结果或用户提供。 +- **上传文件已在对话中**:用户上传的文件内容已在当前消息中,无需再读取。 - 永远不要只输出"我将执行xxx"然后结束。 diff --git a/src/renderer/public/SOUL.md b/src/renderer/public/SOUL.md index a5ebe7b..9505477 100644 --- a/src/renderer/public/SOUL.md +++ b/src/renderer/public/SOUL.md @@ -1,18 +1,18 @@ -# Metona Ollama Agent +# Metona > "推理以求明晰,理解以促行动。" ## 身份 - **名称**: Metona -- **角色**: Metona Ollama 桌面 AI 助手 -- **核心能力**: ReAct Agent Loop v0.11.0 + 38 个增强工具 + 记忆系统 + 技能自动生成 + 智能上下文压缩 +- **角色**: AI 助手 +- **定位**: 具备工具调用能力的智能助手,通过思考-行动-观察-反思循环完成任务。 ## 性格与语气 - 智识上的好奇心 — 主动澄清模糊需求 - 精确而不迂腐 — 重准确,不牺牲清晰 -- 温暖而专业 — 平易近人 +- 温暖而专业 — 平易近人,偶尔机锋 - 谦逊且有分寸 — 证据不足就说"我不知道" - 行动导向 — 有工具就用,不空谈 @@ -30,12 +30,10 @@ ## 行为准则 ### 铁律 — 绝对不可违反 -- **禁止编造**: 无法获取真实数据时如实报告 -- **禁止静默失败**: 工具调用失败必须如实报告 +- **禁止编造**: 无法获取真实数据时如实报告,标注不确定性 +- **禁止静默失败**: 工具调用失败、任务受阻必须如实报告 ### 偏好 - 所有回复使用中文,技术标识符保留英文 - 完成要求后停止,不扩展范围 - 先理解,再行动 - -> 💡 在工作空间目录创建自己的 SOUL.md 即可自定义 AI 人格,下一轮对话立即生效。 diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index c81cd7d..98b7ef6 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -39,12 +39,20 @@ function getOSEnvironment() { const bridge = (window as any).metonaDesktop; const isDesktop = bridge?.isDesktop || false; + // 通过 preload 暴露的真实系统信息(同步,无 IPC 开销) + const sys = bridge?.sys; + const realHomeDir = sys?.homeDir || ''; + const realShell = sys?.shell || ''; + const realArch = sys?.arch || (navigator.platform || 'unknown'); + const realUser = sys?.username || ''; + return { os: isWin ? 'Windows' : isMac ? 'macOS' : 'Linux', - platform: isDesktop ? (bridge.info ? 'Electron Desktop' : 'Desktop') : 'Browser', - arch: navigator.platform || 'unknown', - shell: isWin ? 'cmd.exe / PowerShell' : 'bash', - homeDir: isWin ? 'C:\\Users\\<用户名>' : '/home/<用户名>', + platform: isDesktop ? 'Electron Desktop' : 'Browser', + arch: realArch, + shell: realShell || (isWin ? 'cmd.exe / PowerShell' : 'bash'), + homeDir: realHomeDir || (isWin ? 'C:\\Users\\<用户名>' : '/home/<用户名>'), + username: realUser, lineEnding: isWin ? 'CRLF (\\r\\n)' : 'LF (\\n)', pathSep: isWin ? '\\ (反斜杠)' : '/ (正斜杠)', }; @@ -65,8 +73,8 @@ const TOOL_MAX_RESULT_SIZE: Record = { browser_evaluate: 8000, }; -/** v4.1: 工具并行执行 — 依赖检测用 */ -const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch', 'edit_file', 'move_file', 'delete_file']); +/** v4.1: 工具并行执行 — 依赖检测。只有确实依赖前序工具输出结果的才串行 */ +const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch']); // web_fetch 可能依赖 web_search 的结果 URL /** 始终可并行的只读/独立工具(无数据依赖,永远可以同批执行) */ const ALWAYS_PARALLEL = new Set([ @@ -74,6 +82,8 @@ const ALWAYS_PARALLEL = new Set([ 'web_search', 'browser_screenshot', 'browser_extract', 'browser_evaluate', 'memory_search', 'session_list', 'session_read', 'skill_list', 'skill_view', 'diff_files', 'git', + 'datetime', 'calculator', + 'random', 'uuid', 'json_format', 'hash', ]); /** 工具名白名单:用于文本解析兜底时过滤非法工具名 */ @@ -83,7 +93,9 @@ 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' + 'skill_list', 'skill_view', + 'datetime', 'calculator', + 'random', 'uuid', 'json_format', 'hash' ]); /** @@ -816,10 +828,15 @@ Shell: ${osInfo.shell} /最终答案[::]/, /最终回答[::]/, /总结[::]/, + /任务完成[!!]?/, + /以上(是|为)[我对]?/, + /以上就是/, ]; - const isFinalAnswer = toolCalls.length === 0 - && content.length > 50 - && FINAL_PATTERNS.some(p => p.test(content)); + // 有工具历史 + 回复较长(>300字)且无工具调用 → 大概率是最终回答 + const isFinalAnswer = toolCalls.length === 0 && content.length > 50 && ( + FINAL_PATTERNS.some(p => p.test(content)) || + (allToolRecords.length > 0 && content.length > 300) + ); // 处理空响应:如果之前有工具调用但模型返回空内容,不立即结束 if (toolCalls.length === 0 && !content.trim() && loopCount > 1 && allToolRecords.length > 0) { @@ -838,13 +855,17 @@ Shell: ${osInfo.shell} // ── 反过早结束:如果之前有工具调用,但模型返回的内容不像最终回答,引导继续 ── if (allToolRecords.length > 0 && !isFinalAnswer && loopCount < maxLoops - 1) { const contentLower = content.toLowerCase(); + // "还在思考中"的典型信号:短内容 + 思考动词 const isThinking = contentLower.includes('让我') || contentLower.includes('看看') || - contentLower.includes('观察') || contentLower.includes('分析') || contentLower.includes('检查'); - const isContinuation = content.length < 300 && (isThinking || contentLower.includes('接下来') || contentLower.includes('继续')); + contentLower.includes('观察') || (contentLower.includes('分析') && !contentLower.includes('分析结果')); + const isMidSentence = contentLower.endsWith('...') || contentLower.endsWith('等等') || + contentLower.includes('让我再'); + const isContinuation = content.length < 300 && isThinking && + (isMidSentence || contentLower.includes('接下来') || contentLower.includes('继续')); - if (isContinuation || content.length < 100) { + if (isContinuation) { logWarn('模型可能过早停止,注入继续提示', `${content.length}字`); - messages.pop(); // 移除空/过渡性的 assistant 消息 + messages.pop(); messages.push({ role: 'user', content: '请继续完成任务。你可以调用工具获取更多信息,或者根据已有结果给出完整的最终回答。如果任务已完成,请明确说出"任务完成"或"最终回答"。' @@ -878,19 +899,21 @@ Shell: ${osInfo.shell} return; } - // 跨轮次重复检测:仅当连续两轮调用完全相同且全部成功时才终止(失败的允许重试) + // 跨轮次重复检测:连续两轮调用完全相同且全部成功 → 注入警告而不强制终止 const currentLoopKeys = toolCalls.map(c => getToolCacheKey(c.function.name, c.function.arguments)).sort(); const currentKeysStr = JSON.stringify(currentLoopKeys); const prevKeysStr = JSON.stringify([...prevLoopSuccessKeys].sort()); if (currentKeysStr === prevKeysStr && currentLoopKeys.length > 0) { - // 检查是否有上一轮失败的工具——如果有,不终止,允许重试 const hasFailedInPrev = allToolRecords .filter(r => prevLoopSuccessKeys.includes(getToolCacheKey(r.name, r.arguments))) .some(r => r.status !== 'success'); if (!hasFailedInPrev) { - logWarn('检测到连续两轮工具调用完全相同且全部成功,终止 ReAct Loop', currentKeysStr.slice(0, 200)); - callbacks.onDone(content || '(检测到重复工具调用,已自动停止)', allToolRecords, makeStats()); - return; + logWarn('检测到连续两轮工具调用完全相同且全部成功,注入提醒而非终止'); + messages.push({ + role: 'user', + content: '⚠️ 检测到你连续调用了与上一轮完全相同的工具。请基于已有结果给出最终回答,或者调用不同的工具获取新信息。如果任务已完成,请给出最终回答。' + }); + continue; } logInfo('检测到重复调用但上一轮有失败,允许重试'); } @@ -1045,6 +1068,22 @@ Shell: ${osInfo.shell} } } + // P0-1: 硬限制工具消息数量 — 保留最近 40 条,删除旧的防止上下文无限膨胀 + { + const toolIndices: number[] = []; + for (let i = 0; i < messages.length; i++) { + if (messages[i].role === 'tool') toolIndices.push(i); + } + if (toolIndices.length > 40) { + const toRemove = toolIndices.slice(0, toolIndices.length - 40); + // 从后往前删,避免索引偏移 + for (let i = toRemove.length - 1; i >= 0; i--) { + messages.splice(toRemove[i], 1); + } + logInfo(`工具消息裁剪: ${toRemove.length} 条旧结果已移除 (保留最近 40 条)`); + } + } + // 更新跨轮去重:仅跟踪本轮成功的工具调用 prevLoopSuccessKeys = allToolRecords .filter(r => r.status === 'success') @@ -1056,24 +1095,23 @@ Shell: ${osInfo.shell} ).join('; '); saveTrace(traceStep); - // P2-2: 增量记忆提取 — 每 20 轮自动触发轻量级记忆提取 + // P2-2: 增量记忆提取 — 每 20 轮 fire-and-forget,不阻塞主循环 if (isMemoryEnabled() && loopCount > 1 && loopCount % 20 === 0 && messages.length >= 10) { - try { - const { extractMemoriesFromConversation } = await import('./memory-manager.js'); + import('./memory-manager.js').then(({ extractMemoriesFromConversation }) => { const recentMsgs = messages.slice(-30).filter(m => m.role === 'user' || m.role === 'assistant'); - await extractMemoriesFromConversation( + extractMemoriesFromConversation( recentMsgs.map(m => ({ role: m.role, content: m.content })), currentSession?.title - ); - logInfo('增量记忆提取完成', `第 ${loopCount} 轮`); - } catch { /* 不阻塞 */ } + ).then(() => logInfo('增量记忆提取完成', `第 ${loopCount} 轮`)) + .catch(() => {}); + }).catch(() => {}); } // 保存本轮工具调用,供下一轮 onNewIteration 使用 prevToolCalls = toolCalls; - // P1-3: 增量工具结果截断 — 超过 10 轮的旧工具结果自动截断到 500 字符 - if (loopCount > 10 && loopCount % 5 === 0) { + // P1-3: 增量工具结果截断 — 超过 10 轮的旧工具结果每 3 轮截断到 500 字符 + if (loopCount > 10 && loopCount % 3 === 0) { const truncateBefore = messages.length - 15; for (let i = 0; i < messages.length && i < truncateBefore; i++) { const m = messages[i]; diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index 47ddb1d..a65fe39 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -336,7 +336,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ type: 'function', function: { name: 'web_search', - description: 'Search the web using 4 engines in parallel (Bing + Baidu + DuckDuckGo + Google). Results are intelligently ranked by reachability + engine weight + snippet quality. 5-minute cache. Supports time range filtering (day/week/month/year). Short snippets are auto-enhanced via web_fetch. Use when you need current, time-sensitive, or factual information.', + description: 'Search the web using 4 engines in parallel (Bing + Baidu + Sogou + 360). Results are intelligently ranked by reachability + engine weight + snippet quality. 5-minute cache. Supports time range filtering (day/week/month/year). Short snippets are auto-enhanced via web_fetch. Use when you need current, time-sensitive, or factual information.', parameters: { type: 'object', required: ['query'], @@ -618,6 +618,96 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ description: 'Close the agent browser and free resources.', parameters: { type: 'object', properties: {} } } + }, + { + type: 'function', + function: { + name: 'datetime', + description: 'Get the precise current system time. Returns ISO timestamp, Unix time (seconds and milliseconds), locale-formatted date/time, timezone, and individual components (year/month/day/hour/minute/second/millisecond/day_of_week). Use format parameter to request specific output: "iso" / "unix" / "date" / "time" / "full" (default: full).', + parameters: { + type: 'object', + properties: { + format: { type: 'string', enum: ['full', 'iso', 'unix', 'date', 'time'], description: 'Output format. Default: full (all fields).' }, + timezone: { type: 'string', description: 'IANA timezone name (e.g., "Asia/Shanghai", "America/New_York"). Default: system timezone.' } + } + } + } + }, + { + type: 'function', + function: { + name: 'calculator', + description: 'Safely evaluate a mathematical expression. Supports + - * / ** % () and floating-point numbers. Uses a pure JS recursive descent parser — no eval(), CSP-safe. Returns the numeric result.', + parameters: { + type: 'object', + required: ['expression'], + properties: { + expression: { type: 'string', description: 'The mathematical expression to evaluate (e.g., "(3 + 5) * 2 ** 3"). Max 500 characters.' } + } + } + } + }, + { + type: 'function', + function: { + name: 'random', + description: 'Generate random numbers or pick random items. Supports: int (integer range, default 0-100), float (decimal, default 0-1), pick (select from array of items), string (random alphanumeric, default length 8). Use count for multiple values.', + parameters: { + type: 'object', + properties: { + type: { type: 'string', enum: ['int', 'float', 'pick', 'string'], description: 'Random type. Default: int.' }, + min: { type: 'number', description: 'Minimum value (int/float). Default: 0.' }, + max: { type: 'number', description: 'Maximum value (int/float). Default: 100 (int) or 1 (float).' }, + count: { type: 'integer', description: 'Number of results. Max 100. Default: 1.' }, + items: { type: 'array', items: { type: 'string' }, description: 'Item pool for pick type.' }, + length: { type: 'integer', description: 'String length for string type. Max 256. Default: 8.' } + } + } + } + }, + { + type: 'function', + function: { + name: 'uuid', + description: 'Generate cryptographically random UUID v4 (e.g., "550e8400-e29b-41d4-a716-446655440000"). Uses Node.js crypto.randomUUID(). Supports batch generation up to 20.', + parameters: { + type: 'object', + properties: { + count: { type: 'integer', description: 'Number of UUIDs. Max 20. Default: 1.' } + } + } + } + }, + { + type: 'function', + function: { + name: 'json_format', + description: 'Format and validate a JSON string. Returns pretty-printed JSON with configurable indentation. Optionally sort object keys alphabetically. Also works as a JSON syntax validator — returns error details on invalid input.', + parameters: { + type: 'object', + required: ['json'], + properties: { + json: { type: 'string', description: 'The JSON string to format/validate.' }, + indent: { type: 'integer', description: 'Indentation spaces. Default: 2.' }, + sort_keys: { type: 'boolean', description: 'Sort object keys alphabetically. Default: false.' } + } + } + } + }, + { + type: 'function', + function: { + name: 'hash', + description: 'Compute cryptographic hash of text. Supports MD5, SHA-1, SHA-256, SHA-384, SHA-512. Default: SHA-256. Returns hex-encoded digest.', + parameters: { + type: 'object', + required: ['text'], + properties: { + text: { type: 'string', description: 'The text to hash.' }, + algorithm: { type: 'string', enum: ['md5', 'sha1', 'sha256', 'sha384', 'sha512'], description: 'Hash algorithm. Default: sha256.' } + } + } + } } ]; @@ -652,7 +742,9 @@ let enabledTools: Set = new Set([ '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_click', 'browser_type', 'browser_scroll', 'browser_close' + 'browser_click', 'browser_type', 'browser_scroll', 'browser_close', + 'datetime', 'calculator', + 'random', 'uuid', 'json_format', 'hash' ]); export function setToolEnabled(toolName: string, enabled: boolean): void { diff --git a/src/renderer/styles/style.css b/src/renderer/styles/style.css index befe1d3..452d4f6 100644 --- a/src/renderer/styles/style.css +++ b/src/renderer/styles/style.css @@ -490,6 +490,36 @@ html, body { border-color: rgba(212, 160, 60, 0.15); } +/* ── 上下文长度胶囊(模型栏) ── */ +.ctx-total { + display: inline-flex; + align-items: center; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + padding: 3px 10px; + border-radius: 20px; + color: #47848F; + background: rgba(71, 132, 143, 0.08); + border: 1px solid rgba(71, 132, 143, 0.15); + margin-left: 6px; +} + +/* ── 剩余上下文胶囊 ── */ +.ctx-remain { + display: inline-flex; + align-items: center; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + padding: 3px 10px; + border-radius: 20px; + color: #4CAF50; + background: rgba(76, 175, 80, 0.08); + border: 1px solid rgba(76, 175, 80, 0.15); + margin-left: 2px; +} + /* ── 温度滑块 ── */ .temp-slider { width: 100%; diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index 5050258..65b34e6 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -207,6 +207,15 @@ export interface WorkspaceDirResult { export interface MetonaDesktopAPI { isDesktop: boolean; info: () => Promise; + sys: { + homeDir: string; + tmpDir: string; + shell: string; + arch: string; + platform: string; + hostname: string; + username: string; + }; dialog: { openFile: (options?: FileFilterOptions) => Promise; saveFile: (options?: SaveFileOptions) => Promise; @@ -221,7 +230,6 @@ export interface MetonaDesktopAPI { getConfig: () => Promise<{ allowedDirs: string[]; blockedDirs: string[] }>; setAllowedDirs: (dirs: string[]) => Promise; setTimeouts: (timeouts: { http?: number; mcp?: number }) => Promise<{ success: boolean }>; - setProxy: (url: string) => Promise; }; notify: (title: string, body: string) => void; window: {