v0.12.3: 全面审计修复 + 稳定性加固 + 文档更新
P0 安全/稳定性修复 (6项):
- sqlite persist 改用临时文件+rename,I/O错误日志
- IPC fs 处理器加入 checkPathAllowed 路径验证
- handleCompress 命令注入修复 + 跨平台 (powershell/tar/zip/unzip)
- browser 全页截图简化,使用 capturePage full rect
- workspace 竞态条件修复
- editFile 5MB 大小限制
P1 安全/稳定性修复 (7项):
- 大小写不敏感搜索: query 同步转换小写
- web_fetch 无 content-length 时流式读取+10MB限制防OOM
- Windows SIGTERM 改用 taskkill /PID /T /F 强制终止
- startsWith 路径检查 Windows 大小写不敏感
- 安全黑名单扩展: 33个目录路径 + 30条危险命令 (Linux+Windows)
- plan_track stepIndex 改用 typeof === 'number' 精确检查
- MCP 前缀改用双下划线分隔 mcp_{server}__{tool} 防歧义
P2 代码质量修复 (8项):
- 修复 EXECUTING→THINKING 死循环Bug (转换表缺失)
- COMPRESSING 硬上限绕过时同步 _loopState 全局状态
- Abort 路径补齐 onDone 回调, 防止UI状态残留
- 幻觉检测从4条扩展到16条规则, 覆盖全部工具类别
- 移除 getTokenEfficiency 死代码
- 跨平台路径清理 (replace(/[\/]+$/, ''))
- 动态 import 加 try/catch, 失败自动批准不阻断
- #modelSelect 加 null 守卫, 防止运行时崩溃
文档 & UI 更新:
- README.md: 版本号/特性表/架构图/安全机制/工具清单全面刷新
- 帮助面板: 新增 Plan Mode、抗幻觉&稳定性章节, Agent Loop补8状态机
- 工具面板: plan_track卡片 + plan-mode徽章样式, 标题更新为42+1
- 工作空间面板宽度: 480px → 400px
- 源码注释移除所有版本标签, 仅保留4处必要位置
- 版本号 0.12.2 → 0.12.3
This commit is contained in:
@@ -7,6 +7,7 @@ build-resources/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
*.log
|
*.log
|
||||||
window-state.json
|
window-state.json
|
||||||
|
nul
|
||||||
|
|
||||||
# Build output (release for desktop)
|
# Build output (release for desktop)
|
||||||
!release/*.exe
|
!release/*.exe
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-v0.11.11-E8734A?style=flat-square" alt="version">
|
<img src="https://img.shields.io/badge/version-v0.12.3-E8734A?style=flat-square" alt="version">
|
||||||
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
|
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
|
||||||
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
|
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
|
||||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
|
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
|
||||||
@@ -34,8 +34,10 @@
|
|||||||
|
|
||||||
| | 功能 | 说明 |
|
| | 功能 | 说明 |
|
||||||
|:---:|:---|:---|
|
|:---:|:---|:---|
|
||||||
| 🤖 | **ReAct Agent Loop** | 始终开启的唯一对话模式。Thought → Action → Observation → Reflection 完整循环,最大 85 轮(可配置),自动重试 2 次,工具去重,并行/链式执行 |
|
| 🤖 | **ReAct Agent Loop** | 始终开启的唯一对话模式。8 状态机(INIT→THINKING→PARSING→EXECUTING→OBSERVING→REFLECTING→COMPRESSING→TERMINATED),最大 85 轮(可配置),自动重试 2 次,工具去重,并行/链式执行,看门狗超时保护 |
|
||||||
| 🔧 | **42 个内置工具** | 文件系统(16个) · 命令执行 · 联网搜索 · 浏览器控制(9个) · Git · 记忆 · 会话 · 子代理 · 系统工具 |
|
| 🛡️ | **5 层抗幻觉系统** | 提示词加固 → 任务感知 → 中途检测(16 条规则覆盖全部工具类别)→ 进度锚点 → 完成闸门(6 项检查,幻觉/注入/文件/工具 4 类关键阻断) |
|
||||||
|
| 📋 | **Plan Mode** | 开关切换,AI 首先生成执行计划(Markdown 渲染确认弹窗),批准后按步骤追踪执行,plan_track 工具标记完成状态 |
|
||||||
|
| 🔧 | **42 个内置工具** | 文件系统(16个) · 命令执行 · 联网搜索 · 浏览器控制(9个) · Git · 记忆 · 会话 · 子代理 · 系统工具 · Plan Mode 追踪 |
|
||||||
| 🧠 | **智能记忆系统** | 三类记忆(fact / preference / rule),FTS5 全文搜索 + 向量语义搜索,写入前安全扫描,容量 500 条,90 天衰减 |
|
| 🧠 | **智能记忆系统** | 三类记忆(fact / preference / rule),FTS5 全文搜索 + 向量语义搜索,写入前安全扫描,容量 500 条,90 天衰减 |
|
||||||
| 📋 | **自定义文件** | SOUL.md(人格,不可压缩)+ AGENT.md(行为准则)+ USER.md(用户画像),工作空间优先,内置默认 fallback |
|
| 📋 | **自定义文件** | SOUL.md(人格,不可压缩)+ AGENT.md(行为准则)+ USER.md(用户画像),工作空间优先,内置默认 fallback |
|
||||||
| 🌐 | **MCP 协议扩展** | JSON-RPC 2.0 over stdio,动态工具发现,Shadowing 防护 |
|
| 🌐 | **MCP 协议扩展** | JSON-RPC 2.0 over stdio,动态工具发现,Shadowing 防护 |
|
||||||
@@ -44,6 +46,8 @@
|
|||||||
| 🖥️ | **工作空间面板** | 终端(实时流式输出)+ 文件浏览器,命令安全检查 |
|
| 🖥️ | **工作空间面板** | 终端(实时流式输出)+ 文件浏览器,命令安全检查 |
|
||||||
| 🔢 | **上下文长度自动检测** | 切换模型时自动从 model_info 获取实际支持的上下文长度,无需手动配置 |
|
| 🔢 | **上下文长度自动检测** | 切换模型时自动从 model_info 获取实际支持的上下文长度,无需手动配置 |
|
||||||
| 🗜️ | **智能上下文管理** | 滑动窗口 + Token 自动校准 + 消息重要性评分 + LLM 结构化 JSON 压缩 |
|
| 🗜️ | **智能上下文管理** | 滑动窗口 + Token 自动校准 + 消息重要性评分 + LLM 结构化 JSON 压缩 |
|
||||||
|
| ⏱️ | **看门狗超时** | Agent Loop 全局超时保护(可配,默认 30min),防止模型卡死无限循环 |
|
||||||
|
| 🪝 | **Hook 系统** | 4 阶段生命周期钩子(pre_tool / post_tool / post_iteration / pre_completion),内置文件变更审计、工具结果校验 |
|
||||||
| 👥 | **子代理委派** | spawn_task 工具,独立上下文 + 超时保护 |
|
| 👥 | **子代理委派** | spawn_task 工具,独立上下文 + 超时保护 |
|
||||||
| 📈 | **Token 仪表盘** | 全局 + 会话统计,消耗趋势柱状图,2 秒刷新,输入/输出分色 |
|
| 📈 | **Token 仪表盘** | 全局 + 会话统计,消耗趋势柱状图,2 秒刷新,输入/输出分色 |
|
||||||
| 📋 | **系统提示词卡片** | 每条 AI 回复顶部折叠卡片,点击查看实际发送给模型的完整上下文 |
|
| 📋 | **系统提示词卡片** | 每条 AI 回复顶部折叠卡片,点击查看实际发送给模型的完整上下文 |
|
||||||
@@ -161,20 +165,31 @@
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>📋 Plan Mode(1 个,仅 Plan 模式激活)</strong></summary>
|
||||||
|
|
||||||
|
| 工具 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `plan_track` | Plan 模式执行进度追踪,标记步骤完成,自动统计剩余 |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 🏗️ 架构
|
## 🏗️ 架构
|
||||||
|
|
||||||
```
|
```
|
||||||
用户消息 → 扫描工作空间 SOUL.md(不可压缩层)→ AGENT.md → USER.md
|
用户消息 → 扫描工作空间 SOUL.md(不可压缩层)→ AGENT.md → USER.md
|
||||||
↓
|
↓
|
||||||
Agent Engine (ReAct Loop, ≤85轮)
|
|
||||||
↓
|
|
||||||
记忆检索 (FTS5 + 向量语义搜索) → 上下文注入
|
记忆检索 (FTS5 + 向量语义搜索) → 上下文注入
|
||||||
↓
|
↓
|
||||||
|
Agent Engine (8 状态机 ReAct Loop, ≤85 轮, 看门狗 30min)
|
||||||
|
↓
|
||||||
|
5 层抗幻觉系统 (提示词 → 中途检测 → 进度锚 → 完成闸门 → 校验)
|
||||||
|
↓
|
||||||
Ollama API (流式响应,num_ctx 自动检测)
|
Ollama API (流式响应,num_ctx 自动检测)
|
||||||
↓
|
↓
|
||||||
Tool Registry (42 内置 + MCP 动态)
|
Tool Registry (42 内置 + MCP 动态 + Plan Mode plan_track)
|
||||||
↓
|
↓
|
||||||
观察结果 → 反思 → 循环 / 最终回答
|
Hook 系统 (pre/post tool/iteration/completion) → 观察结果 → 反思 → 循环 / 最终回答
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🧩 UI 组件(原生 DOM)
|
### 🧩 UI 组件(原生 DOM)
|
||||||
@@ -184,15 +199,15 @@ main.ts (入口)
|
|||||||
├── header.ts # 顶部导航栏
|
├── header.ts # 顶部导航栏
|
||||||
├── model-bar.ts # 模型选择栏
|
├── model-bar.ts # 模型选择栏
|
||||||
├── chat-area.ts # 聊天消息区域
|
├── chat-area.ts # 聊天消息区域
|
||||||
├── input-area.ts # 输入框 + 文件上传
|
├── input-area.ts # 输入框 + 文件上传 + Plan Mode 开关
|
||||||
├── workspace-panel.ts # 终端 + 文件浏览器
|
├── workspace-panel.ts # 终端 + 文件浏览器 + 工具卡片
|
||||||
├── settings-modal.ts # 设置面板
|
├── settings-modal.ts # 设置面板(含看门狗超时配置)
|
||||||
├── history-modal.ts # 会话历史
|
├── history-modal.ts # 会话历史
|
||||||
├── memory-modal.ts # 记忆管理
|
├── memory-modal.ts # 记忆管理
|
||||||
├── tools-modal.ts # 工具列表
|
├── tools-modal.ts # 工具列表
|
||||||
├── token-dashboard.ts # Token 消耗仪表盘
|
├── token-dashboard.ts # Token 消耗仪表盘
|
||||||
├── tool-confirm-modal.ts # 工具执行确认
|
├── tool-confirm-modal.ts # 工具执行确认
|
||||||
├── prompt-modal.ts # 系统提示词查看
|
├── prompt-modal.ts # 系统提示词查看 + Plan 确认弹窗
|
||||||
├── toast.ts # Toast 通知
|
├── toast.ts # Toast 通知
|
||||||
└── lightbox.ts # 图片灯箱
|
└── lightbox.ts # 图片灯箱
|
||||||
```
|
```
|
||||||
@@ -214,13 +229,15 @@ SQLite (sql.js WASM),6 张表,WAL 模式 + FTS5 全文搜索:
|
|||||||
|
|
||||||
| 层级 | 措施 |
|
| 层级 | 措施 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📁 文件系统 | `checkPathAllowed()` — 路径黑名单 |
|
| 📁 文件系统 | `checkPathAllowed()` — 路径黑名单(33 个系统/敏感目录,含 Linux + Windows) |
|
||||||
| ⚡ 命令执行 | `checkCommandAllowed()` — 命令黑名单 + 三种模式 |
|
| ⚡ 命令执行 | `checkCommandAllowed()` — 命令黑名单(30 条危险命令,含 POSIX + Windows)+ 三种模式 |
|
||||||
| 🖥️ 前端渲染 | HTML 净化器(白名单标签 + URI 协议检查) |
|
| 🖥️ 前端渲染 | HTML 净化器(白名单标签 + URI 协议检查) |
|
||||||
| 🔒 Electron | `contextIsolation: true` + IPC 白名单 |
|
| 🔒 Electron | `contextIsolation: true` + IPC 白名单 + IPC fs 路径验证 |
|
||||||
| 🔐 数据加密 | AES-256-GCM |
|
| 🔐 数据加密 | AES-256-GCM |
|
||||||
| 🧠 记忆安全 | 写入前 Prompt Injection / 敏感信息检测 |
|
| 🧠 记忆安全 | 写入前 Prompt Injection / 敏感信息检测 |
|
||||||
| 🌐 MCP 安全 | Shadowing 防护 |
|
| 🌐 MCP 安全 | Shadowing 防护 + 双下划线分隔防歧义 |
|
||||||
|
| 🌐 网络安全 | `web_fetch` 流式体积限制(10MB)+ 无 content-length 时防 OOM |
|
||||||
|
| ⚡ 进程安全 | Windows `taskkill` 强制终止 + 工作空间路径大小写不敏感校验 |
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
@@ -239,7 +256,7 @@ npm start
|
|||||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
||||||
```
|
```
|
||||||
|
|
||||||
产出:`release/Metona Ollama Setup v0.11.11.exe`
|
产出:`release/Metona Ollama Setup v0.12.3.exe`
|
||||||
|
|
||||||
## 🛠️ 常用命令
|
## 🛠️ 常用命令
|
||||||
|
|
||||||
@@ -269,8 +286,10 @@ npm run dist # 构建 Windows 安装包
|
|||||||
|
|
||||||
| | Feature | Description |
|
| | 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 |
|
| 🤖 | **ReAct Agent Loop** | Always-on, only chat mode. 8-state machine (INIT→THINKING→PARSING→EXECUTING→OBSERVING→REFLECTING→COMPRESSING→TERMINATED), up to 85 iterations (configurable), auto-retry 2 times, tool dedup, parallel/chain execution, watchdog timeout protection |
|
||||||
| 🔧 | **42 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Sessions · Sub-agent · System |
|
| 🛡️ | **5-Layer Anti-Hallucination** | Prompt hardening → task awareness → mid-task detection (16 rules covering all tool categories) → progress anchors → completion gate (6 checks: hallucination, injection, file, tool — 4 critical blocks) |
|
||||||
|
| 📋 | **Plan Mode** | Toggle switch. AI first generates an execution plan (Markdown-rendered confirmation dialog), then tracks step-by-step execution, with plan_track tool marking completion |
|
||||||
|
| 🔧 | **42 Built-in Tools** | File system · Command · Web search · Browser · Git · Memory · Sessions · Sub-agent · System · Plan Mode tracking |
|
||||||
| 🧠 | **Smart Memory System** | Three types (fact / preference / rule), FTS5 + vector semantic search, pre-write security scan, 500 capacity, 90-day decay |
|
| 🧠 | **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 |
|
| 📋 | **Custom Files** | SOUL.md (persona, never compressed) + AGENT.md (behavior rules) + USER.md (user profile), workspace-first with built-in defaults |
|
||||||
| 🌐 | **MCP Protocol Extension** | JSON-RPC 2.0 over stdio, dynamic tool discovery, Shadowing protection |
|
| 🌐 | **MCP Protocol Extension** | JSON-RPC 2.0 over stdio, dynamic tool discovery, Shadowing protection |
|
||||||
@@ -279,6 +298,8 @@ npm run dist # 构建 Windows 安装包
|
|||||||
| 🖥️ | **Workspace Panel** | Terminal (real-time streaming) + file browser, command security checks |
|
| 🖥️ | **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 |
|
| 🔢 | **Auto Context Length Detection** | Reads actual context_length from model metadata on model switch — no manual config needed |
|
||||||
| 🗜️ | **Smart Context Manager** | Sliding window + Token auto-calibration + message importance scoring + LLM structured JSON compression |
|
| 🗜️ | **Smart Context Manager** | Sliding window + Token auto-calibration + message importance scoring + LLM structured JSON compression |
|
||||||
|
| ⏱️ | **Watchdog Timeout** | Global Agent Loop timeout protection (configurable, default 30min), prevents infinite loops from model stalls |
|
||||||
|
| 🪝 | **Hook System** | 4-phase lifecycle hooks (pre_tool / post_tool / post_iteration / pre_completion), built-in file change audit and tool result verification |
|
||||||
| 👥 | **Sub-agent Delegation** | spawn_task tool, isolated context + timeout protection |
|
| 👥 | **Sub-agent Delegation** | spawn_task tool, isolated context + timeout protection |
|
||||||
| 📈 | **Token Dashboard** | Global + session stats, consumption trend bar chart, 2s refresh, input/output color-coded |
|
| 📈 | **Token Dashboard** | Global + session stats, consumption trend bar chart, 2s refresh, input/output color-coded |
|
||||||
| 📋 | **System Prompt Card** | Collapsible card atop each AI reply — click to inspect the full context sent to the model |
|
| 📋 | **System Prompt Card** | Collapsible card atop each AI reply — click to inspect the full context sent to the model |
|
||||||
@@ -395,6 +416,15 @@ npm run dist # 构建 Windows 安装包
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>📋 Plan Mode (1, only active in Plan Mode)</strong></summary>
|
||||||
|
|
||||||
|
| Tool | Function |
|
||||||
|
|------|------|
|
||||||
|
| `plan_track` | Plan Mode execution progress tracking, mark steps complete, auto tally remaining |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 🏗️ Architecture
|
## 🏗️ Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -402,11 +432,15 @@ User message → workspace SOUL.md (never compressed) → AGENT.md → USER.md
|
|||||||
↓
|
↓
|
||||||
Memory Retrieval (FTS5 + Vector Semantic Search) → Context Injection
|
Memory Retrieval (FTS5 + Vector Semantic Search) → Context Injection
|
||||||
↓
|
↓
|
||||||
|
Agent Engine (8-state ReAct Loop, ≤85 iter, watchdog 30min)
|
||||||
|
↓
|
||||||
|
5-Layer Anti-Hallucination (prompt → mid-task detection → anchors → gate → verification)
|
||||||
|
↓
|
||||||
Ollama API (Streaming Response)
|
Ollama API (Streaming Response)
|
||||||
↓
|
↓
|
||||||
Tool Registry (42 Built-in + MCP Dynamic)
|
Tool Registry (42 Built-in + MCP Dynamic + Plan Mode plan_track)
|
||||||
↓
|
↓
|
||||||
Observation → Reflection → Loop / Final Answer
|
Hook System (pre/post tool/iteration/completion) → Observation → Reflection → Loop / Final Answer
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🧩 UI Components (Native DOM)
|
### 🧩 UI Components (Native DOM)
|
||||||
@@ -416,15 +450,15 @@ main.ts (Entry)
|
|||||||
├── header.ts # Top navigation bar
|
├── header.ts # Top navigation bar
|
||||||
├── model-bar.ts # Model selector
|
├── model-bar.ts # Model selector
|
||||||
├── chat-area.ts # Chat message area
|
├── chat-area.ts # Chat message area
|
||||||
├── input-area.ts # Input box + file upload
|
├── input-area.ts # Input box + file upload + Plan Mode toggle
|
||||||
├── workspace-panel.ts # Terminal + file browser
|
├── workspace-panel.ts # Terminal + file browser + tool cards
|
||||||
├── settings-modal.ts # Settings panel
|
├── settings-modal.ts # Settings panel (with watchdog timeout config)
|
||||||
├── history-modal.ts # Session history
|
├── history-modal.ts # Session history
|
||||||
├── memory-modal.ts # Memory management
|
├── memory-modal.ts # Memory management
|
||||||
├── tools-modal.ts # Tool list
|
├── tools-modal.ts # Tool list
|
||||||
├── token-dashboard.ts # Token consumption dashboard
|
├── token-dashboard.ts # Token consumption dashboard
|
||||||
├── tool-confirm-modal.ts # Tool execution confirmation
|
├── tool-confirm-modal.ts # Tool execution confirmation
|
||||||
├── prompt-modal.ts # System prompt viewer
|
├── prompt-modal.ts # System prompt viewer + Plan confirm dialog
|
||||||
├── toast.ts # Toast notifications
|
├── toast.ts # Toast notifications
|
||||||
└── lightbox.ts # Image lightbox
|
└── lightbox.ts # Image lightbox
|
||||||
```
|
```
|
||||||
@@ -446,13 +480,15 @@ SQLite (sql.js WASM), 6 tables, WAL mode + FTS5 full-text search:
|
|||||||
|
|
||||||
| Layer | Measure |
|
| Layer | Measure |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📁 File System | `checkPathAllowed()` — path blacklist |
|
| 📁 File System | `checkPathAllowed()` — path blacklist (33 system/sensitive dirs, Linux + Windows) |
|
||||||
| ⚡ Command Execution | `checkCommandAllowed()` — command blacklist + three modes |
|
| ⚡ Command Execution | `checkCommandAllowed()` — command blacklist (30 dangerous commands, POSIX + Windows) + three modes |
|
||||||
| 🖥️ Frontend Rendering | HTML sanitizer (whitelist tags + URI protocol check) |
|
| 🖥️ Frontend Rendering | HTML sanitizer (whitelist tags + URI protocol check) |
|
||||||
| 🔒 Electron | `contextIsolation: true` + IPC whitelist |
|
| 🔒 Electron | `contextIsolation: true` + IPC whitelist + IPC fs path validation |
|
||||||
| 🔐 Data Encryption | AES-256-GCM |
|
| 🔐 Data Encryption | AES-256-GCM |
|
||||||
| 🧠 Memory Security | Pre-write prompt injection / sensitive info detection |
|
| 🧠 Memory Security | Pre-write prompt injection / sensitive info detection |
|
||||||
| 🌐 MCP Security | Shadowing protection |
|
| 🌐 MCP Security | Shadowing protection + double-underscore delimiter disambiguation |
|
||||||
|
| 🌐 Network Security | `web_fetch` streaming size limit (10MB) + OOM prevention without content-length |
|
||||||
|
| ⚡ Process Security | Windows `taskkill` forced termination + case-insensitive workspace path validation |
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -471,7 +507,7 @@ npm start
|
|||||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
||||||
```
|
```
|
||||||
|
|
||||||
Output: `release/Metona Ollama Setup v0.11.11.exe`
|
Output: `release/Metona Ollama Setup v0.12.3.exe`
|
||||||
|
|
||||||
## 🛠️ Common Commands
|
## 🛠️ Common Commands
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,439 @@
|
|||||||
|
# AI Agent 工程化完整文档:ReAct Loop 与 Harness Engineering
|
||||||
|
|
||||||
|
> 本文围绕当下 AI Agent 工程化的两大核心命题展开:作为智能体"思考与执行内核"的 **ReAct Loop(推理-行动-观察循环)**,以及作为"运行环境与管控体系"的 **Harness Engineering(驾驭工程)**。两者共同构成生产级 AI Agent 的工程底座。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、背景:从"调教模型"到"建造系统"
|
||||||
|
|
||||||
|
2026 年,AI Agent 的叙事重心发生了根本性转移:从追求单个 Agent 的"智力上限",转向构建整个系统的"可靠性下限"。大模型早已不是 AI 落地的唯一瓶颈,行业已达成共识:
|
||||||
|
|
||||||
|
```
|
||||||
|
Agent = Model + Harness
|
||||||
|
```
|
||||||
|
|
||||||
|
模型是引擎,而 Harness(驾驭系统)才是决定智能体能否稳定跑完复杂长任务、从演示级走向生产级的关键。
|
||||||
|
|
||||||
|
AI 工程范式经历了三个阶段的演进:
|
||||||
|
|
||||||
|
| 阶段 | 时间范围 | 核心关注点 | 解决的问题 | 典型技术 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Prompt Engineering | 2022-2024 | 如何让模型理解你的意图 | 单次输出的质量 | 提示词模板、Few-shot 示例 |
|
||||||
|
| Context Engineering | 2025 | 如何给模型正确的知识边界 | 给模型看什么信息 | RAG、上下文窗口管理 |
|
||||||
|
| Harness Engineering | 2026- | 如何让 Agent 可靠、持续、不失控 | 多步骤、长周期任务的可靠性 | 状态机、沙箱、权限系统 |
|
||||||
|
|
||||||
|
这个演进路径反映了 AI 工程从"单点优化"到"系统构建"的转变:早期关注如何让模型"听懂人话",中期关注如何给模型"正确的参考资料",而现在关注如何让整个系统"持续稳定运行"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、ReAct Loop:Agent 的思考与执行内核
|
||||||
|
|
||||||
|
### 2.1 ReAct 的起源与定义
|
||||||
|
|
||||||
|
ReAct 源自普林斯顿大学与 Google Brain 于 2022 年联合发表的经典论文《ReAct: Synergizing Reasoning and Acting in Language Models》。在 ReAct 出现之前,大模型只有单纯的推理能力(Reasoning),存在严重的幻觉、知识滞后、无法实操的问题;而单纯的工具调用只有行动能力(Acting),缺乏逻辑推理,无法自主判断何时调用工具、调用什么工具。
|
||||||
|
|
||||||
|
ReAct 的核心颠覆式创新在于:**将大模型的推理思考(Thought)与外部工具行动(Action)进行闭环融合**。
|
||||||
|
|
||||||
|
```
|
||||||
|
ReAct = Reasoning(推理思考) + Acting(工具行动)
|
||||||
|
```
|
||||||
|
|
||||||
|
它是一套迭代式循环执行范式:Agent 不再一次性输出答案,而是通过「思考→行动→观察→再思考」的无限循环,逐步拆解复杂任务、调用外部工具、修正推理偏差,直到任务完成。ReAct 是目前 90% 以上开源 Agent(LangChain、LlamaIndex、Meta Agent)的底层核心执行逻辑,也是工业界公认的 AI Agent 标准思考框架。
|
||||||
|
|
||||||
|
### 2.2 T-A-O 三步循环机制
|
||||||
|
|
||||||
|
所有基于 ReAct 的 Agent,底层都是统一的 **T-A-O 循环闭环**,这是必须掌握的核心底层逻辑。
|
||||||
|
|
||||||
|
```
|
||||||
|
用户提问 → Thought 思考 → Action 行动 → Observation 观察 → 任务完成? → 否 → 再次 Thought → 是 → 输出最终答案
|
||||||
|
```
|
||||||
|
|
||||||
|
**1. Thought(推理思考)**
|
||||||
|
|
||||||
|
大模型基于当前用户问题、历史上下文、已有工具列表,进行自主推理判断:
|
||||||
|
- 当前任务是否需要调用工具?
|
||||||
|
- 需要调用哪一个工具?
|
||||||
|
- 工具入参应该如何构造?
|
||||||
|
- 当前任务是否已经完成,可以直接输出答案?
|
||||||
|
|
||||||
|
这一步是 Agent 的智能核心,完全依靠大模型的理解与推理能力。
|
||||||
|
|
||||||
|
**2. Action(工具行动)**
|
||||||
|
|
||||||
|
Agent 根据 Thought 的推理结果,通过 Harness 工具编排层,执行具体外部操作:
|
||||||
|
- 调用计算器、搜索引擎、数据库查询、接口请求、代码解释器等
|
||||||
|
- 严格按照 Harness 约束规则执行,受超时、重试、权限管控
|
||||||
|
- 单次仅执行单一工具任务,保证流程可控
|
||||||
|
|
||||||
|
**3. Observation(结果观察)**
|
||||||
|
|
||||||
|
获取 Action 工具执行的返回结果,将结果作为新的上下文信息喂给大模型,进入下一轮循环。Observation 是修正模型幻觉、补充真实信息的关键,让模型不再依赖陈旧参数知识。
|
||||||
|
|
||||||
|
完整闭环逻辑为:用户提问 → Thought 思考 → Action 执行工具 → Observation 获取结果 → 再次 Thought 迭代 → 任务完成 → 输出最终答案。
|
||||||
|
|
||||||
|
### 2.3 ReAct 范式与传统一次性 Prompt 的对比
|
||||||
|
|
||||||
|
很多新手分不清普通问答和 Agent 的区别,本质就是「是否具备 ReAct 循环能力」:
|
||||||
|
|
||||||
|
| 对比维度 | 传统一次性 Prompt 问答 | ReAct 智能体范式 |
|
||||||
|
|---|---|---|
|
||||||
|
| 执行方式 | 单次推理、一次性输出结果 | 迭代式循环、多轮思考执行 |
|
||||||
|
| 信息来源 | 仅依赖模型训练参数知识 | 模型知识 + 实时外部工具数据 |
|
||||||
|
| 复杂任务能力 | 无法拆解,复杂问题直接答错 | 自动拆解分步解决,适配复杂业务 |
|
||||||
|
| 幻觉概率 | 极高,知识滞后严重 | 大幅降低,以工具真实结果为准 |
|
||||||
|
| 工程依赖 | 无需 Harness,纯 Prompt 即可 | 强依赖 Harness 流程与工具管控 |
|
||||||
|
| 落地场景 | 简单问答、文案生成 | 企业自动化、数据查询、任务调度 |
|
||||||
|
|
||||||
|
### 2.4 ReAct 核心约束 Prompt 模板
|
||||||
|
|
||||||
|
ReAct 之所以能自动完成 T-A-O 循环,核心靠固定格式的系统 Prompt 约束,这也是 Harness 规则约束层的核心体现。原生 ReAct 标准 Prompt 核心结构如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
你是一个可以自主思考和调用工具的智能体。
|
||||||
|
你需要遵循【Thought → Action → Observation】循环逻辑解决问题。
|
||||||
|
可用工具列表:{tools}
|
||||||
|
严格遵循输出格式:
|
||||||
|
1. 思考(Thought): 分析当前问题,判断是否需要调用工具
|
||||||
|
2. 行动(Action): 需要调用工具时,输出工具名称和参数
|
||||||
|
3. 观察(Observation): 接收工具返回结果
|
||||||
|
如果已经获取足够信息,无需继续调用工具,直接输出最终答案。
|
||||||
|
问题:{input}
|
||||||
|
历史记录:{agent_scratchpad}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `agent_scratchpad` 是 ReAct 的核心缓存,记录每一轮的 Thought、Action、Observation,保存迭代全过程状态,属于 Harness 上下文记忆层能力;`tools` 是 Harness 注册的全部可调用工具列表;`input` 是用户原始任务指令。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Harness Engineering:Agent 的运行环境与管控体系
|
||||||
|
|
||||||
|
### 3.1 核心概念与思想
|
||||||
|
|
||||||
|
Harness Engineering,也叫"驾驭工程"或 Agent Harness,是围绕 AI Agent 构建工作环境的一套工程方法。它的目标不是让模型"回答得更好看",而是让 Agent 在真实工程系统里:
|
||||||
|
- 能理解任务
|
||||||
|
- 能读取必要上下文
|
||||||
|
- 能调用合适工具
|
||||||
|
- 能安全修改代码
|
||||||
|
- 能运行测试验证
|
||||||
|
- 能观察日志和失败原因
|
||||||
|
- 能根据反馈继续修复
|
||||||
|
- 能在边界内完成交付
|
||||||
|
|
||||||
|
简而言之:**Prompt 是你怎么跟模型说话,Harness 是你怎么给 Agent 搭工作台**。如果说大模型本身提供的是推理和生成能力,那么 Harness 提供的就是工程环境、工具系统、反馈机制和安全边界。
|
||||||
|
|
||||||
|
Harness 的原意是"马具"——套在马身上用于控制方向、承受重负、连接马车的那套皮革与金属装置。这个比喻非常精准:大模型就像一匹充满力量但难以预测的野马,而 Harness 就是那套让它变得可控、有用的装置。更精确的公式是:
|
||||||
|
|
||||||
|
```
|
||||||
|
生产级 Agent = 模型潜能 - 模型熵增 + Harness 约束
|
||||||
|
```
|
||||||
|
|
||||||
|
其中"模型熵增"指大模型基于概率生成的不确定性,输入微小的 Prompt 变化可能导致巨大的行为漂移;而"Harness 约束"则是用确定性的代码逻辑去框住不确定的模型输出。Harness Engineering 的核心思想是:**每当 Agent 犯错,就将其工程化为一个永久性的系统修复,确保它不会再犯同样的错误**。
|
||||||
|
|
||||||
|
### 3.2 Harness 与 Prompt 工程的本质区别
|
||||||
|
|
||||||
|
| 维度 | Prompt Engineering | Harness Engineering |
|
||||||
|
|---|---|---|
|
||||||
|
| 核心问题 | 如何措辞指令 | 如何构建可靠系统 |
|
||||||
|
| 作用范围 | 单次推理 | 全任务生命周期 |
|
||||||
|
| 控制手段 | 文本指令 | 工具 + 约束 + 反馈 + 基建 |
|
||||||
|
| 失败模式 | 误解意图 | 缺乏纠错机制 |
|
||||||
|
| 可复现性 | 依赖模型一致性 | 依赖工程化保障 |
|
||||||
|
| 类比 | 写指令邮件 | 建项目管理体系 |
|
||||||
|
|
||||||
|
一个具体例子可以说明这种区别。你在 prompt 里写"请遵守项目架构,不要跨层调用"——这是 Prompt 工程;但如果你把架构边界写进 custom linter,每次 Agent 改完代码都会被自动检查,违反规则就失败——这就是 Harness 工程。Prompt 可以是 Harness 的一部分,但 Harness 远远不止 Prompt。
|
||||||
|
|
||||||
|
### 3.3 Harness 七层内核
|
||||||
|
|
||||||
|
一个生产级 Harness 由七大协同组件构成,共同约束与增强智能体行为:
|
||||||
|
|
||||||
|
1. **System Prompts(系统提示)**:行为宪法,定义身份、边界、硬约束。
|
||||||
|
2. **Tools and Capabilities(工具与能力)**:精准能力接口,命名自解释、参数精确、错误可修复。
|
||||||
|
3. **Infrastructure(基础设施)**:沙箱、执行引擎、文件系统等安全运行环境。
|
||||||
|
4. **Orchestration Logic(编排逻辑)**:子智能体调度、任务分发与路由。
|
||||||
|
5. **Hooks and Middleware(钩子与中间件)**:确定性检查点,安全门控、质量回路、完成门控、可观测性。
|
||||||
|
6. **Memory and State(记忆与状态)**:进度与记忆持久化,避免长任务"失忆"。
|
||||||
|
7. **Verification Systems(验证系统)**:Linter、测试、审查 Agent,最后质量防线。
|
||||||
|
|
||||||
|
它们联动形成闭环:验证触发 Hook,记忆动态组装 Prompt,编排决定工具调用。
|
||||||
|
|
||||||
|
### 3.4 引导系统与反馈系统:双控机制
|
||||||
|
|
||||||
|
为方便理解,可以把 Harness 工程拆成两大子系统:
|
||||||
|
|
||||||
|
**引导系统(前馈控制 Guide)**——Agent 执行前,怎么知道该怎么做。核心是把项目里的隐性规则显性化,常见内容包括 AGENTS.md、CLAUDE.md、README、架构文档、编码规范、目录结构说明、项目启动脚本、测试命令说明、API 文档、领域知识、任务拆解模板、团队 review checklist 等。它的作用是行动前设路标与护栏,从源头减少错误。
|
||||||
|
|
||||||
|
**反馈系统(反馈控制 Sensor)**——Agent 执行后,怎么知道有没有做对。常见内容包括单元测试、接口测试、端到端测试、类型检查、linter、静态扫描、架构测试、安全扫描、浏览器自动化、运行日志、metrics、traces、错误堆栈、代码评审 Agent、LLM Judge、人工 review。反馈内部再分为:
|
||||||
|
- **计算性反馈**:规则驱动、毫秒级、100% 可靠,优先用
|
||||||
|
- **推理性反馈**:AI 判断、秒级、非确定,作为补充
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、ReAct 与 Harness 的层级关系
|
||||||
|
|
||||||
|
结合 AI Agent 完整工程化体系,三者层级关系清晰可见:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. LLM 模型:提供基础推理智能,是 Agent 的大脑基础
|
||||||
|
2. ReAct 范式:定义大脑的思考方式(T-A-O 循环),是 Agent 的执行内核逻辑
|
||||||
|
3. Harness 工程:为 ReAct 循环提供约束、容错、记忆、监控、工具调度的整套运行环境
|
||||||
|
```
|
||||||
|
|
||||||
|
终极公式为:
|
||||||
|
|
||||||
|
```
|
||||||
|
企业级 Agent = LLM + ReAct 执行逻辑 + Harness 工程管控
|
||||||
|
```
|
||||||
|
|
||||||
|
如果大模型是 Agent 的大脑,那 ReAct 就是 Agent 的"思考与行动规则",是让 AI 从"只会说话"变成"会干活"的关键转折点;而 Harness 则是为这套规则提供可靠运行环境的"操作系统"。ReAct 是执行内核,Harness 是运行环境,二者缺一不可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、生产级 Agent Harness 四层架构
|
||||||
|
|
||||||
|
深入分析 Claude Code、OpenCode、OpenClaw、Hermess 等代表性生产级 Agent 项目,可以发现一个生产级的 Agent Harness 通常分为四层,每一层都有明确的职责和边界。
|
||||||
|
|
||||||
|
### 5.1 架构全景
|
||||||
|
|
||||||
|
这四层围绕"感知→决策→行动→反馈"闭环紧密协作:
|
||||||
|
|
||||||
|
1. **推理与编排层**:Agent 的"大脑与调度中心",负责核心决策逻辑。
|
||||||
|
2. **上下文与记忆层**:Agent 的"工作记忆与长期记忆",管理输入和进化。
|
||||||
|
3. **工具与安全执行层**:Agent 的"双手与安全护栏",封装外部调用。
|
||||||
|
4. **支撑与基础架构层**:Agent 的"神经系统与循环系统",提供底层支撑。
|
||||||
|
|
||||||
|
以"帮我在项目里添加用户登录功能"为例,完整流程为:支撑层接收请求并分配会话 ID → 上下文层组装输入(从 CLAUDE.md 读取技术栈、从记忆系统加载用户偏好)→ 推理层启动 Plan Mode 生成任务清单 → 用户批准后进入 Execute Mode → 安全层拦截工具调用做 AST 分析与风险评估 → 执行结果返回推理层 → 循环继续。
|
||||||
|
|
||||||
|
### 5.2 推理与编排层:Agent Loop 的状态机化
|
||||||
|
|
||||||
|
最基础的 Agent Loop 就是一个 while 循环:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
while (!done) {
|
||||||
|
const response = await callLLM(messages);
|
||||||
|
if (response.toolCalls.length > 0) {
|
||||||
|
const results = await executeTools(response.toolCalls);
|
||||||
|
messages.push(...results);
|
||||||
|
} else {
|
||||||
|
done = true;
|
||||||
|
return response.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
但在生产环境中这远远不够,需要处理流式响应、并行执行、错误恢复、用户中断、状态持久化等问题。因此生产级系统普遍采用状态机管理循环,例如 Claude Code 内部定义了精细的状态:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
enum LoopState {
|
||||||
|
INIT = 'INIT', // 初始化,准备上下文
|
||||||
|
THINKING = 'THINKING', // 正在调用 LLM
|
||||||
|
PARSING = 'PARSING', // 解析 LLM 输出
|
||||||
|
EXECUTING = 'EXECUTING', // 执行工具(可能并行)
|
||||||
|
OBSERVING = 'OBSERVING', // 收集工具结果
|
||||||
|
REFLECTING = 'REFLECTING', // (可选)反思结果
|
||||||
|
COMPRESSING = 'COMPRESSING', // 触发上下文压缩
|
||||||
|
TERMINATED = 'TERMINATED' // 终止
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
状态机的优势在于明确的阶段划分、易于调试、支持暂停/恢复、错误隔离。实现要点包括:状态转换要原子化、状态数据要隔离、超时控制要精细、可观测性要内置。每个状态对应一个独立的处理器(Handler),状态机只负责调度。
|
||||||
|
|
||||||
|
对于复杂任务,还支持多智能体编排,常见两种模式:
|
||||||
|
- **父子委派模式**(主 Agent 通过 Task 工具委派子任务给 SubAgent,具备上下文隔离、递归深度限制、结果聚合)
|
||||||
|
- **对等协作模式**(多个 Agent 组成团队,通过消息总线异步通信)
|
||||||
|
|
||||||
|
### 5.3 上下文与记忆层:System Prompt 的结构化组装
|
||||||
|
|
||||||
|
生产级系统的 System Prompt 分为静态区和动态区。静态区(角色定义、输出格式、安全规范)放在前面以利用 LLM 缓存,减少 Token 消耗;动态区(项目名、技术栈、当前任务、历史摘要)放在后面每次更新。
|
||||||
|
|
||||||
|
项目级上下文通过智能加载机制实现:Agent 启动时自动扫描项目根目录,按优先级寻找 CLAUDE.md、AGENTS.md、.claude/context.md 等配置文件,并对内容做智能截断(只取前 N 个 Token)。
|
||||||
|
|
||||||
|
### 5.4 上下文工程:渐进式披露
|
||||||
|
|
||||||
|
上下文是稀缺资源,上下文腐烂和描述膨胀会让准确率暴跌。核心策略是**渐进式披露**,分三层管理:
|
||||||
|
|
||||||
|
- **索引层**:始终保留项目结构、入口地图。
|
||||||
|
- **接口层**:操作模块时加载 API 与约束。
|
||||||
|
- **实现层**:修改文件时才加载源码。
|
||||||
|
|
||||||
|
用目录式索引告诉智能体"去哪找",而非"全记住",上下文可从数万 Token 压至几千。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、实战:从零实现标准 ReAct Agent
|
||||||
|
|
||||||
|
下面给出一份基于 LangChain 的标准原生 ReAct 智能体完整可运行代码,完整保留 T-A-O 循环、格式约束、容错机制与 Harness 管控能力,适配国内开源大模型。
|
||||||
|
|
||||||
|
### 6.1 环境依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install langchain langchain-openai python-dotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 完整可运行代码
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langchain.agents import AgentExecutor, create_react_agent
|
||||||
|
from langchain.tools import CalculatorTool
|
||||||
|
from langchain_community.utilities import WikipediaAPIWrapper
|
||||||
|
from langchain_community.tools import WikipediaQueryRun
|
||||||
|
from langchain.prompts import PromptTemplate
|
||||||
|
from langchain.globals import set_debug
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
# 开启全链路日志(Harness 可观测能力)
|
||||||
|
set_debug(True)
|
||||||
|
|
||||||
|
# ===================== 1. 初始化模型(适配国内任意 OpenAI 格式接口) =====================
|
||||||
|
llm = ChatOpenAI(
|
||||||
|
model="qwen-turbo",
|
||||||
|
temperature=0.0, # 零随机性,保证 ReAct 思考逻辑稳定
|
||||||
|
openai_api_key=os.getenv("OPENAI_API_KEY"),
|
||||||
|
openai_api_base=os.getenv("OPENAI_API_BASE")
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===================== 2. 注册工具(Harness 工具层) =====================
|
||||||
|
calc_tool = CalculatorTool()
|
||||||
|
wiki_api = WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=500)
|
||||||
|
wiki_tool = WikipediaQueryRun(api_wrapper=wiki_api)
|
||||||
|
tools = [calc_tool, wiki_tool]
|
||||||
|
|
||||||
|
# ===================== 3. 标准 ReAct 约束 Prompt(核心) =====================
|
||||||
|
react_prompt = PromptTemplate.from_template("""
|
||||||
|
你是严格遵循 ReAct 范式的智能体,必须按照 Thought → Action → Observation 循环执行任务。
|
||||||
|
可用工具:{tools}
|
||||||
|
执行规则:
|
||||||
|
1. 遇到需要计算、外部知识查询的问题,必须调用工具,禁止自行编造答案
|
||||||
|
2. 每一轮只能做一次思考 + 一次工具调用
|
||||||
|
3. 信息足够后,停止循环,输出简洁完整的最终答案
|
||||||
|
用户问题:{input}
|
||||||
|
执行过程记录:{agent_scratchpad}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ===================== 4. 创建 ReAct Agent + Harness 管控 =====================
|
||||||
|
agent = create_react_agent(llm, tools, react_prompt)
|
||||||
|
|
||||||
|
# Harness 容错、限流、防死循环配置
|
||||||
|
agent_executor = AgentExecutor(
|
||||||
|
agent=agent,
|
||||||
|
tools=tools,
|
||||||
|
verbose=True,
|
||||||
|
max_iterations=5, # 最大循环次数,防止 ReAct 死循环
|
||||||
|
handle_parsing_errors=True, # 解析异常兜底
|
||||||
|
timeout=15, # 超时熔断
|
||||||
|
return_intermediate_steps=True # 返回完整 ReAct 步骤
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===================== 5. 测试运行 =====================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
query = "请查询圆周率的近似定义,并计算 3.14159 * 128 的结果"
|
||||||
|
result = agent_executor.invoke({"input": query})
|
||||||
|
print("=" * 50)
|
||||||
|
print("最终答案:", result["output"])
|
||||||
|
print("=" * 50)
|
||||||
|
print("完整 ReAct 迭代步骤:")
|
||||||
|
for step in result["intermediate_steps"]:
|
||||||
|
print(f"步骤详情:{step}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 运行逻辑解析
|
||||||
|
|
||||||
|
执行过程遵循标准 ReAct 多轮迭代闭环:
|
||||||
|
1. 第一轮 Thought 识别任务需要先查询圆周率定义,调用维基百科工具
|
||||||
|
2. 第一轮 Action 执行百科查询
|
||||||
|
3. 第一轮 Observation 拿到文本信息,判断还需要数学计算
|
||||||
|
4. 第二轮 Thought 决定调用计算器工具执行乘法
|
||||||
|
5. 第二轮 Action 执行计算
|
||||||
|
6. 信息充足,终止循环输出最终答案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、ReAct 工程落地常见踩坑与优化
|
||||||
|
|
||||||
|
在 Harness 工程落地中,ReAct 是故障高发点,核心问题全部来自循环机制本身。
|
||||||
|
|
||||||
|
| 问题 | 现象 | 根因 | 解决方案 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 无限循环 | Agent 反复调用同一工具,无法结束任务 | 模型无法判断任务是否完成、工具返回信息重复 | Harness 层配置 `max_iterations` 最大迭代限制,强制熔断 |
|
||||||
|
| 格式解析失败 | 模型输出不遵循 Thought/Action 格式,任务中断 | Prompt 约束不严格 | 开启 `handle_parsing_errors` 异常兜底,优化 Prompt 格式约束 |
|
||||||
|
| 过度调用工具 | 简单问题也强行调用工具,浪费 Token | 缺乏常识判断规则 | 在 Prompt 中增加规则:简单常识问题可直接回答 |
|
||||||
|
| 上下文溢出 | 多轮迭代后 `agent_scratchpad` 过长触发超限 | 迭代日志累积 | 依托 Harness 记忆层,定时精简迭代日志、截断无效历史 |
|
||||||
|
|
||||||
|
### 企业级 ReAct 工程优化方案(结合 Harness 架构)
|
||||||
|
|
||||||
|
原生 ReAct 仅能实现基础能力,企业落地必须结合 Harness 架构做优化:
|
||||||
|
|
||||||
|
1. **约束层优化**:分级规则管控,简单任务弱约束、复杂任务强约束,平衡稳定性与灵活性。
|
||||||
|
2. **容错层优化**:智能重试 + 失败降级,工具调用失败时自动重试 2 次,重试失败后触发兜底答案,不中断业务流程。
|
||||||
|
3. **记忆层优化**:迭代过程轻量化存储,区分"有效迭代步骤"和"冗余日志",长期只保存关键 ReAct 决策过程。
|
||||||
|
4. **可观测层优化**:步骤级监控,统计每轮 ReAct 迭代耗时、失败率、工具调用命中率,数据驱动优化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、Harness 落地实践与治理
|
||||||
|
|
||||||
|
### 8.1 真实项目落地清单
|
||||||
|
|
||||||
|
在一个前后端分离的业务项目里,要让 Coding Agent 帮忙修 Bug、改接口、补测试,可以这样设计 Harness:
|
||||||
|
|
||||||
|
1. 用 AGENTS.md 或 CLAUDE.md 做统一入口,告知 Agent 项目结构、常用命令、关键约束和禁止事项。
|
||||||
|
2. 把详细架构文档放到 docs 目录,让 Agent 按需读取,而不是每次都塞进 prompt。
|
||||||
|
3. 用 linter、类型检查和架构测试限制跨层调用,避免 Agent 改出"能跑但不符合架构"的代码。
|
||||||
|
4. 提供标准化启动脚本,如 `start-backend`、`start-frontend`、`run-unit-test`、`run-api-test`。
|
||||||
|
5. 对接口任务接入 Swagger、OpenAPI、Postman Collection 或自动化接口测试。
|
||||||
|
6. 对前端任务接入 Playwright,让 Agent 不只是看代码,而是真的打开页面验证。
|
||||||
|
7. 暴露运行日志、metrics 和 traces,让 Agent 失败后能看到原因,而不是凭空猜。
|
||||||
|
8. 在任务结束前加入 checklist,要求确认需求点、测试结果、修改范围和风险点。
|
||||||
|
9. 对复杂任务引入独立 Review Agent 或人工 review,避免单 Agent 自说自话。
|
||||||
|
10. 关键仓库必须配置权限和沙箱,限制 Agent 能访问什么、能修改什么、能执行什么命令。
|
||||||
|
|
||||||
|
### 8.2 治理三维度与落地四阶段
|
||||||
|
|
||||||
|
**治理三维度(从易到难)**:
|
||||||
|
|
||||||
|
1. **可维护性**:代码规范、圈复杂度,工具成熟、自动化高。
|
||||||
|
2. **架构适应性**:性能、安全、依赖审计,需复杂基建。
|
||||||
|
3. **行为正确性**:业务需求匹配,最难、自动化最低。
|
||||||
|
|
||||||
|
**落地四阶段**:
|
||||||
|
|
||||||
|
1. **基础验证**:部署 Lint 与测试,打底质量底线。
|
||||||
|
2. **前馈增强**:把失败转为 AGENTS.md 规则,显性化隐性知识。
|
||||||
|
3. **闭环优化**:高频错误变 Hook,形成自纠错。
|
||||||
|
4. **度量驱动**:用指标仪表盘数据定向优化。
|
||||||
|
|
||||||
|
### 8.3 转向循环:让错误只犯一次
|
||||||
|
|
||||||
|
Harness 的终极价值是**复利效应**:观察失败 → 诊断根因 → 工程化修复 → 编码进 Harness → 验证部署。把单次人工修正,变成永久规则。比如智能体总提交超大代码,加一条"单次提交≤200 行",所有会话永久遵守,同类错误彻底消失。
|
||||||
|
|
||||||
|
Terminal-Bench 2.0 基准显示:同一模型仅换 Harness,排名可偏移超 25 位;精良 Harness 的中等模型,能打败粗糙 Harness 的顶级模型——**Harness 质量,才是性能决定性因素**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、总结
|
||||||
|
|
||||||
|
ReAct 不是一个框架、不是一个工具,而是 AI Agent 的标准思考与执行范式,是所有智能体实现"自主解决复杂任务"的核心底层。它通过 T-A-O 循环让 AI 从"只会说话"变成"会干活"。
|
||||||
|
|
||||||
|
Harness Engineering 则让 AI 工程范式从"调教模型"转向"建造系统",把不可控的概率输出变成可控、可复现、可持续优化的生产级能力。它的核心是搭建可验证、可约束的运行体系,让 AI 能可靠完成长链路任务。
|
||||||
|
|
||||||
|
二者的关系是:**ReAct 是执行内核,Harness 是运行环境**。只有吃透 ReAct 的迭代闭环、踩坑痛点与工程优化,同时构建完善的 Harness 约束、容错、记忆、监控体系,才能开发出稳定、可落地、可迭代的企业级 AI Agent,而不是只能跑 Demo 的玩具智能体。
|
||||||
|
|
||||||
|
AI Agent 的竞争早已不是模型军备竞赛,而是系统工程能力的比拼——未来决定 AI 落地上限的,不是模型有多强,而是你的 Harness 有多稳。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考来源
|
||||||
|
|
||||||
|
- 《ReAct: Synergizing Reasoning and Acting in Language Models》— Yao et al., Princeton & Google Brain, 2022
|
||||||
|
- Anthropic Claude Agent SDK 工程博客 — "Agent Harness"
|
||||||
|
- Mitchell Hashimoto — "Harness Engineering" 概念提出
|
||||||
|
- Terminal-Bench 2.0 基准测试数据
|
||||||
|
- Claude Code / OpenCode / OpenClaw / Hermess 生产级 Agent 项目源码分析
|
||||||
|
- CSDN 博客:《AI Agent 驾驭工程:从理论到生产级系统架构实战》
|
||||||
|
- CSDN 博客:《AI Agent 核心范式 ReAct 深度详解》
|
||||||
|
- CSDN 博客:《Harness Engineering:AI Agent 从"能用"到"可靠"的工程革命》
|
||||||
|
- 博客园:《面试官问:什么是 Harness 工程?》
|
||||||
|
- 腾讯新闻:《AI 大模型实战篇:AI Agent 设计模式 ReAct》
|
||||||
+3
-3
@@ -91,7 +91,7 @@ npm run dist # NSIS 安装包
|
|||||||
|
|
||||||
| 文件 | 说明 |
|
| 文件 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `release/Metona Ollama Setup v0.11.11.exe` | NSIS 安装包(可选目录、创建快捷方式) |
|
| `release/Metona Ollama Setup v0.12.2.exe` | NSIS 安装包(可选目录、创建快捷方式) |
|
||||||
|
|
||||||
> 仅提供 NSIS 安装包。
|
> 仅提供 NSIS 安装包。
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ npm run dist # NSIS 安装包
|
|||||||
TOKEN="your_access_token"
|
TOKEN="your_access_token"
|
||||||
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases?access_token=$TOKEN" \
|
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases?access_token=$TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"tag_name":"v0.11.11","name":"v0.11.11","body":"Release notes","target_commitish":"master"}'
|
-d '{"tag_name":"v0.12.2","name":"v0.12.2","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"
|
TOKEN="your_access_token"
|
||||||
RELEASE_ID="<release_id>"
|
RELEASE_ID="<release_id>"
|
||||||
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases/$RELEASE_ID/attach_files?access_token=$TOKEN" \
|
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.11.exe"
|
-H "Content-Type: multipart/form-data" -F "file=@release/Metona Ollama Setup v0.12.2.exe"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 故障排查
|
## 故障排查
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Metona Ollama Desktop — 开发规范
|
# Metona Ollama Desktop — 开发规范
|
||||||
|
|
||||||
> 版本: v0.11.11 | 更新: 2026-06-05 | 维护: 项目团队
|
> 版本: v0.12.2 | 更新: 2026-06-05 | 维护: 项目团队
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "0.11.11",
|
"version": "0.12.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "0.11.11",
|
"version": "0.12.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "0.11.11",
|
"version": "0.12.3",
|
||||||
"description": "Metona Ollama - TypeScript + Electron \u684c\u9762 AI \u804a\u5929\u5ba2\u6237\u7aef",
|
"description": "Metona Ollama - TypeScript + Electron \u684c\u9762 AI \u804a\u5929\u5ba2\u6237\u7aef",
|
||||||
"main": "dist/main/main.js",
|
"main": "dist/main/main.js",
|
||||||
"author": "thzxx",
|
"author": "thzxx",
|
||||||
|
|||||||
+7
-21
@@ -149,37 +149,23 @@ export async function browserScreenshot(params: { full_page?: boolean; selector?
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.full_page) {
|
if (params.full_page) {
|
||||||
// 全页面截图:先滚动获取完整高度,截完恢复
|
// 全页面截图:一次性捕获整个页面矩形区域
|
||||||
const pageSize = await win.webContents.executeJavaScript(`
|
const pageSize = await win.webContents.executeJavaScript(`
|
||||||
(() => {
|
(() => {
|
||||||
const w = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, window.innerWidth);
|
const w = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, window.innerWidth);
|
||||||
const h = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, window.innerHeight);
|
const h = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, window.innerHeight);
|
||||||
const origScroll = { x: window.scrollX, y: window.scrollY };
|
return { width: w, height: h };
|
||||||
window.scrollTo(0, 0);
|
|
||||||
return { width: w, height: h, origScroll };
|
|
||||||
})()
|
})()
|
||||||
`, true) as { width: number; height: number; origScroll: { x: number; y: number } };
|
`, true) as { width: number; height: number };
|
||||||
|
|
||||||
// 分块截图拼接(超过窗口高度的页面)
|
|
||||||
const viewHeight = 800;
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
for (let y = 0; y < pageSize.height; y += viewHeight) {
|
|
||||||
await win.webContents.executeJavaScript(`window.scrollTo(0, ${y})`, true);
|
|
||||||
await new Promise(r => setTimeout(r, 200));
|
|
||||||
const image = await win.webContents.capturePage({
|
const image = await win.webContents.capturePage({
|
||||||
x: 0, y: 0,
|
x: 0, y: 0,
|
||||||
width: pageSize.width,
|
width: pageSize.width,
|
||||||
height: Math.min(viewHeight, pageSize.height - y)
|
height: pageSize.height
|
||||||
});
|
});
|
||||||
chunks.push(image.toPNG());
|
const buffer = image.toPNG();
|
||||||
}
|
sendLog('success', `🌐 browser 截图(全页)`, `${pageSize.width}x${pageSize.height}, ${buffer.length} bytes`);
|
||||||
// 恢复滚动位置
|
return { success: true, png: buffer.toString('base64'), width: pageSize.width, height: pageSize.height };
|
||||||
await win.webContents.executeJavaScript(`window.scrollTo(${pageSize.origScroll.x}, ${pageSize.origScroll.y})`, true);
|
|
||||||
|
|
||||||
// 拼接所有截图(简单合并 base64)
|
|
||||||
const fullPng = Buffer.concat(chunks).toString('base64');
|
|
||||||
sendLog('success', `🌐 browser 截图(全页)`, `${pageSize.width}x${pageSize.height}, ${fullPng.length} chars`);
|
|
||||||
return { success: true, png: fullPng, width: pageSize.width, height: pageSize.height };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视口截图(默认)
|
// 视口截图(默认)
|
||||||
|
|||||||
+15
-6
@@ -87,8 +87,14 @@ function persist(): void {
|
|||||||
try {
|
try {
|
||||||
const data = db.export();
|
const data = db.export();
|
||||||
const buf = Buffer.from(data);
|
const buf = Buffer.from(data);
|
||||||
fs.writeFileSync(dbPath, buf);
|
// 先写临时文件再 rename,避免写一半崩溃导致数据库损坏
|
||||||
} catch { /* 静默失败,不阻断主流程 */ }
|
const tmpPath = dbPath + '.tmp';
|
||||||
|
fs.writeFileSync(tmpPath, buf);
|
||||||
|
fs.renameSync(tmpPath, dbPath);
|
||||||
|
} catch (err) {
|
||||||
|
// 记录到启动日志文件(console.error 会被 main.ts 的 uncaughtException 捕获)
|
||||||
|
console.error(`[SQLite persist] 写入失败: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化数据库(异步,需加载 WASM) */
|
/** 初始化数据库(异步,需加载 WASM) */
|
||||||
@@ -200,6 +206,7 @@ export async function initDatabase(): Promise<SQL.Database> {
|
|||||||
action_input TEXT,
|
action_input TEXT,
|
||||||
observation TEXT,
|
observation TEXT,
|
||||||
loop_count INTEGER,
|
loop_count INTEGER,
|
||||||
|
error_pattern TEXT,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
@@ -210,7 +217,8 @@ export async function initDatabase(): Promise<SQL.Database> {
|
|||||||
// 兼容迁移:为已有 messages 表补充 attachments 列(文件/视频等附件 JSON)
|
// 兼容迁移:为已有 messages 表补充 attachments 列(文件/视频等附件 JSON)
|
||||||
try { db.run('ALTER TABLE messages ADD COLUMN attachments TEXT'); } catch { /* 列已存在,忽略 */ }
|
try { db.run('ALTER TABLE messages ADD COLUMN attachments TEXT'); } catch { /* 列已存在,忽略 */ }
|
||||||
|
|
||||||
// 兼容迁移:为已有 messages 表补充 prompt_eval_count 列
|
// 兼容迁移:v0.12.0 — 为已有 traces 表补充 error_pattern 列
|
||||||
|
try { db.run('ALTER TABLE traces ADD COLUMN error_pattern TEXT'); } catch { /* 列已存在,忽略 */ }
|
||||||
|
|
||||||
|
|
||||||
// 尝试创建 FTS5 全文搜索(可选,sql.js 默认 WASM 可能不包含 FTS5)
|
// 尝试创建 FTS5 全文搜索(可选,sql.js 默认 WASM 可能不包含 FTS5)
|
||||||
@@ -305,6 +313,7 @@ export interface TraceRow {
|
|||||||
action_input: string | null;
|
action_input: string | null;
|
||||||
observation: string | null;
|
observation: string | null;
|
||||||
loop_count: number | null;
|
loop_count: number | null;
|
||||||
|
error_pattern: string | null;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,9 +490,9 @@ export function getToolCallsBySession(sessionId: string): ToolCallRow[] {
|
|||||||
// ─── Traces CRUD ───
|
// ─── Traces CRUD ───
|
||||||
|
|
||||||
export function saveTrace(trace: TraceRow): string {
|
export function saveTrace(trace: TraceRow): string {
|
||||||
runExec(getDb(), `INSERT OR REPLACE INTO traces (id, session_id, step_index, thought, action, action_input, observation, loop_count, created_at)
|
runExec(getDb(), `INSERT OR REPLACE INTO traces (id, session_id, step_index, thought, action, action_input, observation, loop_count, error_pattern, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[trace.id, trace.session_id, trace.step_index, trace.thought, trace.action, trace.action_input, trace.observation, trace.loop_count, trace.created_at]
|
[trace.id, trace.session_id, trace.step_index, trace.thought, trace.action, trace.action_input, trace.observation, trace.loop_count, trace.error_pattern, trace.created_at]
|
||||||
);
|
);
|
||||||
persist();
|
persist();
|
||||||
return trace.id;
|
return trace.id;
|
||||||
|
|||||||
+7
-1
@@ -55,7 +55,7 @@ import {
|
|||||||
} from './tool-handlers.js';
|
} from './tool-handlers.js';
|
||||||
import { browserOpen, browserScreenshot, browserEvaluate, browserExtract, browserClick, browserType, browserScroll, browserClose, browserWait } from './browser.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 { startServer, stopServer, stopAllServers, callTool, getAllTools, getServerStatuses, refreshTools, setMCPTimeout } from './mcp-manager.js';
|
||||||
import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js';
|
import { getAllowedDirs, getBlockedDirs, setAllowedDirs, checkPathAllowed } from './tool-security.js';
|
||||||
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js';
|
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js';
|
||||||
import { setHTTPTimeout } from './tool-handlers-system.js';
|
import { setHTTPTimeout } from './tool-handlers-system.js';
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
@@ -132,6 +132,8 @@ export async function setupIPC(): Promise<void> {
|
|||||||
|
|
||||||
ipcMain.handle('fs:readFile', async (_, filePath: string) => {
|
ipcMain.handle('fs:readFile', async (_, filePath: string) => {
|
||||||
try {
|
try {
|
||||||
|
const check = checkPathAllowed(path.resolve(filePath), 'read');
|
||||||
|
if (!check.ok) return { success: false, error: check.reason };
|
||||||
const content = await fs.promises.readFile(filePath, 'utf-8');
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
return { success: true, content, name: path.basename(filePath) };
|
return { success: true, content, name: path.basename(filePath) };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -141,6 +143,8 @@ export async function setupIPC(): Promise<void> {
|
|||||||
|
|
||||||
ipcMain.handle('fs:readFileBase64', async (_, filePath: string) => {
|
ipcMain.handle('fs:readFileBase64', async (_, filePath: string) => {
|
||||||
try {
|
try {
|
||||||
|
const check = checkPathAllowed(path.resolve(filePath), 'read');
|
||||||
|
if (!check.ok) return { success: false, error: check.reason };
|
||||||
const buf = await fs.promises.readFile(filePath);
|
const buf = await fs.promises.readFile(filePath);
|
||||||
return { success: true, content: buf.toString('base64'), size: buf.length, name: path.basename(filePath) };
|
return { success: true, content: buf.toString('base64'), size: buf.length, name: path.basename(filePath) };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -150,6 +154,8 @@ export async function setupIPC(): Promise<void> {
|
|||||||
|
|
||||||
ipcMain.handle('fs:writeFile', async (_, filePath: string, content: string, encoding?: string) => {
|
ipcMain.handle('fs:writeFile', async (_, filePath: string, content: string, encoding?: string) => {
|
||||||
try {
|
try {
|
||||||
|
const check = checkPathAllowed(path.resolve(filePath), 'write');
|
||||||
|
if (!check.ok) return { success: false, error: check.reason };
|
||||||
await fs.promises.writeFile(filePath, content, (encoding as BufferEncoding) || 'utf-8');
|
await fs.promises.writeFile(filePath, content, (encoding as BufferEncoding) || 'utf-8');
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ export function getAllTools(): MCPToolInfo[] {
|
|||||||
tools.push({
|
tools.push({
|
||||||
serverName,
|
serverName,
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
fullName: `mcp_${serverName}_${tool.name}`,
|
fullName: `mcp_${serverName}__${tool.name}`,
|
||||||
description: `[MCP:${serverName}] ${tool.description || ''}`,
|
description: `[MCP:${serverName}] ${tool.description || ''}`,
|
||||||
inputSchema: tool.inputSchema
|
inputSchema: tool.inputSchema
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -101,7 +101,7 @@ export function createMenu(): void {
|
|||||||
dialog.showMessageBox(mainWindow!, {
|
dialog.showMessageBox(mainWindow!, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: '关于 Metona Ollama',
|
title: '关于 Metona Ollama',
|
||||||
message: 'Metona Ollama Desktop v0.11.11',
|
message: 'Metona Ollama Desktop v0.12.3',
|
||||||
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
|
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
|
||||||
icon: getIconPath()
|
icon: getIconPath()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -357,7 +357,8 @@ export async function handleSearchFiles(params: { path: string; query: string; s
|
|||||||
if (searchType === 'filename' || searchType === 'both') {
|
if (searchType === 'filename' || searchType === 'both') {
|
||||||
const fileName = path.basename(filePath);
|
const fileName = path.basename(filePath);
|
||||||
const nameToCheck = caseSensitive ? fileName : fileName.toLowerCase();
|
const nameToCheck = caseSensitive ? fileName : fileName.toLowerCase();
|
||||||
if (nameToCheck.includes(query)) {
|
const queryToCheck = caseSensitive ? query : query.toLowerCase();
|
||||||
|
if (nameToCheck.includes(queryToCheck)) {
|
||||||
results.push({ path: filePath, matches: [{ line: 0, text: `[文件名匹配] ${fileName}` }] });
|
results.push({ path: filePath, matches: [{ line: 0, text: `[文件名匹配] ${fileName}` }] });
|
||||||
totalMatches++;
|
totalMatches++;
|
||||||
continue;
|
continue;
|
||||||
@@ -471,6 +472,9 @@ export async function handleEditFile(params: { path: string; old_text: string; n
|
|||||||
if (!allowed.ok) return { success: false, error: allowed.reason };
|
if (!allowed.ok) return { success: false, error: allowed.reason };
|
||||||
|
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
|
if (content.length > 5 * 1024 * 1024) {
|
||||||
|
return { success: false, error: `文件过大 (${(content.length / 1024 / 1024).toFixed(1)}MB),最大支持 5MB` };
|
||||||
|
}
|
||||||
|
|
||||||
let replaceCount: number;
|
let replaceCount: number;
|
||||||
let newContent: string;
|
let newContent: string;
|
||||||
|
|||||||
@@ -375,7 +375,34 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
|
|||||||
return { success: false, error: `页面过大 (${(parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB),超过 10MB 限制` };
|
return { success: false, error: `页面过大 (${(parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB),超过 10MB 限制` };
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = await resp.text();
|
// 无 content-length 头时,流式读取并限制最大体积(防 OOM)
|
||||||
|
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
let text: string;
|
||||||
|
if (!contentLength && resp.body) {
|
||||||
|
const reader = resp.body.getReader();
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
let totalSize = 0;
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
totalSize += value.length;
|
||||||
|
if (totalSize > MAX_BODY_SIZE) {
|
||||||
|
reader.cancel();
|
||||||
|
return { success: false, error: `页面过大 (超过 10MB),已中止下载` };
|
||||||
|
}
|
||||||
|
chunks.push(value);
|
||||||
|
}
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
text = decoder.decode(Buffer.concat(chunks));
|
||||||
|
} else if (!contentLength) {
|
||||||
|
// 无 body stream 回退,但仍需限制
|
||||||
|
text = await resp.text();
|
||||||
|
if (text.length > MAX_BODY_SIZE) {
|
||||||
|
return { success: false, error: `页面过大 (${(text.length / 1024 / 1024).toFixed(1)}MB),超过 10MB 限制` };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = await resp.text();
|
||||||
|
}
|
||||||
const isHTML = contentType.includes('html');
|
const isHTML = contentType.includes('html');
|
||||||
if (isHTML) {
|
if (isHTML) {
|
||||||
text = htmlToText(text);
|
text = htmlToText(text);
|
||||||
@@ -1163,6 +1190,7 @@ export async function handleDownloadFile(params: { url: string; destination: str
|
|||||||
export async function handleCompress(params: { action: string; path: string; destination?: string; format?: string }): Promise<ToolResult> {
|
export async function handleCompress(params: { action: string; path: string; destination?: string; format?: string }): Promise<ToolResult> {
|
||||||
try {
|
try {
|
||||||
const format = params.format || 'tar.gz';
|
const format = params.format || 'tar.gz';
|
||||||
|
const isWin = process.platform === 'win32';
|
||||||
|
|
||||||
if (params.action === 'create') {
|
if (params.action === 'create') {
|
||||||
const srcPath = resolvePath(params.path);
|
const srcPath = resolvePath(params.path);
|
||||||
@@ -1176,13 +1204,28 @@ export async function handleCompress(params: { action: string; path: string; des
|
|||||||
if (!destCheck.ok) return { success: false, error: destCheck.reason };
|
if (!destCheck.ok) return { success: false, error: destCheck.reason };
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const cmd = format === 'zip'
|
let child;
|
||||||
? `zip -r "${destPath}" "${path.basename(srcPath)}"`
|
const srcName = path.basename(srcPath);
|
||||||
: `tar czf "${destPath}" "${path.basename(srcPath)}"`;
|
const cwd = path.dirname(srcPath);
|
||||||
const proc = spawn('bash', ['-c', cmd], { cwd: path.dirname(srcPath), stdio: ['pipe', 'pipe', 'pipe'] });
|
|
||||||
|
if (format === 'zip') {
|
||||||
|
if (isWin) {
|
||||||
|
// Windows: PowerShell Compress-Archive
|
||||||
|
child = spawn('powershell', [
|
||||||
|
'-NoProfile', '-Command',
|
||||||
|
`Compress-Archive -Path '${srcName}' -DestinationPath '${destPath}' -Force`
|
||||||
|
], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
} else {
|
||||||
|
child = spawn('zip', ['-r', destPath, srcName], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// tar.gz: Windows 10+ has tar, Unix has tar
|
||||||
|
child = spawn('tar', ['czf', destPath, srcName], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
}
|
||||||
|
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
||||||
proc.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
fs.stat(destPath).then(s => {
|
fs.stat(destPath).then(s => {
|
||||||
resolve({ success: true, action: 'create', archive: destPath, size: s.size });
|
resolve({ success: true, action: 'create', archive: destPath, size: s.size });
|
||||||
@@ -1191,7 +1234,7 @@ export async function handleCompress(params: { action: string; path: string; des
|
|||||||
resolve({ success: false, error: stderr || `压缩失败 (exit ${code})` });
|
resolve({ success: false, error: stderr || `压缩失败 (exit ${code})` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
proc.on('error', (err) => resolve({ success: false, error: err.message }));
|
child.on('error', (err) => resolve({ success: false, error: err.message }));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// extract
|
// extract
|
||||||
@@ -1206,20 +1249,31 @@ export async function handleCompress(params: { action: string; path: string; des
|
|||||||
await fs.mkdir(destDir, { recursive: true });
|
await fs.mkdir(destDir, { recursive: true });
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
let child;
|
||||||
const isZip = archivePath.endsWith('.zip');
|
const isZip = archivePath.endsWith('.zip');
|
||||||
const cmd = isZip
|
|
||||||
? `unzip -o "${archivePath}" -d "${destDir}"`
|
if (isZip) {
|
||||||
: `tar xzf "${archivePath}" -C "${destDir}"`;
|
if (isWin) {
|
||||||
const proc = spawn('bash', ['-c', cmd], { stdio: ['pipe', 'pipe', 'pipe'] });
|
child = spawn('powershell', [
|
||||||
|
'-NoProfile', '-Command',
|
||||||
|
`Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`
|
||||||
|
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
} else {
|
||||||
|
child = spawn('unzip', ['-o', archivePath, '-d', destDir], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
child = spawn('tar', ['xzf', archivePath, '-C', destDir], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
}
|
||||||
|
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
||||||
proc.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
resolve(code === 0
|
resolve(code === 0
|
||||||
? { success: true, action: 'extract', destination: destDir }
|
? { success: true, action: 'extract', destination: destDir }
|
||||||
: { success: false, error: stderr || `解压失败 (exit ${code})` }
|
: { success: false, error: stderr || `解压失败 (exit ${code})` }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
proc.on('error', (err) => resolve({ success: false, error: err.message }));
|
child.on('error', (err) => resolve({ success: false, error: err.message }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -21,15 +21,28 @@ let allowedDirs: string[] = [
|
|||||||
|
|
||||||
/** 永久禁止的目录 */
|
/** 永久禁止的目录 */
|
||||||
const BLOCKED_DIRS: string[] = [
|
const BLOCKED_DIRS: string[] = [
|
||||||
|
// Linux/macOS 系统目录
|
||||||
'/etc', '/sys', '/proc', '/dev', '/boot', '/root',
|
'/etc', '/sys', '/proc', '/dev', '/boot', '/root',
|
||||||
'C:\\Windows', 'C:\\Program Files', 'C:\\ProgramData',
|
'/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/lib',
|
||||||
|
'/var/log', '/var/run', '/var/spool', '/var/mail',
|
||||||
|
'/tmp/.X11-unix', '/tmp/.font-unix',
|
||||||
|
// Windows 系统目录
|
||||||
|
'C:\\Windows', 'C:\\Windows\\System32', 'C:\\Windows\\SysWOW64',
|
||||||
|
'C:\\Windows\\System', 'C:\\Windows\\WinSxS',
|
||||||
|
'C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\ProgramData',
|
||||||
|
// 用户敏感目录
|
||||||
path.join(HOME, '.ssh'),
|
path.join(HOME, '.ssh'),
|
||||||
path.join(HOME, '.gnupg'),
|
path.join(HOME, '.gnupg'),
|
||||||
path.join(HOME, '.aws'),
|
path.join(HOME, '.aws'),
|
||||||
|
path.join(HOME, '.azure'),
|
||||||
|
path.join(HOME, '.config'),
|
||||||
|
path.join(HOME, '.kube'),
|
||||||
|
path.join(HOME, 'AppData'),
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 命令黑名单 */
|
/** 命令黑名单 */
|
||||||
const BLOCKED_COMMANDS: string[] = [
|
const BLOCKED_COMMANDS: string[] = [
|
||||||
|
// POSIX 危险命令
|
||||||
'rm -rf /', 'rm -rf /*', ':(){ :|:& };:',
|
'rm -rf /', 'rm -rf /*', ':(){ :|:& };:',
|
||||||
'mkfs', 'dd if=', 'wipefs', 'shred',
|
'mkfs', 'dd if=', 'wipefs', 'shred',
|
||||||
'shutdown', 'reboot', 'poweroff', 'halt',
|
'shutdown', 'reboot', 'poweroff', 'halt',
|
||||||
@@ -39,6 +52,15 @@ const BLOCKED_COMMANDS: string[] = [
|
|||||||
'systemctl enable', 'systemctl disable',
|
'systemctl enable', 'systemctl disable',
|
||||||
'curl | sh', 'wget | sh', 'curl | bash', 'wget | bash',
|
'curl | sh', 'wget | sh', 'curl | bash', 'wget | bash',
|
||||||
'eval', 'exec >',
|
'eval', 'exec >',
|
||||||
|
// Windows 危险命令
|
||||||
|
'rmdir /s', 'del /f', 'del /s', 'del /q',
|
||||||
|
'format', 'diskpart',
|
||||||
|
'reg add', 'reg delete', 'reg import', 'reg export',
|
||||||
|
'sc stop', 'sc delete', 'sc config',
|
||||||
|
'net user', 'net localgroup', 'net share',
|
||||||
|
'icacls', 'takeown', 'cacls',
|
||||||
|
'bcdedit', 'wmic', 'rundll32',
|
||||||
|
'schtasks /create', 'schtasks /delete',
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface CheckResult {
|
export interface CheckResult {
|
||||||
|
|||||||
+18
-3
@@ -142,7 +142,12 @@ export function killProcess(id: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// Windows 不支持 POSIX 信号,需用 taskkill 强制终止进程树
|
||||||
|
if (process.platform === 'win32' && proc.pid) {
|
||||||
|
spawn('taskkill', ['/PID', String(proc.pid), '/T', '/F']);
|
||||||
|
} else {
|
||||||
proc.kill('SIGTERM');
|
proc.kill('SIGTERM');
|
||||||
|
}
|
||||||
activeProcesses.delete(id);
|
activeProcesses.delete(id);
|
||||||
sendLog('info', `🖥️ 进程已终止`, `ID: ${id}`);
|
sendLog('info', `🖥️ 进程已终止`, `ID: ${id}`);
|
||||||
return true;
|
return true;
|
||||||
@@ -156,7 +161,13 @@ export function killProcess(id: string): boolean {
|
|||||||
/** 停止所有进程(窗口关闭时调用) */
|
/** 停止所有进程(窗口关闭时调用) */
|
||||||
export function killAllProcesses(): void {
|
export function killAllProcesses(): void {
|
||||||
for (const [id, proc] of activeProcesses) {
|
for (const [id, proc] of activeProcesses) {
|
||||||
try { proc.kill('SIGTERM'); } catch { /* ignore */ }
|
try {
|
||||||
|
if (process.platform === 'win32' && proc.pid) {
|
||||||
|
spawn('taskkill', ['/PID', String(proc.pid), '/T', '/F']);
|
||||||
|
} else {
|
||||||
|
proc.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
activeProcesses.clear();
|
activeProcesses.clear();
|
||||||
}
|
}
|
||||||
@@ -166,8 +177,12 @@ export function listWorkspaceDir(dirPath?: string): { success: boolean; entries?
|
|||||||
try {
|
try {
|
||||||
const targetDir = dirPath ? path.resolve(dirPath) : _workspaceDir;
|
const targetDir = dirPath ? path.resolve(dirPath) : _workspaceDir;
|
||||||
|
|
||||||
// 安全检查:必须在 workspace 范围内
|
// 安全检查:必须在 workspace 范围内(Windows 路径大小写不敏感)
|
||||||
if (!targetDir.startsWith(_workspaceDir) && !targetDir.startsWith(DEFAULT_WORKSPACE_DIR)) {
|
const isWin = process.platform === 'win32';
|
||||||
|
const targetCheck = isWin ? targetDir.toLowerCase() : targetDir;
|
||||||
|
const wsCheck = isWin ? _workspaceDir.toLowerCase() : _workspaceDir;
|
||||||
|
const defaultCheck = isWin ? DEFAULT_WORKSPACE_DIR.toLowerCase() : DEFAULT_WORKSPACE_DIR;
|
||||||
|
if (!targetCheck.startsWith(wsCheck) && !targetCheck.startsWith(defaultCheck)) {
|
||||||
return { success: false, error: '只能浏览工作空间目录内的文件' };
|
return { success: false, error: '只能浏览工作空间目录内的文件' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ function renderToolCallCard(tc: ToolCallRecord): string {
|
|||||||
edit_file: '✂️', get_file_info: 'ℹ️', tree: '🌳', download_file: '⬇️',
|
edit_file: '✂️', get_file_info: 'ℹ️', tree: '🌳', download_file: '⬇️',
|
||||||
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
||||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||||
memory_search: '🧠', memory_add: '💾', session_list: '📋', session_read: '📖', spawn_task: '🤖',
|
memory_search: '🧠', memory_add: '💾', session_list: '📋', session_read: '📖', spawn_task: '🤖', plan_track: '📋',
|
||||||
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
||||||
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
||||||
};
|
};
|
||||||
@@ -265,7 +265,7 @@ function renderToolCallCard(tc: ToolCallRecord): string {
|
|||||||
edit_file: '编辑文件', get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
|
edit_file: '编辑文件', get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
|
||||||
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
||||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||||
memory_search: '搜索记忆', memory_add: '添加记忆', session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
|
memory_search: '搜索记忆', memory_add: '添加记忆', session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派', plan_track: '计划追踪',
|
||||||
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
||||||
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,11 +55,12 @@ export function initHistoryModal(): void {
|
|||||||
const target = (e.target as HTMLElement).closest('[data-id]') as HTMLElement;
|
const target = (e.target as HTMLElement).closest('[data-id]') as HTMLElement;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const sessionId = target.dataset.id!;
|
const sessionId = target.dataset.id!;
|
||||||
const db = state.get<ChatDB>(KEYS.DB);
|
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||||
|
if (!db) return;
|
||||||
|
|
||||||
if (target.classList.contains('btn-delete-session')) {
|
if (target.classList.contains('btn-delete-session')) {
|
||||||
if (await showConfirm('确定删除此会话?', '删除会话')) {
|
if (await showConfirm('确定删除此会话?', '删除会话')) {
|
||||||
await db!.deleteSession(sessionId);
|
await db.deleteSession(sessionId);
|
||||||
logSession('删除', sessionId);
|
logSession('删除', sessionId);
|
||||||
const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
|
const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
|
||||||
if (currentSession && currentSession.id === sessionId) {
|
if (currentSession && currentSession.id === sessionId) {
|
||||||
@@ -68,13 +69,13 @@ export function initHistoryModal(): void {
|
|||||||
loadHistory();
|
loadHistory();
|
||||||
}
|
}
|
||||||
} else if (target.classList.contains('btn-export-md')) {
|
} else if (target.classList.contains('btn-export-md')) {
|
||||||
const session = await db!.getSession(sessionId);
|
const session = await db.getSession(sessionId);
|
||||||
if (session) exportAsMarkdown(session);
|
if (session) exportAsMarkdown(session);
|
||||||
} else if (target.classList.contains('btn-export-html')) {
|
} else if (target.classList.contains('btn-export-html')) {
|
||||||
const session = await db!.getSession(sessionId);
|
const session = await db.getSession(sessionId);
|
||||||
if (session) exportAsHtml(session);
|
if (session) exportAsHtml(session);
|
||||||
} else if (target.classList.contains('btn-export-txt')) {
|
} else if (target.classList.contains('btn-export-txt')) {
|
||||||
const session = await db!.getSession(sessionId);
|
const session = await db.getSession(sessionId);
|
||||||
if (session) exportAsTxt(session);
|
if (session) exportAsTxt(session);
|
||||||
} else if (target.classList.contains('history-info') || target.classList.contains('history-item')) {
|
} else if (target.classList.contains('history-info') || target.classList.contains('history-item')) {
|
||||||
loadHistorySession(sessionId);
|
loadHistorySession(sessionId);
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ import { estimateTokens } from '../services/context-manager.js';
|
|||||||
import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js';
|
import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js';
|
||||||
import { showToolConfirm } from './tool-confirm-modal.js';
|
import { showToolConfirm } from './tool-confirm-modal.js';
|
||||||
import { logInfo, logStream, logError, logSuccess, logWarn, resetVideoProgress, updateVideoProgress } from '../services/log-service.js';
|
import { logInfo, logStream, logError, logSuccess, logWarn, resetVideoProgress, updateVideoProgress } from '../services/log-service.js';
|
||||||
import type { ChatSession, ChatMessage, OllamaStreamChunk, FileContent, ChatFile, ToolCallRecord } from '../types.js';
|
import type { ChatSession, ChatMessage, OllamaStreamChunk, FileContent, ChatFile, ToolCallRecord, AgentMode } from '../types.js';
|
||||||
|
|
||||||
let chatInputEl: HTMLTextAreaElement;
|
let chatInputEl: HTMLTextAreaElement;
|
||||||
let btnSendEl: HTMLButtonElement;
|
let btnSendEl: HTMLButtonElement;
|
||||||
let imagePreviewEl: HTMLElement;
|
let imagePreviewEl: HTMLElement;
|
||||||
let filePreviewEl: HTMLElement;
|
let filePreviewEl: HTMLElement;
|
||||||
let videoPreviewEl: HTMLElement;
|
let videoPreviewEl: HTMLElement;
|
||||||
|
let planToggleCheckbox: HTMLInputElement;
|
||||||
let pendingImages: Array<{ name: string; base64: string }> = [];
|
let pendingImages: Array<{ name: string; base64: string }> = [];
|
||||||
let pendingFiles: Array<{ name: string; content: string; language: string; size: number }> = [];
|
let pendingFiles: Array<{ name: string; content: string; language: string; size: number }> = [];
|
||||||
let pendingVideoFrames: Array<{ name: string; base64: string; timestampSeconds: number }> = [];
|
let pendingVideoFrames: Array<{ name: string; base64: string; timestampSeconds: number }> = [];
|
||||||
@@ -150,6 +151,21 @@ export function initInputArea(): void {
|
|||||||
renderFilePreviews();
|
renderFilePreviews();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── v0.12.0 Plan Mode 切换 ──
|
||||||
|
const planToggleEl = document.querySelector('#togglePlan') as HTMLInputElement;
|
||||||
|
if (planToggleEl) {
|
||||||
|
planToggleCheckbox = planToggleEl;
|
||||||
|
planToggleEl.addEventListener('change', () => {
|
||||||
|
const mode: AgentMode = planToggleEl.checked ? 'plan' : 'auto';
|
||||||
|
state.set('agentMode', mode);
|
||||||
|
// Plan Mode 切换时注册/移除 plan_track 工具
|
||||||
|
import('../services/tool-registry.js').then(({ setPlanModeActive }) => {
|
||||||
|
setPlanModeActive(mode === 'plan');
|
||||||
|
});
|
||||||
|
logInfo(`Agent 模式切换: ${mode === 'plan' ? 'Plan(先规划后执行)' : 'Auto(直接执行)'}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoResizeTextarea(): void {
|
function autoResizeTextarea(): void {
|
||||||
@@ -211,13 +227,14 @@ function compressImage(base64: string): Promise<{ compressed: string; origBytes:
|
|||||||
|
|
||||||
async function handleImagePaths(paths: string[]): Promise<void> {
|
async function handleImagePaths(paths: string[]): Promise<void> {
|
||||||
const bridge = getBridge();
|
const bridge = getBridge();
|
||||||
|
if (!bridge) return;
|
||||||
const maxSize = 20 * 1024 * 1024;
|
const maxSize = 20 * 1024 * 1024;
|
||||||
|
|
||||||
// 并行读取所有图片
|
// 并行读取所有图片
|
||||||
const results = await Promise.all(paths.map(async (filePath) => {
|
const results = await Promise.all(paths.map(async (filePath) => {
|
||||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||||
try {
|
try {
|
||||||
const result = await bridge!.fs.readFileBase64(filePath);
|
const result = await bridge.fs.readFileBase64(filePath);
|
||||||
if (!result.success) return { name, error: result.error };
|
if (!result.success) return { name, error: result.error };
|
||||||
const size = result.size ?? 0;
|
const size = result.size ?? 0;
|
||||||
if (size > maxSize) return { name, error: `超过 20MB 限制` };
|
if (size > maxSize) return { name, error: `超过 20MB 限制` };
|
||||||
@@ -372,13 +389,14 @@ function renderImagePreviews(): void {
|
|||||||
|
|
||||||
async function handleTextFilePaths(paths: string[]): Promise<void> {
|
async function handleTextFilePaths(paths: string[]): Promise<void> {
|
||||||
const bridge = getBridge();
|
const bridge = getBridge();
|
||||||
|
if (!bridge) return;
|
||||||
|
|
||||||
// 并行读取所有文件
|
// 并行读取所有文件
|
||||||
const results = await Promise.all(paths.map(async (filePath) => {
|
const results = await Promise.all(paths.map(async (filePath) => {
|
||||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||||
if (!isTextFile(name)) return { name, error: `不是支持的文本/代码文件` };
|
if (!isTextFile(name)) return { name, error: `不是支持的文本/代码文件` };
|
||||||
try {
|
try {
|
||||||
const result = await bridge!.fs.readFile(filePath);
|
const result = await bridge.fs.readFile(filePath);
|
||||||
if (!result.success) return { name, error: result.error };
|
if (!result.success) return { name, error: result.error };
|
||||||
const content = result.content as string;
|
const content = result.content as string;
|
||||||
const size = new TextEncoder().encode(content).length;
|
const size = new TextEncoder().encode(content).length;
|
||||||
@@ -561,6 +579,19 @@ async function handleRetry(): Promise<void> {
|
|||||||
updateMessageToolRecord(name, 'error', { success: false, error });
|
updateMessageToolRecord(name, 'error', { success: false, error });
|
||||||
},
|
},
|
||||||
onConfirmTool: async (call) => showToolConfirm(call),
|
onConfirmTool: async (call) => showToolConfirm(call),
|
||||||
|
onPlanReady: async (plan: string, steps: string[]) => {
|
||||||
|
try {
|
||||||
|
const { showHtmlConfirm } = await import('./prompt-modal.js');
|
||||||
|
const stepsHtml = steps.length > 0
|
||||||
|
? '<br><br><strong>📋 执行步骤:</strong><br>' + steps.map((s: string, i: number) => ` ${i + 1}. ${s}`).join('<br>')
|
||||||
|
: '';
|
||||||
|
const html = `<div>${safeMarkdown(plan.slice(0, 1500))}${plan.length > 1500 ? '<br>…' : ''}${stepsHtml}</div>`;
|
||||||
|
return showHtmlConfirm(html, '📋 Plan Mode — 确认执行计划', true);
|
||||||
|
} catch (err) {
|
||||||
|
logWarn('Plan Mode: 弹窗加载失败,自动批准', (err as Error).message);
|
||||||
|
return true; // 加载失败时自动批准,不阻断执行
|
||||||
|
}
|
||||||
|
},
|
||||||
onDone: async (finalContent, toolRecords, loopStats) => {
|
onDone: async (finalContent, toolRecords, loopStats) => {
|
||||||
retryContent = finalContent;
|
retryContent = finalContent;
|
||||||
if (retryIterations > 0) {
|
if (retryIterations > 0) {
|
||||||
@@ -1237,6 +1268,24 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
|||||||
onConfirmTool: async (call) => {
|
onConfirmTool: async (call) => {
|
||||||
return showToolConfirm(call);
|
return showToolConfirm(call);
|
||||||
},
|
},
|
||||||
|
/** v0.12.1: Plan Mode — 计划生成后等待用户确认(Markdown 渲染) */
|
||||||
|
onPlanReady: async (plan: string, steps: string[]) => {
|
||||||
|
try {
|
||||||
|
const { showHtmlConfirm } = await import('./prompt-modal.js');
|
||||||
|
const stepsHtml = steps.length > 0
|
||||||
|
? '<br><br><strong>📋 执行步骤:</strong><br>' + steps.map((s, i) => ` ${i + 1}. ${s}`).join('<br>')
|
||||||
|
: '';
|
||||||
|
const html = `<div>${safeMarkdown(plan.slice(0, 1500))}${plan.length > 1500 ? '<br>…' : ''}${stepsHtml}</div>`;
|
||||||
|
const approved = await showHtmlConfirm(html, '📋 Plan Mode — 确认执行计划', true);
|
||||||
|
if (approved) {
|
||||||
|
logInfo('Plan Mode: 用户批准计划');
|
||||||
|
}
|
||||||
|
return approved;
|
||||||
|
} catch (err) {
|
||||||
|
logWarn('Plan Mode: 弹窗加载失败,自动批准', (err as Error).message);
|
||||||
|
return true; // 加载失败时自动批准,不阻断执行
|
||||||
|
}
|
||||||
|
},
|
||||||
onDone: async (finalContent, toolRecords, loopStats) => {
|
onDone: async (finalContent, toolRecords, loopStats) => {
|
||||||
assistantContent = finalContent;
|
assistantContent = finalContent;
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,23 @@ export function showPrompt(options: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 显示自定义 confirm(支持 HTML),返回 true/false */
|
||||||
|
export function showHtmlConfirm(html: string, title = '确认', wide = false): Promise<boolean> {
|
||||||
|
ensureModal();
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolveFn = (val) => resolve(val !== null);
|
||||||
|
titleEl!.textContent = title;
|
||||||
|
bodyEl!.innerHTML = `<div style="color:var(--text-secondary);font-size:13px;line-height:1.6;">${html}</div>`;
|
||||||
|
inputEl = null;
|
||||||
|
overlayEl!.style.display = '';
|
||||||
|
// 宽屏模式:Plan Mode 确认框需要更大空间
|
||||||
|
const modalEl = overlayEl!.querySelector('.modal') as HTMLElement;
|
||||||
|
if (modalEl) modalEl.style.maxWidth = wide ? '700px' : '';
|
||||||
|
okBtn!.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 显示自定义 confirm,返回 true/false */
|
/** 显示自定义 confirm,返回 true/false */
|
||||||
export function showConfirm(message: string, title = '确认'): Promise<boolean> {
|
export function showConfirm(message: string, title = '确认'): Promise<boolean> {
|
||||||
ensureModal();
|
ensureModal();
|
||||||
|
|||||||
@@ -182,11 +182,13 @@ export function initSettingsModal(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('#btnReleaseVRAM')!.addEventListener('click', async () => {
|
document.querySelector('#btnReleaseVRAM')!.addEventListener('click', async () => {
|
||||||
const api = state.get<OllamaAPI>(KEYS.API);
|
const api = state.get<OllamaAPI | null>(KEYS.API, null);
|
||||||
const model = (document.querySelector('#modelSelect') as HTMLSelectElement).value;
|
if (!api) { showToast('Ollama API 未连接', 'warning'); return; }
|
||||||
|
const sel = document.querySelector('#modelSelect') as HTMLSelectElement | null;
|
||||||
|
const model = sel?.value || '';
|
||||||
logInfo('释放显存', model || '所有模型');
|
logInfo('释放显存', model || '所有模型');
|
||||||
try {
|
try {
|
||||||
await api!.chat({ model: model || 'any', messages: [], keep_alive: 0 });
|
await api.chat({ model: model || 'any', messages: [], keep_alive: 0 });
|
||||||
showToast('显存已释放', 'success');
|
showToast('显存已释放', 'success');
|
||||||
logSuccess('显存已释放');
|
logSuccess('显存已释放');
|
||||||
updateRunningModels();
|
updateRunningModels();
|
||||||
@@ -198,9 +200,10 @@ export function initSettingsModal(): void {
|
|||||||
|
|
||||||
document.querySelector('#btnClearAllHistory')!.addEventListener('click', async () => {
|
document.querySelector('#btnClearAllHistory')!.addEventListener('click', async () => {
|
||||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||||
|
if (!db) { showToast('数据库未就绪', 'warning'); return; }
|
||||||
if (await showConfirm('确定清空所有历史记录?此操作不可恢复!', '清空历史')) {
|
if (await showConfirm('确定清空所有历史记录?此操作不可恢复!', '清空历史')) {
|
||||||
logWarn('清空所有历史记录');
|
logWarn('清空所有历史记录');
|
||||||
await db!.clearAll();
|
await db.clearAll();
|
||||||
(document.querySelector('#btnNewChat') as HTMLElement).click();
|
(document.querySelector('#btnNewChat') as HTMLElement).click();
|
||||||
showToast('已清空所有历史记录', 'success');
|
showToast('已清空所有历史记录', 'success');
|
||||||
logSuccess('历史记录已清空');
|
logSuccess('历史记录已清空');
|
||||||
@@ -305,6 +308,19 @@ export function openSettingsModal(): void {
|
|||||||
populateSubAgentModels();
|
populateSubAgentModels();
|
||||||
updateMemoryVectorStatus();
|
updateMemoryVectorStatus();
|
||||||
loadTimeoutSettings();
|
loadTimeoutSettings();
|
||||||
|
loadWatchdogSetting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载看门狗设置到输入框 */
|
||||||
|
async function loadWatchdogSetting(): Promise<void> {
|
||||||
|
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||||
|
if (!db) return;
|
||||||
|
const ms = await db.getSetting<number>('loopWatchdogMs', 1_800_000);
|
||||||
|
const el = document.querySelector('#inputLoopWatchdog') as HTMLInputElement;
|
||||||
|
if (!el) return;
|
||||||
|
if (ms === 0) el.value = '0';
|
||||||
|
else if (ms > 0) el.value = String(Math.round(ms / 60_000));
|
||||||
|
else el.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeSettingsModal(): void {
|
export function closeSettingsModal(): void {
|
||||||
@@ -494,6 +510,18 @@ const saveSubAgentTimeout = debounce(async () => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
document.querySelector('#inputSubAgentTimeout')!.addEventListener('input', saveSubAgentTimeout);
|
document.querySelector('#inputSubAgentTimeout')!.addEventListener('input', saveSubAgentTimeout);
|
||||||
|
|
||||||
|
// ── v0.12.1 看门狗超时(分钟,0=禁用)──
|
||||||
|
const saveLoopWatchdog = debounce(async () => {
|
||||||
|
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||||
|
const val = (document.querySelector('#inputLoopWatchdog') as HTMLInputElement).value.trim();
|
||||||
|
const minutes = val ? parseInt(val) : -1;
|
||||||
|
const ms = minutes > 0 ? minutes * 60_000 : (minutes === 0 ? 0 : 1_800_000); // 默认30分钟
|
||||||
|
state.set('loopWatchdogMs', ms);
|
||||||
|
if (db) await db.saveSetting('loopWatchdogMs', ms);
|
||||||
|
logSetting('看门狗超时', minutes >= 0 ? (minutes === 0 ? '已禁用' : `${minutes}分钟`) : '默认(30分钟)');
|
||||||
|
}, 500);
|
||||||
|
document.querySelector('#inputLoopWatchdog')!.addEventListener('input', saveLoopWatchdog);
|
||||||
|
|
||||||
function updateMemoryVectorStatus(): void {
|
function updateMemoryVectorStatus(): void {
|
||||||
const statusEl = document.querySelector('#memoryVectorStatus');
|
const statusEl = document.querySelector('#memoryVectorStatus');
|
||||||
if (!statusEl) return;
|
if (!statusEl) return;
|
||||||
@@ -520,7 +548,8 @@ async function importSessions(filePath: string): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const bridge = window.metonaDesktop;
|
const bridge = window.metonaDesktop;
|
||||||
const result = await bridge!.fs.readFileBase64(filePath);
|
if (!bridge) { showToast('仅桌面端支持导入', 'warning'); return; }
|
||||||
|
const result = await bridge.fs.readFileBase64(filePath);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
showToast(`读取文件失败: ${result.error}`, 'error');
|
showToast(`读取文件失败: ${result.error}`, 'error');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -965,7 +965,7 @@ function getToolDisplayName(name: string): string {
|
|||||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||||
memory_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
|
memory_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
|
||||||
|
|
||||||
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
|
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派', plan_track: '计划追踪',
|
||||||
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
||||||
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
||||||
};
|
};
|
||||||
@@ -982,7 +982,7 @@ function getToolIcon(name: string): string {
|
|||||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||||
memory_search: '🧠', memory_add: '💾', memory_replace: '✏️', memory_remove: '🗑️',
|
memory_search: '🧠', memory_add: '💾', memory_replace: '✏️', memory_remove: '🗑️',
|
||||||
|
|
||||||
session_list: '📋', session_read: '📖', spawn_task: '🤖',
|
session_list: '📋', session_read: '📖', spawn_task: '🤖', plan_track: '📋',
|
||||||
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
||||||
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ function isDesktop(): boolean {
|
|||||||
|
|
||||||
/** 桌面端 DB 桥接(isDesktop 已检查,安全断言非空) */
|
/** 桌面端 DB 桥接(isDesktop 已检查,安全断言非空) */
|
||||||
function dbBridge() {
|
function dbBridge() {
|
||||||
return window.metonaDesktop!.db!;
|
const bridge = window.metonaDesktop;
|
||||||
|
if (!bridge?.db) {
|
||||||
|
throw new Error('DB bridge 不可用:非桌面环境或 SQLite 未初始化');
|
||||||
|
}
|
||||||
|
return bridge.db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChatDB {
|
export class ChatDB {
|
||||||
|
|||||||
+24
-5
@@ -28,7 +28,7 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="logo">🦙</span>
|
<span class="logo">🦙</span>
|
||||||
<span class="app-title">Metona Ollama</span>
|
<span class="app-title">Metona Ollama</span>
|
||||||
<span class="app-version">v0.11.11</span>
|
<span class="app-version">v0.12.3</span>
|
||||||
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||||
@@ -174,6 +174,14 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="plan-toggle" id="planToggleWrap" title="Plan 模式:先规划后执行">
|
||||||
|
<input type="checkbox" id="togglePlan">
|
||||||
|
<span class="plan-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="12" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<button class="send-btn" id="btnSend">
|
<button class="send-btn" id="btnSend">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||||
@@ -371,6 +379,11 @@
|
|||||||
<span style="font-size:13px;">超时(秒)</span>
|
<span style="font-size:13px;">超时(秒)</span>
|
||||||
<input type="number" id="inputSubAgentTimeout" class="setting-input" placeholder="300" min="0" max="3600" step="30" style="width:80px;">
|
<input type="number" id="inputSubAgentTimeout" class="setting-input" placeholder="300" min="0" max="3600" step="30" style="width:80px;">
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:flex;gap:16px;align-items:center;margin-top:8px;">
|
||||||
|
<span style="font-size:13px;">看门狗(分)</span>
|
||||||
|
<input type="number" id="inputLoopWatchdog" class="setting-input" placeholder="30" min="0" max="120" step="5" style="width:80px;">
|
||||||
|
<span class="text-muted" style="font-size:11px;">0=禁用,默认30分钟</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label class="setting-label">显存管理</label>
|
<label class="setting-label">显存管理</label>
|
||||||
@@ -435,10 +448,12 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools)</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
|
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools)</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
|
||||||
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 下方 Think 按钮切换,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>上下文长度自动检测</strong> — 切换模型时自动从模型元数据获取实际支持的上下文长度(如 Qwen3.6 27B = 262,144 tokens),无需手动配置</li><li><strong>多模态</strong> — 上传图片或视频(≤10MB),模型需支持 Vision。图片自动压缩,视频 1fps 提取帧序列带时序标注</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,自动剥离注释并按上下文预算智能截断</li><li><strong>AI 回复顶部</strong> — 每条 AI 回复上方展示 📋 系统提示词折叠卡片,可点击查看实际发送给模型的完整上下文</li></ul></div>
|
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 下方 Think 按钮切换,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>上下文长度自动检测</strong> — 切换模型时自动从模型元数据获取实际支持的上下文长度(如 Qwen3.6 27B = 262,144 tokens),无需手动配置</li><li><strong>多模态</strong> — 上传图片或视频(≤10MB),模型需支持 Vision。图片自动压缩,视频 1fps 提取帧序列带时序标注</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,自动剥离注释并按上下文预算智能截断</li><li><strong>AI 回复顶部</strong> — 每条 AI 回复上方展示 📋 系统提示词折叠卡片,可点击查看实际发送给模型的完整上下文</li></ul></div>
|
||||||
<div class="help-section"><h4>🔧 Tool Calling(始终开启)</h4><ul><li>所有消息均通过 <strong>Agent Loop 自主调用本地工具</strong>,像一个本地 Agent,无普通聊天模式</li><li><strong>42 个工具</strong>,分为 9 类:<ul><li><strong>文件系统</strong>(16 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files / compress</li><li><strong>命令执行</strong>(1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式,可配置超时)</li><li><strong>联网搜索</strong>(2 个):web_search(支持 SearXNG 元搜索引擎 JSON API / 四引擎 HTML 解析,双模式可切换)/ web_fetch(反爬headers+UA自动切换+自动回退浏览器渲染)</li><li><strong>Git</strong>(1 个):git(17 个子操作,push/pull/clone 内置60-120s超时保护)</li><li><strong>浏览器控制</strong>(9 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_wait / browser_close</li><li><strong>记忆 & 会话</strong>(6 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read</li><li><strong>子代理</strong>(1 个):spawn_task</li><li><strong>系统工具</strong>(6 个):datetime / calculator / random / uuid / json_format / hash</li></ul></li><li>read_file 支持 binary模式读取/base64解码/字节分页/2000行默认+截断hint自动续读</li><li>write_file 支持 base64写二进制文件/mode(overwrite+append)合并append_file功能/10MB</li><li>edit_file 支持 use_regex 正则替换</li><li>search_files 支持正则表达式搜索(use_regex=true)</li><li>browser_screenshot 支持全页面截图+元素截图</li><li>browser_extract 支持CSS选择器提取特定区域</li><li>new browser_wait 等待元素出现或定时等待</li><li>仅 <code>run_command</code> 支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code>、<code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code>、<code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,上下文使用率>80%时自动缩减到3轮</li><li>独立工具自动<strong>并行执行</strong>(16个只读工具加入并行白名单),有依赖关系的工具串行执行</li></ul></div>
|
<div class="help-section"><h4>🔧 Tool Calling(始终开启)</h4><ul><li>所有消息均通过 <strong>Agent Loop 自主调用本地工具</strong>,像一个本地 Agent,无普通聊天模式</li><li><strong>42+1 个工具</strong>,分为 9 类:<ul><li><strong>文件系统</strong>(16 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files / compress</li><li><strong>命令执行</strong>(1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式,可配置超时)</li><li><strong>联网搜索</strong>(2 个):web_search(支持 SearXNG 元搜索引擎 JSON API / 四引擎 HTML 解析,双模式可切换)/ web_fetch(反爬headers+UA自动切换+自动回退浏览器渲染)</li><li><strong>Git</strong>(1 个):git(17 个子操作,push/pull/clone 内置60-120s超时保护)</li><li><strong>浏览器控制</strong>(9 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_wait / browser_close</li><li><strong>记忆 & 会话</strong>(6 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read</li><li><strong>子代理</strong>(1 个):spawn_task</li><li><strong>系统工具</strong>(6 个):datetime / calculator / random / uuid / json_format / hash</li></ul></li><li>read_file 支持 binary模式读取/base64解码/字节分页/2000行默认+截断hint自动续读</li><li>write_file 支持 base64写二进制文件/mode(overwrite+append)合并append_file功能/10MB</li><li>edit_file 支持 use_regex 正则替换</li><li>search_files 支持正则表达式搜索(use_regex=true)</li><li>browser_screenshot 支持全页面截图+元素截图</li><li>browser_extract 支持CSS选择器提取特定区域</li><li>new browser_wait 等待元素出现或定时等待</li><li>仅 <code>run_command</code> 支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code>、<code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code>、<code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,上下文使用率>80%时自动缩减到3轮</li><li>独立工具自动<strong>并行执行</strong>(16个只读工具加入并行白名单),有依赖关系的工具串行执行</li></ul></div>
|
||||||
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li><strong>增量提取</strong>:长对话中每 20 轮自动触发轻量级记忆提取,不等到对话结束</li><li>对话结束时自动触发完整记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 memory 工具主动管理记忆</li><li>记忆写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li><strong>增量提取</strong>:长对话中每 20 轮自动触发轻量级记忆提取,不等到对话结束</li><li>对话结束时自动触发完整记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 memory 工具主动管理记忆</li><li>记忆写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
||||||
<div class="help-section"><h4>🤖 Agent Loop v0.11.11 增强</h4><ul><li><strong>🎬 视频上传</strong>:支持上传 .mp4/.avi/.mov/.mkv/.webm 等视频(≤10MB),自动 1fps 提取帧序列(带时间戳),多模态模型原生理解视频时序关系</li><li><strong>智能上下文压缩</strong>:滑动窗口 + LLM结构化JSON摘要,超过50%上下文窗口自动触发</li><li><strong>流式调用超时保护</strong>:可配置超时(默认300s),Ollama 卡死不再永久阻塞</li><li><strong>HTTP/MCP 超时可配</strong>:设置面板可分别调整 HTTP(默认30s)和 MCP(默认60s)超时</li><li><strong>Final Answer 智能检测</strong>:多模式匹配(Final Answer/最终答案/最终回答/总结),避免漏检或误触</li><li><strong>Token 感知迭代预算</strong>:上下文使用率>80%时自动缩减剩余轮次到3轮</li><li><strong>工具缓存 TTL</strong>:搜索5分钟/网页10分钟/文件30分钟,过期自动刷新</li><li><strong>自动子任务拆解</strong>:检测"同时/分别/以及"等并行关键词,自动 spawn 子代理并行处理</li><li><strong>旧工具结果自动截断</strong>:超过10轮的工具结果自动截断到500字符,控制上下文膨胀</li></ul></div>
|
<div class="help-section"><h4>📋 Plan Mode(计划模式)</h4><ul><li>点击输入框上方 📋 按钮开启 <strong>Plan Mode</strong>(开关式)</li><li>开启后,AI <strong>先生成执行计划</strong>(Markdown 渲染弹窗),用户批准后才开始执行</li><li>计划批准后自动初始化追踪器,每个步骤完成后调用 <code>plan_track</code> 工具标记进度</li><li>系统提示词自动注入当前进度状态,AI 始终知道还剩多少步未完成</li><li><strong>多步骤任务防遗忘</strong>:自动检测用户请求中的动作动词,对比已完成步骤,注入提醒</li><li>关闭 Plan Mode 后恢复正常 Agent Loop 模式</li><li><strong>仅 Plan 模式</strong> 下 plan_track 工具可见,避免污染普通模式的工具列表</li></ul></div>
|
||||||
<div class="help-section"><h4>🔌 MCP(Model Context Protocol)</h4><ul><li>支持连接外部 MCP Server,动态扩展工具能力</li><li>设置面板可添加/启用/禁用/删除 MCP 服务器</li><li>MCP 工具以 <code>mcp_{server}_{tool}</code> 前缀注册,与内置工具统一调度</li><li>启动时自动连接已启用的 MCP 服务器</li></ul></div>
|
<div class="help-section"><h4>🛡️ 抗幻觉 & 稳定性</h4><ul><li><strong>5 层防御体系</strong>:系统提示词加固 → 中途幻觉检测(16 条规则覆盖全部工具类别)→ 进度锚点 → 完成闸门(6 项检查)→ 钩子校验</li><li><strong>完成闸门</strong>:对话结束前自动审查 AI 回复,检测 4 类关键问题(工具幻觉/注入/文件操作/遗漏),阻断有问题的回复</li><li><strong>16 条幻觉检测规则</strong>:覆盖文件编辑、浏览器操作、Git、记忆、压缩等所有工具类别,防止 AI 声称执行了未调用的工具</li><li><strong>看门狗超时</strong>:设置面板可配置全局超时(默认 30 分钟),AI 卡死或无限循环时自动中止</li><li><strong>中止保护</strong>:所有状态处理器 + 重试循环均检查中止信号,点击 ■ 按钮立即生效</li><li><strong>上下文硬上限</strong>:消息数超过阈值时强制触发压缩,防止 OOM</li></ul></div>
|
||||||
|
<div class="help-section"><h4>🤖 Agent Loop 增强</h4><ul><li><strong>🎬 视频上传</strong>:支持上传 .mp4/.avi/.mov/.mkv/.webm 等视频(≤10MB),自动 1fps 提取帧序列(带时间戳),多模态模型原生理解视频时序关系</li><li><strong>8 状态机</strong>:INIT→THINKING→PARSING→EXECUTING→OBSERVING→REFLECTING→COMPRESSING→TERMINATED,每个状态独立的中止检查和处理</li><li><strong>智能上下文压缩</strong>:滑动窗口 + LLM结构化JSON摘要,超过50%上下文窗口自动触发</li><li><strong>流式调用超时保护</strong>:可配置超时(默认300s),Ollama 卡死不再永久阻塞</li><li><strong>HTTP/MCP 超时可配</strong>:设置面板可分别调整 HTTP(默认30s)和 MCP(默认60s)超时</li><li><strong>Final Answer 智能检测</strong>:多模式匹配(Final Answer/最终答案/最终回答/总结),避免漏检或误触</li><li><strong>Token 感知迭代预算</strong>:上下文使用率>80%时自动缩减剩余轮次到3轮</li><li><strong>工具缓存 TTL</strong>:搜索5分钟/网页10分钟/文件30分钟,过期自动刷新</li><li><strong>自动子任务拆解</strong>:检测"同时/分别/以及"等并行关键词,自动 spawn 子代理并行处理</li><li><strong>旧工具结果自动截断</strong>:超过10轮的工具结果自动截断到500字符,控制上下文膨胀</li></ul></div>
|
||||||
|
<div class="help-section"><h4>🔌 MCP(Model Context Protocol)</h4><ul><li>支持连接外部 MCP Server,动态扩展工具能力</li><li>设置面板可添加/启用/禁用/删除 MCP 服务器</li><li>MCP 工具以 <code>mcp_{server}__{tool}</code> 前缀注册,与内置工具统一调度</li><li>启动时自动连接已启用的 MCP 服务器</li></ul></div>
|
||||||
<div class="help-section"><h4>🔍 SearXNG 元搜索引擎</h4><ul><li>点击顶部 🔍 按钮打开配置面板,可接入自部署的 SearXNG 实例</li><li><strong>JSON 模式</strong>:调用 SearXNG JSON API,聚合 70+ 引擎结果,结构化解析</li><li><strong>HTML 模式</strong>:获取原始搜索结果页面,交由 AI 自行分析提取信息</li><li>支持认证 Key(HTTP Header Authorization),保护私有实例</li><li>启用后替代内置四引擎方案;关闭即回退,无缝切换</li><li>所有参数(引擎、语言、安全搜索、时间范围等)均可独立配置</li></ul></div>
|
<div class="help-section"><h4>🔍 SearXNG 元搜索引擎</h4><ul><li>点击顶部 🔍 按钮打开配置面板,可接入自部署的 SearXNG 实例</li><li><strong>JSON 模式</strong>:调用 SearXNG JSON API,聚合 70+ 引擎结果,结构化解析</li><li><strong>HTML 模式</strong>:获取原始搜索结果页面,交由 AI 自行分析提取信息</li><li>支持认证 Key(HTTP Header Authorization),保护私有实例</li><li>启用后替代内置四引擎方案;关闭即回退,无缝切换</li><li>所有参数(引擎、语言、安全搜索、时间范围等)均可独立配置</li></ul></div>
|
||||||
<div class="help-section"><h4>📋 自定义文件(SOUL.md / AGENT.md / USER.md)</h4><ul><li>在工作空间目录创建以下文件即可自定义 AI 行为,修改后下一轮对话立即生效</li><li><strong>SOUL.md</strong> — AI 身份、性格、行为准则(<strong>永远不可被压缩</strong>,注入为最高优先级系统提示词)</li><li><strong>AGENT.md</strong> — 工具调用规则、链式调用模式、核心约束(内置精简默认版,可通过工作空间覆盖)</li><li><strong>USER.md</strong> — 用户画像:技术栈、偏好、习惯等个人信息,AI 在对话中自动参考</li><li>可在 AI 回复顶部的 📋 系统提示词卡片中查看实际注入的完整上下文</li><li>删除工作空间中的文件即可恢复为内置默认版本</li></ul></div>
|
<div class="help-section"><h4>📋 自定义文件(SOUL.md / AGENT.md / USER.md)</h4><ul><li>在工作空间目录创建以下文件即可自定义 AI 行为,修改后下一轮对话立即生效</li><li><strong>SOUL.md</strong> — AI 身份、性格、行为准则(<strong>永远不可被压缩</strong>,注入为最高优先级系统提示词)</li><li><strong>AGENT.md</strong> — 工具调用规则、链式调用模式、核心约束(内置精简默认版,可通过工作空间覆盖)</li><li><strong>USER.md</strong> — 用户画像:技术栈、偏好、习惯等个人信息,AI 在对话中自动参考</li><li>可在 AI 回复顶部的 📋 系统提示词卡片中查看实际注入的完整上下文</li><li>删除工作空间中的文件即可恢复为内置默认版本</li></ul></div>
|
||||||
<div class="help-section"><h4>📊 Token 实时监控</h4><ul><li>点击顶部 📊 按钮打开 Token 监控仪表盘</li><li><strong>全局统计</strong> — 跨会话累计 Token 消耗,柱状图按会话展示趋势</li><li><strong>当前会话</strong> — 实时显示本轮对话的 Token 消耗,按轮次展示明细</li><li>每 2 秒自动刷新数据,支持输入/输出分色显示</li></ul></div>
|
<div class="help-section"><h4>📊 Token 实时监控</h4><ul><li>点击顶部 📊 按钮打开 Token 监控仪表盘</li><li><strong>全局统计</strong> — 跨会话累计 Token 消耗,柱状图按会话展示趋势</li><li><strong>当前会话</strong> — 实时显示本轮对话的 Token 消耗,按轮次展示明细</li><li>每 2 秒自动刷新数据,支持输入/输出分色显示</li></ul></div>
|
||||||
@@ -453,7 +468,7 @@
|
|||||||
<div class="modal-overlay" id="toolsModal" style="display:none;">
|
<div class="modal-overlay" id="toolsModal" style="display:none;">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>🔧 工具面板(42 个)</h3>
|
<h3>🔧 工具面板(42 个 + 1 Plan 专属)</h3>
|
||||||
<button class="icon-btn" id="btnCloseTools">
|
<button class="icon-btn" id="btnCloseTools">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
@@ -639,6 +654,10 @@
|
|||||||
<div class="tool-card-header"><span class="tool-card-icon">🔐</span><span class="tool-card-name">hash</span><span class="tool-card-badge auto">自动</span></div>
|
<div class="tool-card-header"><span class="tool-card-icon">🔐</span><span class="tool-card-name">hash</span><span class="tool-card-badge auto">自动</span></div>
|
||||||
<div class="tool-card-desc">哈希计算(MD5/SHA1/SHA256/SHA384/SHA512)</div>
|
<div class="tool-card-desc">哈希计算(MD5/SHA1/SHA256/SHA384/SHA512)</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tool-card">
|
||||||
|
<div class="tool-card-header"><span class="tool-card-icon">📋</span><span class="tool-card-name">plan_track</span><span class="tool-card-badge plan-mode">Plan</span></div>
|
||||||
|
<div class="tool-card-desc">Plan 模式专属:追踪执行进度,标记/查看步骤完成状态</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+19
-1
@@ -31,6 +31,8 @@ import { initWorkspacePanel, clearToolCardsExternal, clearTerminalExternal, swit
|
|||||||
import { initLogPanel, addLog } from './services/log-service.js';
|
import { initLogPanel, addLog } from './services/log-service.js';
|
||||||
import { logInfo, logSuccess, logError, logDebug, logInit, logWarn } from './services/log-service.js';
|
import { logInfo, logSuccess, logError, logDebug, logInit, logWarn } from './services/log-service.js';
|
||||||
import { initSearxngModal, closeSearxngModal, loadSearxngConfig } from './components/searxng-modal.js';
|
import { initSearxngModal, closeSearxngModal, loadSearxngConfig } from './components/searxng-modal.js';
|
||||||
|
import { initHarnessHooks } from './services/hooks.js';
|
||||||
|
import { initVerificationSystem } from './services/verification.js';
|
||||||
import type { ChatSession } from './types.js';
|
import type { ChatSession } from './types.js';
|
||||||
|
|
||||||
// ─── v4.0 数据迁移:IndexedDB → SQLite ───
|
// ─── v4.0 数据迁移:IndexedDB → SQLite ───
|
||||||
@@ -346,7 +348,14 @@ async function init(): Promise<void> {
|
|||||||
await loadModels();
|
await loadModels();
|
||||||
await initMemoryManager();
|
await initMemoryManager();
|
||||||
await loadSearxngConfig(db);
|
await loadSearxngConfig(db);
|
||||||
logInit('所有组件已就绪');
|
|
||||||
|
// ── v0.12.0: Harness Engineering 系统初始化 ──
|
||||||
|
initHarnessHooks();
|
||||||
|
initVerificationSystem();
|
||||||
|
// Agent Metrics 无需显式初始化(按需启动)
|
||||||
|
// Context Indexer 在 Agent INIT 状态按需构建
|
||||||
|
|
||||||
|
logInit('所有组件已就绪(含 Harness v0.12.0)');
|
||||||
|
|
||||||
const savedModel = await db.getSetting('selectedModel', '');
|
const savedModel = await db.getSetting('selectedModel', '');
|
||||||
if (savedModel) {
|
if (savedModel) {
|
||||||
@@ -409,6 +418,13 @@ async function init(): Promise<void> {
|
|||||||
(document.querySelector('#inputSubAgentMaxLoops') as HTMLInputElement).value = String(subAgentMaxLoops);
|
(document.querySelector('#inputSubAgentMaxLoops') as HTMLInputElement).value = String(subAgentMaxLoops);
|
||||||
(document.querySelector('#inputSubAgentTimeout') as HTMLInputElement).value = subAgentTimeout >= 0 ? String(subAgentTimeout) : '';
|
(document.querySelector('#inputSubAgentTimeout') as HTMLInputElement).value = subAgentTimeout >= 0 ? String(subAgentTimeout) : '';
|
||||||
|
|
||||||
|
// ── v0.12.1 看门狗超时 ──
|
||||||
|
let loopWatchdogMs = await db.getSetting<number>('loopWatchdogMs', 1_800_000);
|
||||||
|
state.set('loopWatchdogMs', loopWatchdogMs);
|
||||||
|
const watchdogMin = loopWatchdogMs > 0 ? Math.round(loopWatchdogMs / 60_000) : loopWatchdogMs;
|
||||||
|
const elWatchdog = document.querySelector('#inputLoopWatchdog') as HTMLInputElement;
|
||||||
|
if (elWatchdog) elWatchdog.value = loopWatchdogMs > 0 ? String(watchdogMin) : (loopWatchdogMs === 0 ? '0' : '');
|
||||||
|
|
||||||
// ── Tool Calling 设置(永久开启,不可关闭)──
|
// ── Tool Calling 设置(永久开启,不可关闭)──
|
||||||
state.set('toolCallingEnabled', true);
|
state.set('toolCallingEnabled', true);
|
||||||
const runCommandMode = await db.getSetting('runCommandMode', 'confirm') as 'auto' | 'confirm' | 'disabled';
|
const runCommandMode = await db.getSetting('runCommandMode', 'confirm') as 'auto' | 'confirm' | 'disabled';
|
||||||
@@ -453,6 +469,8 @@ async function init(): Promise<void> {
|
|||||||
initTokenDashboard();
|
initTokenDashboard();
|
||||||
initWorkspacePanel();
|
initWorkspacePanel();
|
||||||
setupDesktopIntegration();
|
setupDesktopIntegration();
|
||||||
|
initHarnessHooks();
|
||||||
|
initVerificationSystem();
|
||||||
checkConnection().then(loadModels);
|
checkConnection().then(loadModels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* Agent Metrics — 治理与度量模块 (v0.12.0)
|
||||||
|
* Harness Engineering Phase 8: 度量驱动的优化循环
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* - 会话级度量采集(迭代效率、工具成功率、完成质量)
|
||||||
|
* - 全局趋势聚合
|
||||||
|
* - 错误模式识别(转向循环)
|
||||||
|
* - 度量仪表盘数据源
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logInfo, logDebug } from './log-service.js';
|
||||||
|
import type { AgentMetrics, LoopContext } from '../types.js';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 度量采集
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 当前会话的度量快照 */
|
||||||
|
interface SessionMetrics {
|
||||||
|
sessionId: string;
|
||||||
|
model: string;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
totalIterations: number;
|
||||||
|
toolCalls: Array<{ name: string; status: string; duration?: number }>;
|
||||||
|
totalInputTokens: number;
|
||||||
|
totalOutputTokens: number;
|
||||||
|
completionGatePassed: boolean;
|
||||||
|
errorPatterns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentMetrics: SessionMetrics | null = null;
|
||||||
|
const sessionMetricsHistory: SessionMetrics[] = [];
|
||||||
|
|
||||||
|
/** 开始采集会话度量 */
|
||||||
|
export function startSessionMetrics(sessionId: string, model: string): void {
|
||||||
|
currentMetrics = {
|
||||||
|
sessionId,
|
||||||
|
model,
|
||||||
|
startTime: Date.now(),
|
||||||
|
endTime: 0,
|
||||||
|
totalIterations: 0,
|
||||||
|
toolCalls: [],
|
||||||
|
totalInputTokens: 0,
|
||||||
|
totalOutputTokens: 0,
|
||||||
|
completionGatePassed: false,
|
||||||
|
errorPatterns: [],
|
||||||
|
};
|
||||||
|
logDebug(`Agent Metrics: 开始采集会话 ${sessionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录一轮迭代 */
|
||||||
|
export function recordIteration(ctx: LoopContext): void {
|
||||||
|
if (!currentMetrics) return;
|
||||||
|
currentMetrics.totalIterations = ctx.loopCount;
|
||||||
|
currentMetrics.totalInputTokens = ctx.totalPromptEvalCount;
|
||||||
|
currentMetrics.totalOutputTokens = ctx.totalEvalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录工具调用 */
|
||||||
|
export function recordToolCall(name: string, status: string, durationMs?: number): void {
|
||||||
|
if (!currentMetrics) return;
|
||||||
|
currentMetrics.toolCalls.push({ name, status, duration: durationMs });
|
||||||
|
if (status === 'error') {
|
||||||
|
// 错误模式:记录工具名和失败状态
|
||||||
|
const pattern = `${name}:error`;
|
||||||
|
if (!currentMetrics.errorPatterns.includes(pattern)) {
|
||||||
|
currentMetrics.errorPatterns.push(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完成门控结果 */
|
||||||
|
export function recordCompletionGate(passed: boolean): void {
|
||||||
|
if (!currentMetrics) return;
|
||||||
|
currentMetrics.completionGatePassed = passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 结束会话度量 */
|
||||||
|
export function endSessionMetrics(): SessionMetrics | null {
|
||||||
|
if (!currentMetrics) return null;
|
||||||
|
currentMetrics.endTime = Date.now();
|
||||||
|
sessionMetricsHistory.push({ ...currentMetrics });
|
||||||
|
const metrics = { ...currentMetrics };
|
||||||
|
logInfo(
|
||||||
|
`Agent Metrics: 会话完成 — ` +
|
||||||
|
`${metrics.totalIterations} 轮, ` +
|
||||||
|
`${metrics.toolCalls.length} 工具调用, ` +
|
||||||
|
`${formatPercent(getToolSuccessRate(metrics))} 成功率`
|
||||||
|
);
|
||||||
|
currentMetrics = null;
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 度量计算
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 计算工具成功率 */
|
||||||
|
function getToolSuccessRate(m: SessionMetrics): number {
|
||||||
|
if (m.toolCalls.length === 0) return 1;
|
||||||
|
const successCount = m.toolCalls.filter(t => t.status === 'success').length;
|
||||||
|
return successCount / m.toolCalls.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化百分比 */
|
||||||
|
function formatPercent(ratio: number): string {
|
||||||
|
return (ratio * 100).toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 聚合全局度量 */
|
||||||
|
export function aggregateMetrics(): AgentMetrics {
|
||||||
|
const history = sessionMetricsHistory;
|
||||||
|
if (history.length === 0) {
|
||||||
|
return {
|
||||||
|
totalSessions: 0,
|
||||||
|
avgIterationsPerTask: 0,
|
||||||
|
toolSuccessRate: 0,
|
||||||
|
avgCompletionScore: 0,
|
||||||
|
frequentErrors: [],
|
||||||
|
tokenEfficiency: 0,
|
||||||
|
collectedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalIterations = history.reduce((s, m) => s + m.totalIterations, 0);
|
||||||
|
const allToolCalls = history.flatMap(m => m.toolCalls);
|
||||||
|
const successCalls = allToolCalls.filter(t => t.status === 'success').length;
|
||||||
|
const totalInputTokens = history.reduce((s, m) => s + m.totalInputTokens, 0);
|
||||||
|
const totalOutputTokens = history.reduce((s, m) => s + m.totalOutputTokens, 0);
|
||||||
|
|
||||||
|
// 聚合错误模式
|
||||||
|
const errorCounts = new Map<string, number>();
|
||||||
|
for (const m of history) {
|
||||||
|
for (const pattern of m.errorPatterns) {
|
||||||
|
errorCounts.set(pattern, (errorCounts.get(pattern) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const frequentErrors = Array.from(errorCounts.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([pattern, count]) => ({ pattern, count }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalSessions: history.length,
|
||||||
|
avgIterationsPerTask: Math.round(totalIterations / history.length),
|
||||||
|
toolSuccessRate: allToolCalls.length > 0 ? successCalls / allToolCalls.length : 0,
|
||||||
|
avgCompletionScore: history.filter(m => m.completionGatePassed).length / history.length,
|
||||||
|
frequentErrors,
|
||||||
|
tokenEfficiency: totalInputTokens > 0 ? totalOutputTokens / totalInputTokens : 0,
|
||||||
|
collectedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 转向循环:错误模式 → 改进建议
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export interface ImprovementSuggestion {
|
||||||
|
pattern: string;
|
||||||
|
frequency: number;
|
||||||
|
suggestion: string;
|
||||||
|
severity: 'high' | 'medium' | 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析错误模式,生成改进建议
|
||||||
|
* 实现"让错误只犯一次"的核心理念
|
||||||
|
*/
|
||||||
|
export function generateImprovementSuggestions(): ImprovementSuggestion[] {
|
||||||
|
const metrics = aggregateMetrics();
|
||||||
|
const suggestions: ImprovementSuggestion[] = [];
|
||||||
|
|
||||||
|
for (const err of metrics.frequentErrors) {
|
||||||
|
const [tool, status] = err.pattern.split(':');
|
||||||
|
|
||||||
|
// 特定工具的重复失败
|
||||||
|
if (status === 'error') {
|
||||||
|
if (tool === 'web_fetch' && err.count >= 3) {
|
||||||
|
suggestions.push({
|
||||||
|
pattern: err.pattern,
|
||||||
|
frequency: err.count,
|
||||||
|
suggestion: `web_fetch 频繁失败 (${err.count}次)。考虑在 AGENT.md 中添加规则:优先使用 browser_open + browser_extract 作为替代方案。`,
|
||||||
|
severity: 'high',
|
||||||
|
});
|
||||||
|
} else if (tool === 'web_search' && err.count >= 3) {
|
||||||
|
suggestions.push({
|
||||||
|
pattern: err.pattern,
|
||||||
|
frequency: err.count,
|
||||||
|
suggestion: `web_search 频繁失败 (${err.count}次)。检查 SearXNG 配置或网络连接。可在 AGENT.md 中添加备用搜索策略。`,
|
||||||
|
severity: 'high',
|
||||||
|
});
|
||||||
|
} else if (err.count >= 5) {
|
||||||
|
suggestions.push({
|
||||||
|
pattern: err.pattern,
|
||||||
|
frequency: err.count,
|
||||||
|
suggestion: `工具 ${tool} 反复失败 ${err.count} 次。建议审查该工具的参数构造逻辑,或在 AGENT.md 中添加错误处理指南。`,
|
||||||
|
severity: 'medium',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token 效率过低
|
||||||
|
if (metrics.tokenEfficiency < 0.1 && metrics.totalSessions > 3) {
|
||||||
|
suggestions.push({
|
||||||
|
pattern: 'low_token_efficiency',
|
||||||
|
frequency: metrics.totalSessions,
|
||||||
|
suggestion: `Token 效率过低 (${formatPercent(metrics.tokenEfficiency)})。Agent 可能过度调用工具。建议在 AGENT.md 中添加:简单常识问题直接回答,不要调用工具。`,
|
||||||
|
severity: 'medium',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迭代次数过高
|
||||||
|
if (metrics.avgIterationsPerTask > 50) {
|
||||||
|
suggestions.push({
|
||||||
|
pattern: 'high_iterations',
|
||||||
|
frequency: metrics.totalSessions,
|
||||||
|
suggestion: `平均迭代次数过高 (${metrics.avgIterationsPerTask} 轮)。建议降低 maxTurns 设置或在 AGENT.md 中添加提前终止条件。`,
|
||||||
|
severity: 'medium',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将改进建议格式化为 AGENT.md 补充规则
|
||||||
|
*/
|
||||||
|
export function formatSuggestionsAsRules(suggestions: ImprovementSuggestion[]): string {
|
||||||
|
if (suggestions.length === 0) return '';
|
||||||
|
|
||||||
|
let rules = '\n\n## 自动生成的改进规则 (v0.12.0)\n';
|
||||||
|
rules += '> 以下规则由 Agent Metrics 系统根据历史错误模式自动生成\n\n';
|
||||||
|
|
||||||
|
for (const s of suggestions) {
|
||||||
|
rules += `### ${s.pattern}\n`;
|
||||||
|
rules += `- **严重程度**: ${s.severity}\n`;
|
||||||
|
rules += `- **出现频率**: ${s.frequency} 次\n`;
|
||||||
|
rules += `- **建议**: ${s.suggestion}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 格式化输出(供仪表盘使用)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export function formatMetricsReport(metrics: AgentMetrics): string {
|
||||||
|
return [
|
||||||
|
`Agent Metrics 报告 (${new Date(metrics.collectedAt).toLocaleString()})`,
|
||||||
|
`${'─'.repeat(50)}`,
|
||||||
|
`总会话数: ${metrics.totalSessions}`,
|
||||||
|
`平均迭代/任务: ${metrics.avgIterationsPerTask}`,
|
||||||
|
`工具成功率: ${formatPercent(metrics.toolSuccessRate)}`,
|
||||||
|
`完成门控通过率: ${formatPercent(metrics.avgCompletionScore)}`,
|
||||||
|
`Token 效率: ${formatPercent(metrics.tokenEfficiency)}`,
|
||||||
|
`高频错误: ${metrics.frequentErrors.length > 0 ? metrics.frequentErrors.map(e => `${e.pattern}(${e.count}次)`).join(', ') : '无'}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
/**
|
||||||
|
* Completion Gate — 完成门控模块 (v0.12.0)
|
||||||
|
* Harness Engineering Phase 3: 在 Agent 输出最终答案前进行结构化检查
|
||||||
|
*
|
||||||
|
* 设计理念:
|
||||||
|
* - 计算性反馈优先(规则驱动、毫秒级、100% 可靠)
|
||||||
|
* - 推理性反馈补充(AI 判断、秒级、非确定)
|
||||||
|
* - 所有检查项可独立开关
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logWarn, logInfo } from './log-service.js';
|
||||||
|
import { estimateTokens } from './context-manager.js';
|
||||||
|
import type { LoopContext, CompletionCheck } from '../types.js';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 内置检查项
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 内容质量检查:回复不应过短或不完整 */
|
||||||
|
const contentQualityCheck: CompletionCheck = {
|
||||||
|
name: 'contentQuality',
|
||||||
|
description: '检查回复长度和质量是否达到最低标准',
|
||||||
|
check: async (ctx: LoopContext) => {
|
||||||
|
if (ctx.content.length < 50) {
|
||||||
|
return { passed: false, reason: '回复内容过短(<50字),可能不完整。请给出更详细的最终回答。' };
|
||||||
|
}
|
||||||
|
return { passed: true, reason: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 工具结果审查:是否存在未处理的工具错误 */
|
||||||
|
const toolResultReviewCheck: CompletionCheck = {
|
||||||
|
name: 'toolResultReview',
|
||||||
|
description: '检查是否存在未处理的工具执行错误',
|
||||||
|
check: async (ctx: LoopContext) => {
|
||||||
|
const errorRecords = ctx.allToolRecords.filter(r => r.status === 'error');
|
||||||
|
if (errorRecords.length === 0) return { passed: true, reason: '' };
|
||||||
|
|
||||||
|
// 检查这些错误是否在最近的上下文中被讨论过
|
||||||
|
const recentContent = ctx.messages.slice(-5).map(m => m.content || '').join(' ');
|
||||||
|
const unresolvedErrors = errorRecords.filter(r => {
|
||||||
|
const errorName = r.name;
|
||||||
|
return !recentContent.includes(errorName + ' 失败')
|
||||||
|
&& !recentContent.includes(errorName + ' error')
|
||||||
|
&& !recentContent.includes(errorName + ' 错误');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unresolvedErrors.length > 0) {
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
reason: `存在 ${unresolvedErrors.length} 个未处理的工具错误: ${unresolvedErrors.map(r => r.name).join(', ')}。请说明这些错误的影响或提供替代方案。`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { passed: true, reason: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 思考阶段检测:回复不应仍在"思考中"状态 */
|
||||||
|
const notThinkingCheck: CompletionCheck = {
|
||||||
|
name: 'notThinking',
|
||||||
|
description: '检查回复是否已从思考阶段过渡到结论阶段',
|
||||||
|
check: async (ctx: LoopContext) => {
|
||||||
|
// 仅检查短回复(<200字)——长回复几乎肯定是完整回答
|
||||||
|
if (ctx.content.length > 200) return { passed: true, reason: '' };
|
||||||
|
|
||||||
|
const contentLower = ctx.content.slice(0, 100).toLowerCase();
|
||||||
|
const thinkingMarkers = ['让我看看', '观察一下', '先检查一下', '我需要先', '正在分析', '让我再'];
|
||||||
|
const continuationSignals = ctx.content.endsWith('...') || ctx.content.endsWith('等等') || ctx.content.endsWith('…');
|
||||||
|
const conclusionMarkers = [
|
||||||
|
/Final\s*Answer/i, /最终答案/, /最终回答/, /总结/, /任务完成/, /以上就是/,
|
||||||
|
/以上[是为]/, /我的能力/, /可以帮你/, /能够/, /我是/, /以下[是为]/,
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasConclusion = conclusionMarkers.some(p => p.test(ctx.content));
|
||||||
|
if (hasConclusion) return { passed: true, reason: '' };
|
||||||
|
|
||||||
|
const thinkingCount = thinkingMarkers.filter(m => contentLower.includes(m)).length;
|
||||||
|
const isClearlyThinking = thinkingCount >= 2 || (thinkingCount >= 1 && continuationSignals);
|
||||||
|
|
||||||
|
if (isClearlyThinking) {
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
reason: '回复看起来仍在思考阶段(短回复 + 思考动词 + 无结论标志),请基于已有结果给出明确的结论。'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { passed: true, reason: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 上下文效率检查:是否存在过度调用工具 */
|
||||||
|
const contextEfficiencyCheck: CompletionCheck = {
|
||||||
|
name: 'contextEfficiency',
|
||||||
|
description: '检查工具调用效率是否合理(避免过度调用)',
|
||||||
|
check: async (ctx: LoopContext) => {
|
||||||
|
const toolMessages = ctx.messages.filter(m => m.role === 'tool');
|
||||||
|
const nonToolMessages = ctx.messages.filter(m => m.role !== 'tool' && m.role !== 'system');
|
||||||
|
|
||||||
|
if (toolMessages.length > nonToolMessages.length * 3 && ctx.loopCount > 5) {
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
reason: `工具调用过多(${toolMessages.length} 次 vs ${nonToolMessages.length} 次非工具消息),可能存在重复或低效的工具调用。请检查是否可以直接基于已有结果给出回答。`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { passed: true, reason: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 系统提示词注入检测:防止模型在回复中尝试注入 prompt */
|
||||||
|
const promptInjectionCheck: CompletionCheck = {
|
||||||
|
name: 'promptInjection',
|
||||||
|
description: '检测回复中是否包含疑似 prompt injection 内容',
|
||||||
|
check: async (ctx: LoopContext) => {
|
||||||
|
const injectionPatterns = [
|
||||||
|
/ignore\s+(all\s+)?(previous|above)\s+instructions/i,
|
||||||
|
/you\s+are\s+now\s+(a|an)\s+/i,
|
||||||
|
/new\s+system\s*prompt/i,
|
||||||
|
/forget\s+(all\s+)?(previous|above)/i,
|
||||||
|
/\[SOUL\.md\]/,
|
||||||
|
/\[AGENT\.md\]/,
|
||||||
|
];
|
||||||
|
for (const pattern of injectionPatterns) {
|
||||||
|
if (pattern.test(ctx.content)) {
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
reason: '回复中包含疑似 prompt injection 内容(试图覆盖系统提示词)。请移除相关内容后重新回答。'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { passed: true, reason: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 工具幻觉检测:模型声称完成了某操作但该工具从未被调用 */
|
||||||
|
const toolHallucinationCheck: CompletionCheck = {
|
||||||
|
name: 'toolHallucination',
|
||||||
|
description: '检测模型是否编造了工具调用结果',
|
||||||
|
check: async (ctx: LoopContext) => {
|
||||||
|
const calledTools = new Set(ctx.allToolRecords.map(r => r.name));
|
||||||
|
const content = ctx.content;
|
||||||
|
|
||||||
|
// ── 统一的"声称 → 必需工具"映射表(覆盖全部 42 个工具)──
|
||||||
|
const CLAIM_RULES: Array<{ patterns: RegExp[]; requiredTools: string[]; label: string }> = [
|
||||||
|
// 文件写入
|
||||||
|
{ patterns: [/已写入文件/, /已创建文件/, /文件已生成/, /已保存到/, /已成功创建/, /成功写入/,
|
||||||
|
/文件.*已.*创建/, /文件.*已.*写入/, /生成.*文件/, /写入.*工作空间/],
|
||||||
|
requiredTools: ['write_file'], label: '写入/创建文件' },
|
||||||
|
// 文件读取
|
||||||
|
{ patterns: [/已读取文件/, /文件内容显示/, /读取了.*文件/, /文件.*内容.*如下/,
|
||||||
|
/查看.*文件.*内容/, /打开文件.*看到/],
|
||||||
|
requiredTools: ['read_file', 'read_multiple_files'], label: '读取文件' },
|
||||||
|
// 文件编辑
|
||||||
|
{ patterns: [/已修改文件/, /已编辑文件/, /文件已更新/, /已替换.*内容/,
|
||||||
|
/修改了.*文件/, /更新了.*文件/],
|
||||||
|
requiredTools: ['edit_file', 'write_file'], label: '编辑/修改文件' },
|
||||||
|
// 文件删除
|
||||||
|
{ patterns: [/已删除文件/, /已移除文件/, /文件已清除/, /删除了.*文件/],
|
||||||
|
requiredTools: ['delete_file'], label: '删除文件' },
|
||||||
|
// 文件移动/复制/重命名
|
||||||
|
{ patterns: [/已移动文件/, /已复制文件/, /已重命名/, /文件.*已.*移动/, /文件.*已.*复制/],
|
||||||
|
requiredTools: ['move_file', 'copy_file'], label: '移动/复制文件' },
|
||||||
|
// 目录浏览
|
||||||
|
{ patterns: [/目录包含/, /列出.*目录/, /目录.*结构/, /文件列表.*如下/, /目录.*内容/],
|
||||||
|
requiredTools: ['list_directory', 'tree'], label: '列出目录' },
|
||||||
|
// 文件搜索
|
||||||
|
{ patterns: [/搜索到.*文件/, /找到.*匹配/, /搜索.*结果.*文件/],
|
||||||
|
requiredTools: ['search_files'], label: '搜索文件' },
|
||||||
|
// 搜索/抓取
|
||||||
|
{ patterns: [/搜索结果显示/, /根据搜索/, /抓取了.*网页/, /从.*搜索.*得到/,
|
||||||
|
/查询到/, /检索到/, /搜索到/, /网络.*显示/, /实时.*数据.*显示/],
|
||||||
|
requiredTools: ['web_search', 'web_fetch'], label: '搜索/抓取网页' },
|
||||||
|
// 浏览器
|
||||||
|
{ patterns: [/浏览器截图显示/, /打开网页.*看到/, /页面显示/, /浏览器.*打开/,
|
||||||
|
/点击了.*按钮/, /输入了.*文本/, /页面.*截图/],
|
||||||
|
requiredTools: ['browser_open', 'browser_screenshot', 'browser_extract', 'browser_click', 'browser_type'], label: '浏览器操作' },
|
||||||
|
// 命令执行
|
||||||
|
{ patterns: [/运行.*命令/, /执行.*脚本/, /命令.*输出/, /运行结果/, /命令.*返回/,
|
||||||
|
/执行.*结果/, /终端.*显示/],
|
||||||
|
requiredTools: ['run_command'], label: '执行命令' },
|
||||||
|
// Git 操作
|
||||||
|
{ patterns: [/已提交/, /已推送/, /已暂存/, /已创建分支/, /已合并/, /已克隆/,
|
||||||
|
/git.*commit/, /git.*push/, /git.*add/, /提交了.*代码/],
|
||||||
|
requiredTools: ['git'], label: 'Git 操作' },
|
||||||
|
// 下载文件
|
||||||
|
{ patterns: [/已下载文件/, /下载完成/, /文件.*已.*下载/, /从.*下载/],
|
||||||
|
requiredTools: ['download_file'], label: '下载文件' },
|
||||||
|
// 压缩/解压
|
||||||
|
{ patterns: [/已压缩/, /已解压/, /压缩完成/, /解压完成/, /归档.*已.*创建/],
|
||||||
|
requiredTools: ['compress'], label: '压缩/解压' },
|
||||||
|
// 文件信息
|
||||||
|
{ patterns: [/文件.*大小.*字节/, /文件.*权限.*为/, /文件.*修改日期/],
|
||||||
|
requiredTools: ['get_file_info'], label: '获取文件信息' },
|
||||||
|
// 内存/记忆操作
|
||||||
|
{ patterns: [/已记住/, /已添加记忆/, /记忆已保存/, /已更新记忆/, /已删除记忆/],
|
||||||
|
requiredTools: ['memory_add', 'memory_replace', 'memory_remove'], label: '记忆操作' },
|
||||||
|
// 会话操作
|
||||||
|
{ patterns: [/历史会话.*显示/, /已读取.*会话/, /会话.*内容.*如下/],
|
||||||
|
requiredTools: ['session_list', 'session_read'], label: '会话操作' },
|
||||||
|
// 子代理
|
||||||
|
{ patterns: [/子代理.*完成/, /子任务.*已.*执行/, /spawn.*task.*完成/],
|
||||||
|
requiredTools: ['spawn_task'], label: '子代理委派' },
|
||||||
|
// 系统工具
|
||||||
|
{ patterns: [/计算.*结果.*为/, /哈希.*值.*为/, /UUID.*生成/, /随机.*生成.*为/],
|
||||||
|
requiredTools: ['calculator', 'hash', 'uuid', 'random'], label: '系统工具' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const rule of CLAIM_RULES) {
|
||||||
|
for (const pattern of rule.patterns) {
|
||||||
|
if (pattern.test(content)) {
|
||||||
|
const anyCalled = rule.requiredTools.some(t => calledTools.has(t));
|
||||||
|
if (!anyCalled) {
|
||||||
|
const toolList = rule.requiredTools.join(' / ');
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
reason: `回复声称进行了"${rule.label}"操作,但 ${toolList} 工具从未被调用。请实际调用 ${toolList} 来完成任务,不要编造结果。`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break; // 该类别匹配到了且工具已调用,跳出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { passed: true, reason: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 检查项注册与管理
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 默认启用的检查项 */
|
||||||
|
const DEFAULT_ENABLED_CHECKS = new Set([
|
||||||
|
'contentQuality',
|
||||||
|
'toolResultReview',
|
||||||
|
'notThinking',
|
||||||
|
'promptInjection',
|
||||||
|
'toolHallucination',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 阻断级检查:不通过时强制模型重新回答 */
|
||||||
|
const CRITICAL_CHECKS = new Set([
|
||||||
|
'toolHallucination',
|
||||||
|
'promptInjection',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 注册表 */
|
||||||
|
const checkRegistry = new Map<string, CompletionCheck>([
|
||||||
|
['contentQuality', contentQualityCheck],
|
||||||
|
['toolResultReview', toolResultReviewCheck],
|
||||||
|
['notThinking', notThinkingCheck],
|
||||||
|
['contextEfficiency', contextEfficiencyCheck],
|
||||||
|
['promptInjection', promptInjectionCheck],
|
||||||
|
['toolHallucination', toolHallucinationCheck],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 已启用的检查项 */
|
||||||
|
const enabledChecks = new Set(DEFAULT_ENABLED_CHECKS);
|
||||||
|
|
||||||
|
/** 注册自定义检查项 */
|
||||||
|
export function registerCompletionCheck(check: CompletionCheck): void {
|
||||||
|
checkRegistry.set(check.name, check);
|
||||||
|
logInfo(`Completion Gate: 注册检查项 "${check.name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除检查项 */
|
||||||
|
export function unregisterCompletionCheck(name: string): void {
|
||||||
|
checkRegistry.delete(name);
|
||||||
|
enabledChecks.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启用/禁用检查项 */
|
||||||
|
export function setCheckEnabled(name: string, enabled: boolean): void {
|
||||||
|
if (enabled) {
|
||||||
|
if (checkRegistry.has(name)) enabledChecks.add(name);
|
||||||
|
} else {
|
||||||
|
enabledChecks.delete(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取所有已注册检查项 */
|
||||||
|
export function getRegisteredChecks(): CompletionCheck[] {
|
||||||
|
return Array.from(checkRegistry.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取已启用的检查项 */
|
||||||
|
export function getEnabledChecks(): CompletionCheck[] {
|
||||||
|
return getRegisteredChecks().filter(c => enabledChecks.has(c.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 执行门控
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export interface GateResult {
|
||||||
|
passed: boolean;
|
||||||
|
reason: string;
|
||||||
|
/** 是否为阻断级失败(必须重新回答) */
|
||||||
|
critical: boolean;
|
||||||
|
/** 各项检查的详细结果 */
|
||||||
|
details: Array<{ name: string; passed: boolean; reason: string; durationMs: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行完成门控检查
|
||||||
|
*/
|
||||||
|
export async function runCompletionGate(ctx: LoopContext): Promise<GateResult> {
|
||||||
|
const checks = getEnabledChecks();
|
||||||
|
if (checks.length === 0) {
|
||||||
|
return { passed: true, reason: '', critical: false, details: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const details: GateResult['details'] = [];
|
||||||
|
|
||||||
|
// 关键检查优先(幻觉/注入),咨询检查在后
|
||||||
|
const sorted = [...checks].sort((a, b) => {
|
||||||
|
const aCrit = CRITICAL_CHECKS.has(a.name) ? 0 : 1;
|
||||||
|
const bCrit = CRITICAL_CHECKS.has(b.name) ? 0 : 1;
|
||||||
|
return aCrit - bCrit;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const check of sorted) {
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const result = await check.check(ctx);
|
||||||
|
details.push({
|
||||||
|
name: check.name,
|
||||||
|
passed: result.passed,
|
||||||
|
reason: result.reason,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
});
|
||||||
|
if (!result.passed) {
|
||||||
|
const isCritical = CRITICAL_CHECKS.has(check.name);
|
||||||
|
logWarn(`Completion Gate: "${check.name}" ${isCritical ? '🔴阻断' : '🟡咨询'} — ${result.reason}`);
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
reason: result.reason,
|
||||||
|
critical: isCritical,
|
||||||
|
details,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
details.push({
|
||||||
|
name: check.name,
|
||||||
|
passed: true,
|
||||||
|
reason: `检查异常: ${(err as Error).message}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { passed: true, reason: '', critical: false, details };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成门控报告(供日志/调试使用)
|
||||||
|
*/
|
||||||
|
export function formatGateReport(result: GateResult): string {
|
||||||
|
const status = result.passed ? '✅ 通过' : '❌ 未通过';
|
||||||
|
let report = `Completion Gate ${status}`;
|
||||||
|
if (!result.passed) {
|
||||||
|
report += ` — ${result.reason}`;
|
||||||
|
}
|
||||||
|
report += `\n${'─'.repeat(40)}`;
|
||||||
|
for (const d of result.details) {
|
||||||
|
const icon = d.passed ? '✅' : '❌';
|
||||||
|
report += `\n${icon} ${d.name}: ${d.reason || '通过'} (${d.durationMs}ms)`;
|
||||||
|
}
|
||||||
|
return report;
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* Context Indexer — 渐进式披露模块 (v0.12.0)
|
||||||
|
* Harness Engineering Phase 6: 三级上下文管理
|
||||||
|
*
|
||||||
|
* 索引层 (Index) — 始终保留:项目结构树 + 入口文件地图 + 技术栈摘要
|
||||||
|
* 接口层 (Interface) — 按需加载:模块 API 声明 + 类型定义 + 配置文件
|
||||||
|
* 实现层 (Implementation) — 修改时加载:具体源代码
|
||||||
|
*
|
||||||
|
* 设计理念:
|
||||||
|
* - 用目录式索引告诉智能体"去哪找",而非"全记住"
|
||||||
|
* - 上下文可从数万 Token 压至几千
|
||||||
|
* - 通过 load_context 工具按需触发接口层和实现层的加载
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logInfo, logDebug, logWarn } from './log-service.js';
|
||||||
|
import { estimateTokens } from './context-manager.js';
|
||||||
|
import type { ProjectIndex, ContextTier } from '../types.js';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 项目索引缓存
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 项目索引缓存(5 分钟 TTL) */
|
||||||
|
let cachedIndex: ProjectIndex | null = null;
|
||||||
|
let cacheTimestamp = 0;
|
||||||
|
const INDEX_CACHE_TTL = 5 * 60 * 1000; // 5 分钟
|
||||||
|
|
||||||
|
/** 最大索引 Token 预算 */
|
||||||
|
const MAX_INDEX_TOKENS = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建项目索引
|
||||||
|
* 扫描工作空间目录结构,生成精简的结构摘要
|
||||||
|
*/
|
||||||
|
export async function buildProjectIndex(workspaceDir: string): Promise<ProjectIndex> {
|
||||||
|
// 检查缓存
|
||||||
|
if (cachedIndex && Date.now() - cacheTimestamp < INDEX_CACHE_TTL) {
|
||||||
|
return cachedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bridge = window.metonaDesktop;
|
||||||
|
if (!bridge?.isDesktop) {
|
||||||
|
return createEmptyIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 利用现有 tree 工具扫描目录结构(限制深度 3 层)
|
||||||
|
const treeResult = await bridge.tool.execute('tree', {
|
||||||
|
path: workspaceDir,
|
||||||
|
max_depth: 3,
|
||||||
|
include_hidden: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let structure = '';
|
||||||
|
if (treeResult.success && treeResult.tree) {
|
||||||
|
structure = String(treeResult.tree);
|
||||||
|
// Token 预算截断
|
||||||
|
if (estimateTokens(structure) > MAX_INDEX_TOKENS) {
|
||||||
|
const lines = structure.split('\n');
|
||||||
|
structure = lines.slice(0, Math.min(lines.length, 60)).join('\n')
|
||||||
|
+ '\n... (目录结构已截断,使用 list_directory 查看完整内容)';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
structure = '(无法读取工作空间目录结构)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 识别入口文件
|
||||||
|
const entryFiles: string[] = [];
|
||||||
|
const commonEntries = [
|
||||||
|
'package.json', 'tsconfig.json', 'vite.config.ts',
|
||||||
|
'main.ts', 'index.ts', 'index.html', 'app.ts',
|
||||||
|
'Cargo.toml', 'pyproject.toml', 'go.mod', 'CMakeLists.txt',
|
||||||
|
'README.md', 'Makefile', 'docker-compose.yml',
|
||||||
|
];
|
||||||
|
for (const entry of commonEntries) {
|
||||||
|
try {
|
||||||
|
// 跨平台路径拼接(清理尾部斜杠,统一用 posix 风格,Node.js 可容错处理)
|
||||||
|
const cleanDir = workspaceDir.replace(/[\\/]+$/, '');
|
||||||
|
const filePath = cleanDir + '/' + entry;
|
||||||
|
const checkResult = await bridge.workspace.readFile(filePath);
|
||||||
|
if (checkResult?.success) {
|
||||||
|
entryFiles.push(entry);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测技术栈
|
||||||
|
const techStack = detectTechStack(entryFiles);
|
||||||
|
|
||||||
|
const index: ProjectIndex = {
|
||||||
|
structure,
|
||||||
|
entryFiles,
|
||||||
|
techStack,
|
||||||
|
tokenCount: estimateTokens(structure),
|
||||||
|
generatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cachedIndex = index;
|
||||||
|
cacheTimestamp = Date.now();
|
||||||
|
logInfo('项目索引已构建', `${techStack.join(', ')}, ${entryFiles.length} 入口文件, ${index.tokenCount} tokens`);
|
||||||
|
return index;
|
||||||
|
} catch (err) {
|
||||||
|
logWarn('项目索引构建失败', (err as Error).message);
|
||||||
|
return createEmptyIndex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建空索引 */
|
||||||
|
function createEmptyIndex(): ProjectIndex {
|
||||||
|
return {
|
||||||
|
structure: '(未检测到工作空间)',
|
||||||
|
entryFiles: [],
|
||||||
|
techStack: [],
|
||||||
|
tokenCount: 0,
|
||||||
|
generatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据入口文件检测技术栈 */
|
||||||
|
function detectTechStack(entryFiles: string[]): string[] {
|
||||||
|
const stack: string[] = [];
|
||||||
|
const fileSet = new Set(entryFiles.map(f => f.toLowerCase()));
|
||||||
|
|
||||||
|
if (fileSet.has('package.json')) stack.push('Node.js');
|
||||||
|
if (fileSet.has('tsconfig.json')) stack.push('TypeScript');
|
||||||
|
if (fileSet.has('vite.config.ts')) stack.push('Vite');
|
||||||
|
if (fileSet.has('cargo.toml')) stack.push('Rust');
|
||||||
|
if (fileSet.has('pyproject.toml')) stack.push('Python');
|
||||||
|
if (fileSet.has('go.mod')) stack.push('Go');
|
||||||
|
if (fileSet.has('cmakelists.txt')) stack.push('C/C++');
|
||||||
|
if (fileSet.has('docker-compose.yml')) stack.push('Docker');
|
||||||
|
if (fileSet.has('makefile')) stack.push('Make');
|
||||||
|
|
||||||
|
return stack.length > 0 ? stack : ['未知'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成索引层系统提示词
|
||||||
|
* 始终保留在上下文中,告诉 AI "去哪找"
|
||||||
|
*/
|
||||||
|
export function buildIndexContext(index: ProjectIndex): string {
|
||||||
|
if (!index.structure || index.tokenCount === 0) return '';
|
||||||
|
|
||||||
|
let context = `【项目索引 — 始终可见】
|
||||||
|
项目结构:
|
||||||
|
${index.structure}
|
||||||
|
|
||||||
|
技术栈: ${index.techStack.join(', ') || '未检测'}
|
||||||
|
入口文件: ${index.entryFiles.length > 0 ? index.entryFiles.join(', ') : '未检测'}
|
||||||
|
|
||||||
|
💡 使用 list_directory 查看目录详情,使用 read_file 读取具体文件。
|
||||||
|
💡 使用 search_files 按内容搜索代码。
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Token 预算控制
|
||||||
|
if (estimateTokens(context) > MAX_INDEX_TOKENS) {
|
||||||
|
context = context.slice(0, Math.floor(context.length * 0.8)) + '\n... (索引已截断)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建接口层上下文(按需加载)
|
||||||
|
* @param modulePattern 模块匹配模式,如 "src/services/"
|
||||||
|
*/
|
||||||
|
export async function buildInterfaceContext(modulePattern: string, workspaceDir: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const bridge = window.metonaDesktop;
|
||||||
|
if (!bridge?.isDesktop) return '';
|
||||||
|
|
||||||
|
// 搜索模块相关的类型定义和配置文件
|
||||||
|
const searchResult = await bridge.tool.execute('search_files', {
|
||||||
|
path: workspaceDir,
|
||||||
|
query: modulePattern,
|
||||||
|
search_type: 'filename',
|
||||||
|
max_results: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!searchResult.success || !(searchResult as any).results?.length) {
|
||||||
|
return `(未找到与 "${modulePattern}" 相关的接口文件)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = (searchResult as any).results as Array<{ path: string }>;
|
||||||
|
const paths = results.map(r => r.path).slice(0, 8);
|
||||||
|
|
||||||
|
// 批量读取接口文件(限制每文件 2000 字符)
|
||||||
|
const readResult = await bridge.tool.execute('read_multiple_files', {
|
||||||
|
paths,
|
||||||
|
max_chars_per_file: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (readResult.success) {
|
||||||
|
const filesInfo = paths.map(p => ` 📄 ${p}`).join('\n');
|
||||||
|
return `【接口层 — ${modulePattern}】
|
||||||
|
相关文件:
|
||||||
|
${filesInfo}
|
||||||
|
|
||||||
|
内容预览:
|
||||||
|
${JSON.stringify((readResult as any).files)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `【接口层 — ${modulePattern}】
|
||||||
|
相关文件:${paths.join(', ')}`;
|
||||||
|
} catch (err) {
|
||||||
|
logWarn('接口层上下文加载失败', (err as Error).message);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载指定层级的上下文
|
||||||
|
*/
|
||||||
|
export async function loadContextByTier(
|
||||||
|
tier: ContextTier,
|
||||||
|
modulePattern: string,
|
||||||
|
workspaceDir: string,
|
||||||
|
): Promise<string> {
|
||||||
|
switch (tier) {
|
||||||
|
case 'index': {
|
||||||
|
const index = await buildProjectIndex(workspaceDir);
|
||||||
|
return buildIndexContext(index);
|
||||||
|
}
|
||||||
|
case 'interface':
|
||||||
|
return buildInterfaceContext(modulePattern, workspaceDir);
|
||||||
|
case 'implementation':
|
||||||
|
// 实现层由 Agent 自行通过 read_file 加载
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* Harness Hook 系统 (v0.12.0)
|
||||||
|
* Harness Engineering Phase 4: 可扩展的 Agent 执行钩子架构
|
||||||
|
*
|
||||||
|
* 钩子阶段:
|
||||||
|
* - pre_tool: 工具执行前(安全检查、缓存检查、权限验证)
|
||||||
|
* - post_tool: 工具执行后(结果验证、自动 lint、自动测试)
|
||||||
|
* - post_iteration: 每轮迭代后(记忆提取、上下文裁剪)
|
||||||
|
* - pre_completion: 最终输出前(完成门控、质量检查)
|
||||||
|
*
|
||||||
|
* 设计原则:
|
||||||
|
* - 异步并行执行,失败不阻塞主流程
|
||||||
|
* - 优先级排序,高优先级先执行
|
||||||
|
* - 可动态注册/移除
|
||||||
|
* - MCP 工具可注册自定义 Hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logInfo, logWarn, logDebug } from './log-service.js';
|
||||||
|
import type { HarnessHook, HookPhase, HookResult, HookData, LoopContext } from '../types.js';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Hook 注册表
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const hookRegistry = new Map<string, HarnessHook>();
|
||||||
|
const phaseIndex = new Map<HookPhase, HarnessHook[]>();
|
||||||
|
|
||||||
|
function ensurePhaseIndex(phase: HookPhase): void {
|
||||||
|
if (!phaseIndex.has(phase)) {
|
||||||
|
phaseIndex.set(phase, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册 Hook */
|
||||||
|
export function registerHook(hook: HarnessHook): void {
|
||||||
|
hookRegistry.set(hook.name, hook);
|
||||||
|
ensurePhaseIndex(hook.phase);
|
||||||
|
phaseIndex.get(hook.phase)!.push(hook);
|
||||||
|
// 按优先级降序排列
|
||||||
|
phaseIndex.get(hook.phase)!.sort((a, b) => b.priority - a.priority);
|
||||||
|
logDebug(`Hook 已注册: "${hook.name}" phase=${hook.phase} priority=${hook.priority}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除 Hook */
|
||||||
|
export function unregisterHook(name: string): void {
|
||||||
|
const hook = hookRegistry.get(name);
|
||||||
|
if (!hook) return;
|
||||||
|
hookRegistry.delete(name);
|
||||||
|
const list = phaseIndex.get(hook.phase);
|
||||||
|
if (list) {
|
||||||
|
const idx = list.findIndex(h => h.name === name);
|
||||||
|
if (idx !== -1) list.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启用/禁用 Hook */
|
||||||
|
export function setHookEnabled(name: string, enabled: boolean): void {
|
||||||
|
const hook = hookRegistry.get(name);
|
||||||
|
if (hook) {
|
||||||
|
hook.enabled = enabled;
|
||||||
|
logDebug(`Hook "${name}" ${enabled ? '已启用' : '已禁用'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取所有已注册 Hook */
|
||||||
|
export function getRegisteredHooks(): HarnessHook[] {
|
||||||
|
return Array.from(hookRegistry.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取指定阶段已启用的 Hook */
|
||||||
|
export function getPhaseHooks(phase: HookPhase): HarnessHook[] {
|
||||||
|
return (phaseIndex.get(phase) || []).filter(h => h.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Hook 执行引擎
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 并行执行指定阶段的所有 Hook
|
||||||
|
* @returns 所有 Hook 的执行结果汇总
|
||||||
|
*/
|
||||||
|
export async function executeHooks(
|
||||||
|
phase: HookPhase,
|
||||||
|
ctx: LoopContext,
|
||||||
|
data: HookData = {}
|
||||||
|
): Promise<{ results: HookResult[]; allPassed: boolean }> {
|
||||||
|
const hooks = getPhaseHooks(phase);
|
||||||
|
if (hooks.length === 0) {
|
||||||
|
return { results: [], allPassed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
hooks.map(async (hook) => {
|
||||||
|
try {
|
||||||
|
const result = await hook.handler(ctx, data);
|
||||||
|
if (!result.passed) {
|
||||||
|
logWarn(`Hook "${hook.name}" 未通过: ${result.message}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
logWarn(`Hook "${hook.name}" 执行异常: ${(err as Error).message}`);
|
||||||
|
return { passed: true, message: `Hook 异常(已忽略): ${(err as Error).message}` };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const allPassed = results.every(r => r.passed);
|
||||||
|
return { results, allPassed };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 内置 Hook
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SecurityCheckHook — 工具执行前安全检查
|
||||||
|
* 检查命令和路径是否在黑名单内
|
||||||
|
*/
|
||||||
|
export const securityCheckHook: HarnessHook = {
|
||||||
|
name: 'SecurityCheck',
|
||||||
|
phase: 'pre_tool',
|
||||||
|
priority: 100, // 最高优先级
|
||||||
|
enabled: true,
|
||||||
|
handler: async (_ctx: LoopContext, data: HookData) => {
|
||||||
|
const { toolName, toolArgs } = data;
|
||||||
|
if (toolName === 'run_command' && typeof toolArgs?.command === 'string') {
|
||||||
|
const cmd = toolArgs.command as string;
|
||||||
|
// 快速黑名单检查
|
||||||
|
const dangerous = ['rm -rf /', 'mkfs', 'dd if=', 'shutdown', 'reboot', ':(){', 'chmod 777 /'];
|
||||||
|
for (const d of dangerous) {
|
||||||
|
if (cmd.toLowerCase().includes(d.toLowerCase())) {
|
||||||
|
return { passed: false, message: `危险命令被拦截: ${cmd.slice(0, 100)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 管道执行 shell 检测
|
||||||
|
if (/(\||>|<)\s*(sh|bash|zsh|powershell|cmd)/i.test(cmd)) {
|
||||||
|
return { passed: false, message: `禁止通过管道执行 shell: ${cmd.slice(0, 100)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toolName === 'delete_file' && typeof toolArgs?.path === 'string') {
|
||||||
|
const p = (toolArgs.path as string).toLowerCase();
|
||||||
|
const protectedPaths = ['/etc', '/sys', '/proc', '/dev', '/boot', 'c:\\windows', '.ssh', '.gnupg'];
|
||||||
|
for (const pp of protectedPaths) {
|
||||||
|
if (p.includes(pp)) {
|
||||||
|
return { passed: false, message: `受保护路径不可删除: ${p}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { passed: true, message: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResultValidationHook — 工具执行后结果验证
|
||||||
|
* 检查工具返回是否有效。注意:此 Hook 为咨询性(post_tool 阶段不阻断执行),
|
||||||
|
* 实际的截断由 agent-engine 的 formatToolResultForModel 和 40 条消息硬限制负责。
|
||||||
|
*/
|
||||||
|
export const resultValidationHook: HarnessHook = {
|
||||||
|
name: 'ResultValidation',
|
||||||
|
phase: 'post_tool',
|
||||||
|
priority: 90,
|
||||||
|
enabled: true,
|
||||||
|
handler: async (_ctx: LoopContext, data: HookData) => {
|
||||||
|
const { toolName, toolResult } = data;
|
||||||
|
if (!toolResult) return { passed: true, message: '' };
|
||||||
|
|
||||||
|
// 检查空结果
|
||||||
|
if (!toolResult.success && !toolResult.error) {
|
||||||
|
return { passed: false, message: `工具 ${toolName} 执行失败但未提供错误信息` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查超大结果。不同工具有不同阈值:
|
||||||
|
// web_search 包含自动抓取内容,允许较大结果
|
||||||
|
const SIZE_LIMITS: Record<string, number> = {
|
||||||
|
web_search: 1_000_000, // 1MB(含自动抓取的多页内容)
|
||||||
|
web_fetch: 500_000, // 500KB
|
||||||
|
read_file: 200_000, // 200KB
|
||||||
|
default: 500_000, // 默认 500KB
|
||||||
|
};
|
||||||
|
const limit = SIZE_LIMITS[toolName || ''] || SIZE_LIMITS.default;
|
||||||
|
|
||||||
|
if (toolResult.success && toolResult) {
|
||||||
|
const resultStr = JSON.stringify(toolResult);
|
||||||
|
if (resultStr.length > limit) {
|
||||||
|
// 仅警告,不阻断(实际截断由 agent-engine 负责)
|
||||||
|
return {
|
||||||
|
passed: true,
|
||||||
|
message: `工具 ${toolName} 返回结果较大 (${(resultStr.length / 1024).toFixed(0)}KB),将由上下文管理器自动截断`,
|
||||||
|
data: { oversized: true, size: resultStr.length },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { passed: true, message: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IterationMetricsHook — 迭代度量收集
|
||||||
|
* 每轮迭代后记录关键指标
|
||||||
|
*/
|
||||||
|
export const iterationMetricsHook: HarnessHook = {
|
||||||
|
name: 'IterationMetrics',
|
||||||
|
phase: 'post_iteration',
|
||||||
|
priority: 50,
|
||||||
|
enabled: true,
|
||||||
|
handler: async (ctx: LoopContext, _data: HookData) => {
|
||||||
|
// 收集本轮迭代统计
|
||||||
|
const totalMessages = ctx.messages.length;
|
||||||
|
const toolMessages = ctx.messages.filter(m => m.role === 'tool').length;
|
||||||
|
logDebug(
|
||||||
|
`迭代度量 [第 ${ctx.loopCount} 轮]: ` +
|
||||||
|
`${totalMessages} 消息, ${toolMessages} 工具消息, ` +
|
||||||
|
`${ctx.totalEvalCount} eval tokens, ${ctx.allToolRecords.length} 总工具调用`
|
||||||
|
);
|
||||||
|
return { passed: true, message: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 初始化:注册所有内置 Hook
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
let hooksInitialized = false;
|
||||||
|
|
||||||
|
export function initHarnessHooks(): void {
|
||||||
|
if (hooksInitialized) return;
|
||||||
|
registerHook(securityCheckHook);
|
||||||
|
registerHook(resultValidationHook);
|
||||||
|
registerHook(iterationMetricsHook);
|
||||||
|
hooksInitialized = true;
|
||||||
|
logInfo('Harness Hook 系统已初始化', `${hookRegistry.size} 个 Hook 已注册`);
|
||||||
|
}
|
||||||
@@ -151,8 +151,8 @@ export async function executeMCPTool(toolName: string, args: Record<string, unkn
|
|||||||
return { success: false, error: 'MCP API 不可用' };
|
return { success: false, error: 'MCP API 不可用' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析工具全名: mcp_{serverName}_{toolName}
|
// 解析工具全名: mcp_{serverName}__{toolName}(双下划线分隔,避免 serverName/toolName 含下划线时歧义)
|
||||||
const match = toolName.match(/^mcp_(.+?)_(.+)$/);
|
const match = toolName.match(/^mcp_(.+?)__(.+)$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return { success: false, error: `无效的 MCP 工具名: ${toolName}` };
|
return { success: false, error: `无效的 MCP 工具名: ${toolName}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -465,6 +465,25 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
|
// 新增工具:Plan Mode 执行追踪
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'plan_track',
|
||||||
|
description: 'Manage Plan Mode execution progress. Use this to mark plan steps as completed so you always know what remains. REQUIRED in Plan Mode — call plan_track after completing each step.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['action'],
|
||||||
|
properties: {
|
||||||
|
action: { type: 'string', enum: ['status', 'mark_done', 'mark_all_done'], description: 'status=查看当前进度, mark_done=标记步骤完成, mark_all_done=全部完成' },
|
||||||
|
step_index: { type: 'integer', description: 'Step number (1-indexed) to mark as done. Required for mark_done.' },
|
||||||
|
step_label: { type: 'string', description: 'Optional: description of what was completed, for logging.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
// v5.0 新增工具:浏览器控制
|
// v5.0 新增工具:浏览器控制
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
// v5.1 Browser 控制工具(增强版)
|
// v5.1 Browser 控制工具(增强版)
|
||||||
@@ -717,7 +736,7 @@ let enabledTools: Set<string> = new Set([
|
|||||||
'memory_search', 'memory_add', 'memory_replace', 'memory_remove',
|
'memory_search', 'memory_add', 'memory_replace', 'memory_remove',
|
||||||
'session_list', 'session_read', 'spawn_task',
|
'session_list', 'session_read', 'spawn_task',
|
||||||
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
|
'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract',
|
||||||
'browser_click', 'browser_type', 'browser_scroll', 'browser_close',
|
'browser_click', 'browser_type', 'browser_scroll', 'browser_wait', 'browser_close',
|
||||||
'datetime', 'calculator',
|
'datetime', 'calculator',
|
||||||
'random', 'uuid', 'json_format', 'hash'
|
'random', 'uuid', 'json_format', 'hash'
|
||||||
]);
|
]);
|
||||||
@@ -727,6 +746,17 @@ export function setToolEnabled(toolName: string, enabled: boolean): void {
|
|||||||
else enabledTools.delete(toolName);
|
else enabledTools.delete(toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Plan Mode 激活时注册 plan_track,关闭时移除 */
|
||||||
|
export function setPlanModeActive(active: boolean): void {
|
||||||
|
if (active) {
|
||||||
|
enabledTools.add('plan_track');
|
||||||
|
} else {
|
||||||
|
enabledTools.delete('plan_track');
|
||||||
|
// 清除追踪器状态
|
||||||
|
state.set('_planTracker', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getEnabledToolDefinitions(): ToolDefinition[] {
|
export function getEnabledToolDefinitions(): ToolDefinition[] {
|
||||||
const builtIn = TOOL_DEFINITIONS.filter(def => enabledTools.has(def.function.name));
|
const builtIn = TOOL_DEFINITIONS.filter(def => enabledTools.has(def.function.name));
|
||||||
@@ -879,6 +909,38 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// plan_track — Plan Mode 执行追踪
|
||||||
|
if (toolName === 'plan_track') {
|
||||||
|
const action = args.action as string;
|
||||||
|
const stepIndex = args.step_index as number | undefined;
|
||||||
|
const stepLabel = (args.step_label as string) || '';
|
||||||
|
const tracker = getPlanTracker();
|
||||||
|
if (action === 'status') {
|
||||||
|
return { success: true, action: 'status', steps: tracker.steps, total: tracker.total, done: tracker.done };
|
||||||
|
}
|
||||||
|
if (action === 'mark_done' && typeof stepIndex === 'number' && stepIndex > 0) {
|
||||||
|
const idx = stepIndex - 1;
|
||||||
|
if (idx >= 0 && idx < tracker.steps.length) {
|
||||||
|
tracker.steps[idx].done = true;
|
||||||
|
tracker.done = tracker.steps.filter(s => s.done).length;
|
||||||
|
if (stepLabel) tracker.steps[idx].label = stepLabel;
|
||||||
|
savePlanTracker(tracker);
|
||||||
|
const remaining = tracker.total - tracker.done;
|
||||||
|
logToolResult('plan_track', true, `步骤 ${stepIndex} 已完成 (${tracker.done}/${tracker.total}${remaining > 0 ? `, 剩余 ${remaining}` : ', 全部完成!'})`);
|
||||||
|
return { success: true, action: 'mark_done', step: stepIndex, done: tracker.done, total: tracker.total, remaining };
|
||||||
|
}
|
||||||
|
return { success: false, error: `步骤 ${stepIndex} 不存在(共 ${tracker.total} 步)` };
|
||||||
|
}
|
||||||
|
if (action === 'mark_all_done') {
|
||||||
|
for (const s of tracker.steps) s.done = true;
|
||||||
|
tracker.done = tracker.total;
|
||||||
|
savePlanTracker(tracker);
|
||||||
|
logToolResult('plan_track', true, `全部 ${tracker.total} 步已完成`);
|
||||||
|
return { success: true, action: 'mark_all_done', done: tracker.total, total: tracker.total, remaining: 0 };
|
||||||
|
}
|
||||||
|
return { success: false, error: `未知操作: ${action}` };
|
||||||
|
}
|
||||||
|
|
||||||
// run_command 走 workspace IPC
|
// run_command 走 workspace IPC
|
||||||
if (toolName === 'run_command') {
|
if (toolName === 'run_command') {
|
||||||
const command = args.command as string;
|
const command = args.command as string;
|
||||||
@@ -918,12 +980,70 @@ export function getToolIcon(name: string): string {
|
|||||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||||
memory_search: '🧠', memory_add: '💾', memory_replace: '🔄', memory_remove: '🗑️',
|
memory_search: '🧠', memory_add: '💾', memory_replace: '🔄', memory_remove: '🗑️',
|
||||||
session_list: '📋', session_read: '📖', spawn_task: '🤖',
|
session_list: '📋', session_read: '📖', spawn_task: '🤖',
|
||||||
|
plan_track: '📋',
|
||||||
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
||||||
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
||||||
};
|
};
|
||||||
return icons[name] || '🔧';
|
return icons[name] || '🔧';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ── Plan Mode 执行追踪器 ── */
|
||||||
|
|
||||||
|
export interface PlanStep {
|
||||||
|
index: number;
|
||||||
|
label: string;
|
||||||
|
done: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanTracker {
|
||||||
|
steps: PlanStep[];
|
||||||
|
total: number;
|
||||||
|
done: number;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _planTracker: PlanTracker = { steps: [], total: 0, done: 0, active: false };
|
||||||
|
|
||||||
|
export function getPlanTracker(): PlanTracker {
|
||||||
|
// 每次读取时同步 state 中的最新状态
|
||||||
|
const saved = state.get<PlanTracker | null>('_planTracker', null);
|
||||||
|
if (saved) _planTracker = saved;
|
||||||
|
return _planTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePlanTracker(tracker: PlanTracker): void {
|
||||||
|
_planTracker = tracker;
|
||||||
|
state.set('_planTracker', tracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initPlanTracker(steps: string[]): PlanTracker {
|
||||||
|
const tracker: PlanTracker = {
|
||||||
|
steps: steps.map((label, i) => ({ index: i + 1, label, done: false })),
|
||||||
|
total: steps.length,
|
||||||
|
done: 0,
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
savePlanTracker(tracker);
|
||||||
|
logInfo('Plan Tracker 已初始化', `${tracker.total} 个步骤`);
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearPlanTracker(): void {
|
||||||
|
_planTracker = { steps: [], total: 0, done: 0, active: false };
|
||||||
|
state.set('_planTracker', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPlanStatus(): string {
|
||||||
|
const t = getPlanTracker();
|
||||||
|
if (!t.active || t.steps.length === 0) return '';
|
||||||
|
const lines = t.steps.map(s => {
|
||||||
|
const icon = s.done ? '✅' : '⬜';
|
||||||
|
return `${icon} 步骤${s.index}: ${s.label}`;
|
||||||
|
});
|
||||||
|
const pct = t.total > 0 ? Math.round(t.done / t.total * 100) : 0;
|
||||||
|
return `[Plan Mode 执行进度: ${t.done}/${t.total} (${pct}%)]\n${lines.join('\n')}\n\n完成每一步后请调用 plan_track(action='mark_done', step_index=N) 标记已完成。`;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatToolName(name: string): string {
|
export function formatToolName(name: string): string {
|
||||||
const names: Record<string, string> = {
|
const names: Record<string, string> = {
|
||||||
read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录',
|
read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录',
|
||||||
@@ -935,6 +1055,7 @@ export function formatToolName(name: string): string {
|
|||||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||||
memory_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
|
memory_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
|
||||||
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
|
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
|
||||||
|
plan_track: '计划追踪',
|
||||||
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
||||||
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* Verification System — 验证系统模块 (v0.12.0)
|
||||||
|
* Harness Engineering Phase 5: 计算性反馈管道
|
||||||
|
*
|
||||||
|
* 在 Agent 修改文件后自动运行:
|
||||||
|
* - Linter(代码风格检查)
|
||||||
|
* - Type Checker(类型检查)
|
||||||
|
* - Test Runner(单元测试)
|
||||||
|
* - Diff Analyzer(修改范围分析)
|
||||||
|
*
|
||||||
|
* 设计原则:
|
||||||
|
* - 所有验证利用现有 run_command 工具执行
|
||||||
|
* - 验证 Hook 注册到 hooks.ts 的 post_tool 阶段
|
||||||
|
* - 验证失败的结果作为 Observation 注入 Agent 上下文
|
||||||
|
* - 可配置开关(设置面板)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerHook } from './hooks.js';
|
||||||
|
import type { HarnessHook } from '../types.js';
|
||||||
|
import { logInfo, logDebug, logWarn } from './log-service.js';
|
||||||
|
import type { LoopContext, HookData, HookResult } from '../types.js';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 验证配置
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export interface VerificationConfig {
|
||||||
|
/** 是否启用自动 Lint */
|
||||||
|
autoLint: boolean;
|
||||||
|
/** 是否启用自动类型检查 */
|
||||||
|
autoTypeCheck: boolean;
|
||||||
|
/** 是否启用自动测试 */
|
||||||
|
autoTest: boolean;
|
||||||
|
/** Lint 命令(留空自动检测) */
|
||||||
|
lintCommand: string;
|
||||||
|
/** 类型检查命令 */
|
||||||
|
typeCheckCommand: string;
|
||||||
|
/** 测试命令 */
|
||||||
|
testCommand: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 默认配置 */
|
||||||
|
const defaultConfig: VerificationConfig = {
|
||||||
|
autoLint: false,
|
||||||
|
autoTypeCheck: false,
|
||||||
|
autoTest: false,
|
||||||
|
lintCommand: '',
|
||||||
|
typeCheckCommand: '',
|
||||||
|
testCommand: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 运行时配置 */
|
||||||
|
let config: VerificationConfig = { ...defaultConfig };
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 文件类型检测
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 检测文件扩展名对应的语言 */
|
||||||
|
function detectFileType(filePath: string): string | null {
|
||||||
|
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx',
|
||||||
|
py: 'python', rs: 'rust', go: 'go', java: 'java',
|
||||||
|
cpp: 'cpp', c: 'c', rb: 'ruby', css: 'css',
|
||||||
|
html: 'html', vue: 'vue', svelte: 'svelte',
|
||||||
|
};
|
||||||
|
return map[ext] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 检测项目根目录(查找 package.json / pyproject.toml / Cargo.toml) */
|
||||||
|
function detectProjectRoot(filePath: string): string | null {
|
||||||
|
// 简化实现:向上查找 package.json
|
||||||
|
const parts = filePath.replace(/\\/g, '/').split('/');
|
||||||
|
for (let i = parts.length - 1; i >= 0; i--) {
|
||||||
|
const dir = parts.slice(0, i + 1).join('/');
|
||||||
|
// 这里无法实际访问文件系统,返回文件所在目录的父级作为近似
|
||||||
|
}
|
||||||
|
// 返回文件所在目录
|
||||||
|
const lastSep = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
|
||||||
|
return lastSep > 0 ? filePath.slice(0, lastSep) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 验证 Hook 实现
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DiffAnalyzerHook — 修改范围分析
|
||||||
|
* 检测 Agent 的修改是否跨越了架构边界
|
||||||
|
*/
|
||||||
|
export const diffAnalyzerHook: HarnessHook = {
|
||||||
|
name: 'DiffAnalyzer',
|
||||||
|
phase: 'post_tool',
|
||||||
|
priority: 75,
|
||||||
|
enabled: true,
|
||||||
|
handler: async (_ctx: LoopContext, data: HookData): Promise<HookResult> => {
|
||||||
|
const { toolName, toolArgs } = data;
|
||||||
|
if (toolName !== 'write_file' && toolName !== 'edit_file') {
|
||||||
|
return { passed: true, message: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = (toolArgs?.path as string) || '';
|
||||||
|
if (!filePath) return { passed: true, message: '' };
|
||||||
|
|
||||||
|
// 分析修改的文件类型
|
||||||
|
const fileType = detectFileType(filePath);
|
||||||
|
if (!fileType) return { passed: true, message: '' };
|
||||||
|
|
||||||
|
// 分析跨层调用风险
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
// 检测是否修改了核心基础模块
|
||||||
|
const criticalPaths = ['/core/', '/base/', '/common/', '/shared/', '/utils/', '/lib/'];
|
||||||
|
const filePathLower = filePath.toLowerCase();
|
||||||
|
for (const cp of criticalPaths) {
|
||||||
|
if (filePathLower.includes(cp)) {
|
||||||
|
warnings.push(`⚠️ 修改了核心基础模块 "${filePath}",可能影响多个依赖方`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
return {
|
||||||
|
passed: true, // 警告不阻止,但注入提醒
|
||||||
|
message: warnings.join('; '),
|
||||||
|
data: { warnings },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { passed: true, message: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileChangeAuditHook — 文件变更审计
|
||||||
|
* 记录 Agent 的每一次文件修改
|
||||||
|
*/
|
||||||
|
export const fileChangeAuditHook: HarnessHook = {
|
||||||
|
name: 'FileChangeAudit',
|
||||||
|
phase: 'post_tool',
|
||||||
|
priority: 30,
|
||||||
|
enabled: true,
|
||||||
|
handler: async (ctx: LoopContext, data: HookData): Promise<HookResult> => {
|
||||||
|
const { toolName, toolArgs, toolResult } = data;
|
||||||
|
if (toolName !== 'write_file' && toolName !== 'edit_file' && toolName !== 'delete_file') {
|
||||||
|
return { passed: true, message: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = (toolArgs?.path as string) || '';
|
||||||
|
const success = toolResult?.success || false;
|
||||||
|
|
||||||
|
logInfo(
|
||||||
|
`文件变更审计 [第 ${ctx.loopCount} 轮]: ` +
|
||||||
|
`${toolName} ${filePath} — ${success ? '✅' : '❌'}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { passed: true, message: '' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 初始化
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
let verificationInitialized = false;
|
||||||
|
|
||||||
|
export function initVerificationSystem(): void {
|
||||||
|
if (verificationInitialized) return;
|
||||||
|
registerHook(diffAnalyzerHook);
|
||||||
|
registerHook(fileChangeAuditHook);
|
||||||
|
verificationInitialized = true;
|
||||||
|
logInfo('验证系统已初始化', 'DiffAnalyzer + FileChangeAudit Hook 已注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取验证配置 */
|
||||||
|
export function getVerificationConfig(): VerificationConfig {
|
||||||
|
return { ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新验证配置 */
|
||||||
|
export function updateVerificationConfig(updates: Partial<VerificationConfig>): void {
|
||||||
|
Object.assign(config, updates);
|
||||||
|
logInfo('验证配置已更新', JSON.stringify(config));
|
||||||
|
}
|
||||||
@@ -631,6 +631,52 @@ html, body {
|
|||||||
background: var(--critical-bg);
|
background: var(--critical-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Plan 按钮 (v0.12.0) ── */
|
||||||
|
.plan-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-toggle input { display: none; }
|
||||||
|
|
||||||
|
.plan-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: var(--transition);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-btn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-toggle:hover .plan-btn {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-toggle input:checked + .plan-btn {
|
||||||
|
background: rgba(71, 132, 143, 0.1);
|
||||||
|
border-color: #47848F;
|
||||||
|
color: #47848F;
|
||||||
|
box-shadow: 0 0 16px rgba(71, 132, 143, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-toggle input:checked + .plan-btn:hover {
|
||||||
|
background: rgba(71, 132, 143, 0.15);
|
||||||
|
box-shadow: 0 0 20px rgba(71, 132, 143, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 开关控件 ── */
|
/* ── 开关控件 ── */
|
||||||
.toggle-label {
|
.toggle-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2449,8 +2495,8 @@ html, body {
|
|||||||
═══════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.workspace-panel {
|
.workspace-panel {
|
||||||
width: 480px;
|
width: 400px;
|
||||||
min-width: 480px;
|
min-width: 400px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -3539,6 +3585,12 @@ html, body {
|
|||||||
border: 1px solid rgba(217, 79, 92, 0.15);
|
border: 1px solid rgba(217, 79, 92, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-card-badge.plan-mode {
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(232, 115, 74, 0.08);
|
||||||
|
border: 1px solid rgba(232, 115, 74, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.tool-card-desc {
|
.tool-card-desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|||||||
Vendored
+150
-1
@@ -324,7 +324,22 @@ export type StateKey =
|
|||||||
| 'runCommandMode'
|
| 'runCommandMode'
|
||||||
| 'memoryEnabled'
|
| 'memoryEnabled'
|
||||||
| 'memoryEntries'
|
| 'memoryEntries'
|
||||||
| 'embeddingModel';
|
| 'embeddingModel'
|
||||||
|
| '_defaultModel'
|
||||||
|
| '_lastSystemPrompt'
|
||||||
|
| '_currentEvalCount'
|
||||||
|
| '_abortToolRecords'
|
||||||
|
| '_loopState'
|
||||||
|
| '_loopContext'
|
||||||
|
| 'modelSupportsTools'
|
||||||
|
| 'maxTurns'
|
||||||
|
| 'streamTimeout'
|
||||||
|
| 'subAgentMaxLoops'
|
||||||
|
| 'subAgentTimeout'
|
||||||
|
| 'subAgentModel'
|
||||||
|
| 'agentMode'
|
||||||
|
| 'loopWatchdogMs'
|
||||||
|
| '_planTracker';
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Tool Calling 类型
|
// Tool Calling 类型
|
||||||
@@ -379,6 +394,139 @@ export interface ToolCallRecord {
|
|||||||
|
|
||||||
export type AgentState = 'idle' | 'sending' | 'accumulating' | 'executing' | 'confirming' | 'done';
|
export type AgentState = 'idle' | 'sending' | 'accumulating' | 'executing' | 'confirming' | 'done';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Harness Engineering: Agent Loop 状态机 (v0.12.0)
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Agent Loop 状态枚举 */
|
||||||
|
export type LoopState =
|
||||||
|
| 'INIT' // 构建系统提示词、准备上下文
|
||||||
|
| 'THINKING' // 调用 LLM 流式
|
||||||
|
| 'PARSING' // 解析输出(tool_calls 提取 + 文本兜底)
|
||||||
|
| 'EXECUTING' // 执行工具(按批次并行)
|
||||||
|
| 'OBSERVING' // 收集工具结果、格式化
|
||||||
|
| 'REFLECTING' // 反思:去重检测、过早停止检测、Final Answer 检测
|
||||||
|
| 'COMPRESSING' // 触发上下文压缩
|
||||||
|
| 'TERMINATED'; // 终止
|
||||||
|
|
||||||
|
/** Agent Loop 上下文(可序列化,支持暂停/恢复) */
|
||||||
|
export interface LoopContext {
|
||||||
|
/** 状态机当前状态 */
|
||||||
|
state: LoopState;
|
||||||
|
/** 会话 ID */
|
||||||
|
sessionId: string;
|
||||||
|
/** 当前循环计数 */
|
||||||
|
loopCount: number;
|
||||||
|
/** 最大循环次数 */
|
||||||
|
maxLoops: number;
|
||||||
|
/** 当前轮模型输出内容 */
|
||||||
|
content: string;
|
||||||
|
/** 当前轮思考内容 */
|
||||||
|
thinking: string;
|
||||||
|
/** 本轮工具调用列表 */
|
||||||
|
toolCalls: ToolCall[];
|
||||||
|
/** 所有工具调用记录 */
|
||||||
|
allToolRecords: ToolCallRecord[];
|
||||||
|
/** 消息列表(Ollama 格式) */
|
||||||
|
messages: OllamaMessage[];
|
||||||
|
/** 累计 eval tokens */
|
||||||
|
totalEvalCount: number;
|
||||||
|
/** 累计 prompt_eval tokens */
|
||||||
|
totalPromptEvalCount: number;
|
||||||
|
/** 累计推理时间(纳秒) */
|
||||||
|
totalInferenceNs: number;
|
||||||
|
/** 本轮 eval tokens */
|
||||||
|
loopEvalCount: number;
|
||||||
|
/** 本轮 prompt_eval tokens */
|
||||||
|
loopPromptEvalCount: number;
|
||||||
|
/** 本轮推理时间 */
|
||||||
|
loopInferenceNs: number;
|
||||||
|
/** 上一轮成功工具调用的缓存键(用于去重) */
|
||||||
|
prevLoopSuccessKeys: string[];
|
||||||
|
/** 上一轮工具调用(用于 onNewIteration) */
|
||||||
|
prevToolCalls: ToolCall[];
|
||||||
|
/** Agent 模式:auto(直接执行)| plan(先规划后执行) */
|
||||||
|
mode: AgentMode;
|
||||||
|
/** Plan 模式下等待用户确认的 Promise resolve */
|
||||||
|
planApproved?: boolean;
|
||||||
|
/** Plan Mode 重试计数(防止无限循环) */
|
||||||
|
planRetries: number;
|
||||||
|
/** 循环开始时间 */
|
||||||
|
startTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Agent 运行模式 */
|
||||||
|
export type AgentMode = 'auto' | 'plan';
|
||||||
|
|
||||||
|
/** System Prompt 分区(支持 LLM Prefix Caching) */
|
||||||
|
export interface SystemPromptZones {
|
||||||
|
/** 静态区:SOUL.md + 环境基础 → 利用 LLM Prefix Caching */
|
||||||
|
staticZone: string;
|
||||||
|
/** 动态区:记忆检索 + 工作空间 + 实时日期 */
|
||||||
|
dynamicZone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook 阶段 */
|
||||||
|
export type HookPhase = 'pre_tool' | 'post_tool' | 'post_iteration' | 'pre_completion';
|
||||||
|
|
||||||
|
/** Hook 执行结果 */
|
||||||
|
export interface HookResult {
|
||||||
|
/** 是否通过 */
|
||||||
|
passed: boolean;
|
||||||
|
/** 消息(通过/失败原因) */
|
||||||
|
message: string;
|
||||||
|
/** 附加数据 */
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Harness Hook 接口 */
|
||||||
|
export interface HarnessHook {
|
||||||
|
name: string;
|
||||||
|
phase: HookPhase;
|
||||||
|
priority: number;
|
||||||
|
handler: (ctx: LoopContext, data: HookData) => Promise<HookResult>;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook 上下文数据 */
|
||||||
|
export interface HookData {
|
||||||
|
toolName?: string;
|
||||||
|
toolArgs?: Record<string, unknown>;
|
||||||
|
toolResult?: ToolResult;
|
||||||
|
iterationIndex?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完成门控检查项 */
|
||||||
|
export interface CompletionCheck {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
check: (ctx: LoopContext) => Promise<{ passed: boolean; reason: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Agent 度量指标 */
|
||||||
|
export interface AgentMetrics {
|
||||||
|
totalSessions: number;
|
||||||
|
avgIterationsPerTask: number;
|
||||||
|
toolSuccessRate: number;
|
||||||
|
avgCompletionScore: number;
|
||||||
|
frequentErrors: Array<{ pattern: string; count: number }>;
|
||||||
|
tokenEfficiency: number;
|
||||||
|
collectedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 渐进式披露:上下文层级 */
|
||||||
|
export type ContextTier = 'index' | 'interface' | 'implementation';
|
||||||
|
|
||||||
|
/** 项目索引摘要 */
|
||||||
|
export interface ProjectIndex {
|
||||||
|
structure: string;
|
||||||
|
entryFiles: string[];
|
||||||
|
techStack: string[];
|
||||||
|
tokenCount: number;
|
||||||
|
generatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// ReAct Trace 类型 (v4.0)
|
// ReAct Trace 类型 (v4.0)
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -392,6 +540,7 @@ export interface TraceEntry {
|
|||||||
actionInput: string;
|
actionInput: string;
|
||||||
observation: string;
|
observation: string;
|
||||||
loopCount: number;
|
loopCount: number;
|
||||||
|
errorPattern?: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user