feat(permission): add auto-approval mode with approval agent, UI flow, and docs sync
Implemented a new permission mode between and , including backend policy, runtime behavior, frontend display, and documentation updates. Backend & policy changes: - Added to permission mode validation and persistence paths. - Updated tool permission evaluation: workspace-local direct pass, out-of-workspace requires approval. - Kept readonly-first flow and added auto-approval decision path when permission denial occurs. - Added approval reason/decider support in tool approval manager. - Improved rejection tool-content format to natural language: '工具调用被拒绝\n原因:...' - Fixed permission-mode sync edge cases on conversation create/load and restart-first-switch behavior. Approval agent subsystem: - Added and . - Added lightweight auto-approval config at (name/url/key/model/extra_params + timeouts). - Added optional transcript debug switch in code and transcript output under logs/approval_agent. - Aligned transcript saving toward cumulative messages format and captured reasoning/content/tool_calls/tool sequence. Frontend changes: - Added option to personalization and permission menus. - Reworked approval panel auto-review display into a dedicated block with simplified lines: start, command, final approve/reject + reason. - Added delayed sidebar auto-close (10s) after approval resolved. - Added stricter permission-switch verification and rollback on mismatch/error. Docs updated: - Synced AGENTS.md, README.md, and docs/host_sandbox_and_permission_model.md with new permission mode, approval-agent behavior, config, and current constraints (including docker caveat).
This commit is contained in:
parent
02d10ec7bb
commit
8d665ad6de
15
AGENTS.md
15
AGENTS.md
@ -161,7 +161,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
|||||||
|
|
||||||
为避免误解,当前系统有两套独立但叠加的控制:
|
为避免误解,当前系统有两套独立但叠加的控制:
|
||||||
|
|
||||||
1. **权限模式(Permission Mode)**:`readonly` / `approval` / `unrestricted`
|
1. **权限模式(Permission Mode)**:`readonly` / `approval` / `auto_approval` / `unrestricted`
|
||||||
2. **执行环境(Execution Mode)**:`sandbox` / `direct`(仅宿主机模式可切换)
|
2. **执行环境(Execution Mode)**:`sandbox` / `direct`(仅宿主机模式可切换)
|
||||||
|
|
||||||
### 10.1 权限模式
|
### 10.1 权限模式
|
||||||
@ -173,6 +173,10 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
|||||||
- `run_command` 先走只读沙箱
|
- `run_command` 先走只读沙箱
|
||||||
- 若出现权限拒绝(例如 `Operation not permitted` / `Permission denied`),触发前端审批
|
- 若出现权限拒绝(例如 `Operation not permitted` / `Permission denied`),触发前端审批
|
||||||
- 审批通过后,仅该次命令以可写沙箱重试;工具结果返回“重试后的最终结果”
|
- 审批通过后,仅该次命令以可写沙箱重试;工具结果返回“重试后的最终结果”
|
||||||
|
- `auto_approval`
|
||||||
|
- `write_file` / `edit_file`:工作区内路径直接执行,工作区外路径进入审批流程
|
||||||
|
- `run_command` 先走只读沙箱;触发权限拒绝后由后台审批智能体自动审核
|
||||||
|
- 自动审核拒绝时,工具返回“被拒绝+理由”并继续主循环(不强制结束任务)
|
||||||
- `unrestricted`
|
- `unrestricted`
|
||||||
- 不做权限模式拦截;是否沙箱由执行环境决定
|
- 不做权限模式拦截;是否沙箱由执行环境决定
|
||||||
|
|
||||||
@ -195,3 +199,12 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
|||||||
|
|
||||||
- 不要在提示词里暴露内部实现细节(例如“命令文本猜测”等)
|
- 不要在提示词里暴露内部实现细节(例如“命令文本猜测”等)
|
||||||
- 文案应面向用户能力边界与操作建议,不描述内部判定算法
|
- 文案应面向用户能力边界与操作建议,不描述内部判定算法
|
||||||
|
|
||||||
|
### 10.5 自动审批智能体配置与调试
|
||||||
|
|
||||||
|
- 自动审批配置文件:`config/auto_approval.json`
|
||||||
|
- 建议字段:`name` / `url` / `key` / `model` / `extra_params`
|
||||||
|
- 额外控制:`timeout_seconds` / `max_rounds` / `max_command_timeout`
|
||||||
|
- 调试开关(代码变量):`modules/approval_agent.py` 中 `DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT`
|
||||||
|
- 开启后写入:`logs/approval_agent/`
|
||||||
|
- 记录以累积 `messages` 为主,便于对齐主/子智能体会话格式
|
||||||
|
|||||||
@ -136,12 +136,18 @@ HOST_WORKSPACES_FILE=./config/host_workspaces.json
|
|||||||
- **权限模式(Permission Mode)**
|
- **权限模式(Permission Mode)**
|
||||||
- `readonly`:`run_command` 在只读沙箱执行;写入会被系统拒绝
|
- `readonly`:`run_command` 在只读沙箱执行;写入会被系统拒绝
|
||||||
- `approval`:`run_command` 先按只读沙箱执行;若遇权限拒绝,再触发前端审批;审批通过后仅该次命令以可写沙箱重试
|
- `approval`:`run_command` 先按只读沙箱执行;若遇权限拒绝,再触发前端审批;审批通过后仅该次命令以可写沙箱重试
|
||||||
|
- `auto_approval`:工作区内 `write_file/edit_file` 直通;高风险操作由后台审批智能体自动审核。`run_command` 仍先只读执行,遇权限拒绝后再自动审批
|
||||||
- `unrestricted`:不做权限模式拦截(仍受执行环境约束)
|
- `unrestricted`:不做权限模式拦截(仍受执行环境约束)
|
||||||
- **路径授权(Path Authorization)**
|
- **路径授权(Path Authorization)**
|
||||||
- 可配置两类路径:`可读可写` 与 `仅可读`
|
- 可配置两类路径:`可读可写` 与 `仅可读`
|
||||||
- 语义:`可读集合 = 可读可写 + 仅可读`;`可写集合 = 可读可写`
|
- 语义:`可读集合 = 可读可写 + 仅可读`;`可写集合 = 可读可写`
|
||||||
- 默认建议:仅工作区 + 临时目录可写,其余按需白名单
|
- 默认建议:仅工作区 + 临时目录可写,其余按需白名单
|
||||||
|
|
||||||
|
- **自动审批智能体(Auto Approval Agent)**
|
||||||
|
- 配置文件:`config/auto_approval.json`
|
||||||
|
- 核心字段:`name` / `url` / `key` / `model` / `extra_params`
|
||||||
|
- 调试记录:可在 `modules/approval_agent.py` 打开 `DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT`,输出到 `logs/approval_agent/`
|
||||||
|
|
||||||
`HOST_WORKSPACES_FILE` 对应 JSON 示例:
|
`HOST_WORKSPACES_FILE` 对应 JSON 示例:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
14
config/auto_approval.json
Normal file
14
config/auto_approval.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "auto-approval-agent",
|
||||||
|
"url": "https://api.deepseek.com",
|
||||||
|
"key": "${API_KEY_DEEPSEEK}",
|
||||||
|
"model": "deepseek-v4-flash",
|
||||||
|
"extra_params": {
|
||||||
|
"thinking": {
|
||||||
|
"type": "enabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout_seconds": 60,
|
||||||
|
"max_rounds": 3,
|
||||||
|
"max_command_timeout": 60
|
||||||
|
}
|
||||||
@ -93,12 +93,14 @@ class MainTerminalContextMixin:
|
|||||||
"unrestricted": "当前处于无限制模式,所有工具均可直接使用,无需额外批准。",
|
"unrestricted": "当前处于无限制模式,所有工具均可直接使用,无需额外批准。",
|
||||||
"readonly": "当前处于只读模式,仅能执行不会修改工作区的读取类操作。",
|
"readonly": "当前处于只读模式,仅能执行不会修改工作区的读取类操作。",
|
||||||
"approval": "当前处于批准模式,修改工作区的操作需要用户批准后方可执行。",
|
"approval": "当前处于批准模式,修改工作区的操作需要用户批准后方可执行。",
|
||||||
|
"auto_approval": "当前处于自动审核模式,工作区内文件编辑可直接执行,高风险操作由后台审批智能体自动审核。",
|
||||||
}
|
}
|
||||||
|
|
||||||
_PERMISSION_MODE_LABEL = {
|
_PERMISSION_MODE_LABEL = {
|
||||||
"unrestricted": "无限制",
|
"unrestricted": "无限制",
|
||||||
"readonly": "只读",
|
"readonly": "只读",
|
||||||
"approval": "批准",
|
"approval": "批准",
|
||||||
|
"auto_approval": "自动审核",
|
||||||
}
|
}
|
||||||
|
|
||||||
_EXECUTION_MODE_LABEL = {
|
_EXECUTION_MODE_LABEL = {
|
||||||
@ -135,6 +137,14 @@ class MainTerminalContextMixin:
|
|||||||
"- 需要用户批准:terminal_input、run_python、write_file、edit_file、save_webpage 及其他会修改工作区的操作\n"
|
"- 需要用户批准:terminal_input、run_python、write_file、edit_file、save_webpage 及其他会修改工作区的操作\n"
|
||||||
"- 被拒绝或超时:本次操作不会执行写入"
|
"- 被拒绝或超时:本次操作不会执行写入"
|
||||||
)
|
)
|
||||||
|
elif mode == "auto_approval":
|
||||||
|
detailed_rules = (
|
||||||
|
"- write_file / edit_file:若路径在当前工作区内,直接执行;工作区外路径会进入审批流程\n"
|
||||||
|
"- run_command:先在系统只读沙箱执行;仅当出现权限拒绝时才触发自动审批\n"
|
||||||
|
"- 自动审批由后台审批智能体执行,默认只判断危险性与越权风险,不判断任务必要性\n"
|
||||||
|
"- 自动审批拒绝:本次工具调用会返回“拒绝+理由”,主循环继续;人工拒绝可随时接管\n"
|
||||||
|
"- 可随时人工接管:用户在审批面板点击同意/拒绝/切换无限制后,自动审批会立即停止并以人工决策为准"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
detailed_rules = ""
|
detailed_rules = ""
|
||||||
|
|
||||||
|
|||||||
@ -341,6 +341,29 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
return {"allowed": True, "mode": mode, "requires_approval": True}
|
return {"allowed": True, "mode": mode, "requires_approval": True}
|
||||||
return {"allowed": True, "mode": mode}
|
return {"allowed": True, "mode": mode}
|
||||||
|
|
||||||
|
if mode == "auto_approval":
|
||||||
|
if tool_name in {"write_file", "edit_file"}:
|
||||||
|
path = args.get("file_path") or args.get("path") or ""
|
||||||
|
try:
|
||||||
|
valid, _, full_path = self.file_manager._validate_path(str(path))
|
||||||
|
if valid and full_path is not None:
|
||||||
|
project_root = Path(self.context_manager.project_path).resolve()
|
||||||
|
resolved = Path(full_path).resolve()
|
||||||
|
if resolved == project_root or project_root in resolved.parents:
|
||||||
|
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"allowed": True, "mode": mode, "requires_approval": True}
|
||||||
|
if tool_name == "run_command":
|
||||||
|
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||||
|
if tool_name == "terminal_input":
|
||||||
|
command_text = args.get("command") or args.get("input")
|
||||||
|
if self._is_readonly_run_command_allowed(command_text):
|
||||||
|
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||||
|
if tool_name in self._APPROVAL_REQUIRED_TOOLS:
|
||||||
|
return {"allowed": True, "mode": mode, "requires_approval": True}
|
||||||
|
return {"allowed": True, "mode": mode}
|
||||||
|
|
||||||
return {"allowed": True, "mode": mode}
|
return {"allowed": True, "mode": mode}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1272,7 +1295,7 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
write_granted_once = bool(arguments.get("_approval_write_granted", False))
|
write_granted_once = bool(arguments.get("_approval_write_granted", False))
|
||||||
sandbox_write_access = not (
|
sandbox_write_access = not (
|
||||||
permission_mode == "readonly"
|
permission_mode == "readonly"
|
||||||
or (permission_mode == "approval" and not write_granted_once)
|
or (permission_mode in {"approval", "auto_approval"} and not write_granted_once)
|
||||||
)
|
)
|
||||||
run_in_background = bool(arguments.get("run_in_background", False))
|
run_in_background = bool(arguments.get("run_in_background", False))
|
||||||
timeout_value = arguments.get("timeout")
|
timeout_value = arguments.get("timeout")
|
||||||
|
|||||||
@ -84,7 +84,7 @@ from config.model_profiles import (
|
|||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
DISABLE_LENGTH_CHECK = True
|
DISABLE_LENGTH_CHECK = True
|
||||||
PERMISSION_MODES = {"readonly", "approval", "unrestricted"}
|
PERMISSION_MODES = {"readonly", "approval", "auto_approval", "unrestricted"}
|
||||||
|
|
||||||
class MainTerminalToolsPolicyMixin:
|
class MainTerminalToolsPolicyMixin:
|
||||||
def _ensure_runtime_tool_overrides(self) -> Dict[str, bool]:
|
def _ensure_runtime_tool_overrides(self) -> Dict[str, bool]:
|
||||||
@ -209,7 +209,7 @@ class MainTerminalToolsPolicyMixin:
|
|||||||
def set_permission_mode(self, mode: str, *, persist: bool = True, conversation_id: Optional[str] = None) -> str:
|
def set_permission_mode(self, mode: str, *, persist: bool = True, conversation_id: Optional[str] = None) -> str:
|
||||||
normalized = str(mode or "").strip().lower()
|
normalized = str(mode or "").strip().lower()
|
||||||
if normalized not in PERMISSION_MODES:
|
if normalized not in PERMISSION_MODES:
|
||||||
raise ValueError("无效权限模式,仅支持 readonly / approval / unrestricted")
|
raise ValueError("无效权限模式,仅支持 readonly / approval / auto_approval / unrestricted")
|
||||||
self.current_permission_mode = normalized
|
self.current_permission_mode = normalized
|
||||||
if not persist:
|
if not persist:
|
||||||
return normalized
|
return normalized
|
||||||
|
|||||||
@ -130,7 +130,13 @@ class WebTerminal(MainTerminal):
|
|||||||
logger.warning("忽略无效默认模型 %s: %s", preferred_model, exc)
|
logger.warning("忽略无效默认模型 %s: %s", preferred_model, exc)
|
||||||
|
|
||||||
preferred_mode = prefs.get("default_run_mode")
|
preferred_mode = prefs.get("default_run_mode")
|
||||||
preferred_permission_mode = prefs.get("default_permission_mode") or "unrestricted"
|
preferred_permission_mode = None
|
||||||
|
try:
|
||||||
|
preferred_permission_mode = self.get_permission_mode()
|
||||||
|
except Exception:
|
||||||
|
preferred_permission_mode = None
|
||||||
|
if not isinstance(preferred_permission_mode, str) or not preferred_permission_mode.strip():
|
||||||
|
preferred_permission_mode = prefs.get("default_permission_mode") or "unrestricted"
|
||||||
if isinstance(preferred_mode, str) and preferred_mode.lower() in {"fast", "thinking", "deep"}:
|
if isinstance(preferred_mode, str) and preferred_mode.lower() in {"fast", "thinking", "deep"}:
|
||||||
try:
|
try:
|
||||||
self.set_run_mode(preferred_mode.lower())
|
self.set_run_mode(preferred_mode.lower())
|
||||||
@ -210,6 +216,12 @@ class WebTerminal(MainTerminal):
|
|||||||
self.thinking_mode = mode
|
self.thinking_mode = mode
|
||||||
self.api_client.thinking_mode = mode
|
self.api_client.thinking_mode = mode
|
||||||
self.api_client.start_new_task()
|
self.api_client.start_new_task()
|
||||||
|
permission_mode = str(meta.get("permission_mode") or "").strip().lower()
|
||||||
|
if permission_mode:
|
||||||
|
try:
|
||||||
|
self.set_permission_mode(permission_mode, persist=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# 重置相关状态
|
# 重置相关状态
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
- `readonly`
|
- `readonly`
|
||||||
- `approval`
|
- `approval`
|
||||||
|
- `auto_approval`
|
||||||
- `unrestricted`
|
- `unrestricted`
|
||||||
|
|
||||||
作用:决定工具是否允许调用、是否先只读执行、是否需要用户审批。
|
作用:决定工具是否允许调用、是否先只读执行、是否需要用户审批。
|
||||||
@ -89,7 +90,19 @@
|
|||||||
- 工具结果返回“最终执行结果”(即重试后结果),不会把中间权限拒绝作为最终结果返回给模型
|
- 工具结果返回“最终执行结果”(即重试后结果),不会把中间权限拒绝作为最终结果返回给模型
|
||||||
- 审批拒绝或超时:返回 rejected,不执行写入重试
|
- 审批拒绝或超时:返回 rejected,不执行写入重试
|
||||||
|
|
||||||
## 5.3 unrestricted
|
## 5.3 auto_approval
|
||||||
|
|
||||||
|
- `write_file` / `edit_file`:
|
||||||
|
- 目标路径在当前工作区内:直接执行
|
||||||
|
- 目标路径不在当前工作区内:进入审批流程
|
||||||
|
- `run_command` 采用“两段式”:
|
||||||
|
1) 先在只读沙箱执行
|
||||||
|
2) 若出现权限拒绝,触发自动审批(审批智能体)
|
||||||
|
3) 自动审批通过后,仅该次命令可写重试
|
||||||
|
- 自动审批拒绝:返回“工具调用被拒绝 + 原因”,主智能体继续下一步(不强制中断整轮任务)
|
||||||
|
- 用户可随时人工接管审批结果(同意/拒绝/切换无限制)
|
||||||
|
|
||||||
|
## 5.4 unrestricted
|
||||||
|
|
||||||
- 权限模式层不拦截工具
|
- 权限模式层不拦截工具
|
||||||
- 是否沙箱执行取决于执行环境:
|
- 是否沙箱执行取决于执行环境:
|
||||||
@ -121,6 +134,7 @@
|
|||||||
- 支持权限模式联动(readonly/approval/unrestricted)
|
- 支持权限模式联动(readonly/approval/unrestricted)
|
||||||
- 在 host + sandbox 下可走只读沙箱或可写沙箱
|
- 在 host + sandbox 下可走只读沙箱或可写沙箱
|
||||||
- 在 approval 模式可触发“权限拒绝 -> 审批 -> 单次可写重试”
|
- 在 approval 模式可触发“权限拒绝 -> 审批 -> 单次可写重试”
|
||||||
|
- 在 auto_approval 模式可触发“权限拒绝 -> 自动审批 -> 单次可写重试”
|
||||||
|
|
||||||
### 7.2 run_python
|
### 7.2 run_python
|
||||||
|
|
||||||
@ -140,6 +154,19 @@
|
|||||||
- `direct`:直接宿主机启动
|
- `direct`:直接宿主机启动
|
||||||
- Docker 模式维持容器执行
|
- Docker 模式维持容器执行
|
||||||
|
|
||||||
|
### 7.5 自动审批(approval agent)
|
||||||
|
|
||||||
|
- 自动审批由独立审批智能体执行(与主智能体分离)
|
||||||
|
- 配置文件:`config/auto_approval.json`
|
||||||
|
- `name` / `url` / `key` / `model` / `extra_params`
|
||||||
|
- `timeout_seconds` / `max_rounds` / `max_command_timeout`
|
||||||
|
- 审批智能体可调用能力:
|
||||||
|
- `run_command`(用于只读核查)
|
||||||
|
- 审批决策工具(approved / rejected + reason)
|
||||||
|
- 调试开关(代码变量):
|
||||||
|
- `modules/approval_agent.py` 中 `DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT`
|
||||||
|
- 开启后记录到 `logs/approval_agent/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 配置与运行建议
|
## 8. 配置与运行建议
|
||||||
@ -167,6 +194,7 @@
|
|||||||
1. `run_command` 若严格收紧读路径,可能导致大量系统命令可用性下降
|
1. `run_command` 若严格收紧读路径,可能导致大量系统命令可用性下降
|
||||||
2. 权限拒绝识别基于错误特征,需持续优化误判/漏判边界
|
2. 权限拒绝识别基于错误特征,需持续优化误判/漏判边界
|
||||||
3. 不同 OS 的沙箱能力不完全对齐(Windows 侧为 WSL2 路径)
|
3. 不同 OS 的沙箱能力不完全对齐(Windows 侧为 WSL2 路径)
|
||||||
|
4. Docker 模式下目前不存在与宿主机同等的 OS 只读沙箱机制;`sandbox_write_access=False` 不会提供同等级只读隔离(需后续补充容器侧只读策略)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -175,4 +203,3 @@
|
|||||||
- 面向用户的提示词仅描述“能力边界”和“可操作建议”
|
- 面向用户的提示词仅描述“能力边界”和“可操作建议”
|
||||||
- 不暴露内部实现细节(如内部判定逻辑、实现细节名称)
|
- 不暴露内部实现细节(如内部判定逻辑、实现细节名称)
|
||||||
- 审批流程文案要强调“单次授权、最小权限、可回退”
|
- 审批流程文案要强调“单次授权、最小权限、可回退”
|
||||||
|
|
||||||
|
|||||||
323
modules/approval_agent.py
Normal file
323
modules/approval_agent.py
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_PATH = Path(__file__).resolve().parents[1] / "config" / "auto_approval.json"
|
||||||
|
DEFAULT_MAX_ROUNDS = 3
|
||||||
|
DEFAULT_TIMEOUT_SECONDS = 60
|
||||||
|
DEFAULT_MAX_COMMAND_TIMEOUT = 20
|
||||||
|
DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT = True
|
||||||
|
DEBUG_TRANSCRIPT_DIR = Path(__file__).resolve().parents[1] / "logs" / "approval_agent"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_env_token(value: str) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text.startswith("${") and text.endswith("}"):
|
||||||
|
key = text[2:-1].strip()
|
||||||
|
return os.environ.get(key, "")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def load_approval_agent_config() -> Dict[str, Any]:
|
||||||
|
base = {
|
||||||
|
"name": "auto-approval-agent",
|
||||||
|
"url": "",
|
||||||
|
"key": "",
|
||||||
|
"model": "deepseek-v4-flash",
|
||||||
|
"extra_params": {},
|
||||||
|
"timeout_seconds": DEFAULT_TIMEOUT_SECONDS,
|
||||||
|
"max_rounds": DEFAULT_MAX_ROUNDS,
|
||||||
|
"max_command_timeout": DEFAULT_MAX_COMMAND_TIMEOUT,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
if not CONFIG_PATH.exists():
|
||||||
|
return base
|
||||||
|
raw = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
base.update(raw)
|
||||||
|
base["url"] = _resolve_env_token(base.get("url", ""))
|
||||||
|
base["key"] = _resolve_env_token(base.get("key", ""))
|
||||||
|
base["model"] = str(base.get("model") or "deepseek-v4-flash").strip()
|
||||||
|
base["extra_params"] = base.get("extra_params") if isinstance(base.get("extra_params"), dict) else {}
|
||||||
|
base["timeout_seconds"] = int(base.get("timeout_seconds") or DEFAULT_TIMEOUT_SECONDS)
|
||||||
|
base["max_rounds"] = max(1, int(base.get("max_rounds") or DEFAULT_MAX_ROUNDS))
|
||||||
|
base["max_command_timeout"] = max(1, int(base.get("max_command_timeout") or DEFAULT_MAX_COMMAND_TIMEOUT))
|
||||||
|
return base
|
||||||
|
except Exception:
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovalAgent:
|
||||||
|
def __init__(self, *, web_terminal: Any):
|
||||||
|
self.web_terminal = web_terminal
|
||||||
|
self.cfg = load_approval_agent_config()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _system_prompt() -> str:
|
||||||
|
return (
|
||||||
|
"你是审批智能体,只负责风险与越权判断,不判断任务必要性。"
|
||||||
|
"审批模式允许正常文件修改,因此“写入/删除”不能一概拒绝,应结合用户输入意图判断。"
|
||||||
|
"若操作涉及凭据读取(如 .env、密钥文件、token)、越权访问(工作区外敏感目录)、"
|
||||||
|
"危险破坏(如 rm -rf、权限变更、系统级配置修改、可疑网络外传),"
|
||||||
|
"且用户输入并未明确要求这些行为,则应拒绝。"
|
||||||
|
"需要特别注意:提权命令、删除系统文件、批量破坏性 git 操作、访问 SSH/GPG/AWS 凭据目录。"
|
||||||
|
"你可最多使用少量只读检查(尽量在3轮内完成),然后必须调用 approve_decision 给出 approved/rejected + reason。"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _run_readonly_command(self, command: str) -> Dict[str, Any]:
|
||||||
|
work_path = Path(getattr(self.web_terminal.context_manager, "project_path", "."))
|
||||||
|
timeout = int(self.cfg.get("max_command_timeout") or DEFAULT_MAX_COMMAND_TIMEOUT)
|
||||||
|
try:
|
||||||
|
result = await self.web_terminal.terminal_ops.run_command(
|
||||||
|
command=command,
|
||||||
|
working_dir=str(work_path),
|
||||||
|
timeout=timeout,
|
||||||
|
sandbox_write_access=False,
|
||||||
|
)
|
||||||
|
return result if isinstance(result, dict) else {"success": False, "error": "run_command 返回格式异常"}
|
||||||
|
except Exception as exc:
|
||||||
|
return {"success": False, "error": str(exc)}
|
||||||
|
|
||||||
|
async def review(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
payload_text: str,
|
||||||
|
progress_cb: Optional[Callable[[Dict[str, Any]], None]] = None,
|
||||||
|
cancel_check: Optional[Callable[[], Optional[Dict[str, Any]]]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
debug_transcript: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def _trace(event: str, payload: Dict[str, Any]) -> None:
|
||||||
|
if not DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT:
|
||||||
|
return
|
||||||
|
debug_transcript.append({
|
||||||
|
"ts": int(time.time() * 1000),
|
||||||
|
"event": event,
|
||||||
|
"payload": payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _flush_trace(final_result: Dict[str, Any]) -> None:
|
||||||
|
if not DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
DEBUG_TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
row = {
|
||||||
|
"created_at": int(time.time() * 1000),
|
||||||
|
"messages": messages,
|
||||||
|
}
|
||||||
|
file = DEBUG_TRANSCRIPT_DIR / f"approval_{int(time.time() * 1000)}.json"
|
||||||
|
file.write_text(json.dumps(row, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
url = str(self.cfg.get("url") or "").strip()
|
||||||
|
key = str(self.cfg.get("key") or "").strip()
|
||||||
|
if not url or not key:
|
||||||
|
out = {"decision": "rejected", "reason": "审批智能体配置缺失", "source": "approval_agent"}
|
||||||
|
_flush_trace(out)
|
||||||
|
return out
|
||||||
|
endpoint = f"{url.rstrip('/')}/chat/completions"
|
||||||
|
headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
||||||
|
max_rounds = int(self.cfg.get("max_rounds") or DEFAULT_MAX_ROUNDS)
|
||||||
|
timeout_seconds = int(self.cfg.get("timeout_seconds") or DEFAULT_TIMEOUT_SECONDS)
|
||||||
|
extra_params = dict(self.cfg.get("extra_params") or {})
|
||||||
|
tools = [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "run_command",
|
||||||
|
"description": (
|
||||||
|
"在终端中执行只读指令。可用于查看文件内容、搜索代码/文本、检查目录结构、"
|
||||||
|
"查看 git 状态与差异、确认路径是否存在、收集审批判断所需证据。"
|
||||||
|
"禁止用于写入、删除、改权限或其他修改性操作。"
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"要执行的终端命令字符串。应优先使用只读命令,例如 ls、cat、grep、find、"
|
||||||
|
"rg、git status、git diff、pwd、stat 等。"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["command"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "approve_decision",
|
||||||
|
"description": (
|
||||||
|
"提交最终审批结果并结束本次审批流程。只能返回 approved 或 rejected,"
|
||||||
|
"并提供简洁、可审计的理由。"
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"decision": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["approved", "rejected"],
|
||||||
|
"description": "审批结论:approved 表示同意执行;rejected 表示拒绝执行。",
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"审批理由。应明确风险点或放行依据,例如是否涉及凭据访问、越权路径、"
|
||||||
|
"破坏性命令、以及是否与用户输入意图一致。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["decision", "reason"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
messages: List[Dict[str, Any]] = [
|
||||||
|
{"role": "system", "content": self._system_prompt()},
|
||||||
|
{"role": "user", "content": payload_text},
|
||||||
|
]
|
||||||
|
async with httpx.AsyncClient(timeout=timeout_seconds) as client:
|
||||||
|
rounds = 0
|
||||||
|
while rounds < max_rounds + 1:
|
||||||
|
rounds += 1
|
||||||
|
if cancel_check:
|
||||||
|
external = cancel_check()
|
||||||
|
if external:
|
||||||
|
return external
|
||||||
|
if progress_cb:
|
||||||
|
progress_cb({"stage": "model_call", "round": rounds, "message": f"审批轮次 {rounds}"})
|
||||||
|
req = {
|
||||||
|
"model": self.cfg.get("model"),
|
||||||
|
"messages": messages,
|
||||||
|
"tools": tools,
|
||||||
|
"tool_choice": "auto",
|
||||||
|
"temperature": 0.0,
|
||||||
|
**extra_params,
|
||||||
|
}
|
||||||
|
_trace("request", {"round": rounds, "request": req})
|
||||||
|
try:
|
||||||
|
resp = await client.post(endpoint, headers=headers, json=req)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
body = ""
|
||||||
|
try:
|
||||||
|
body = exc.response.text
|
||||||
|
except Exception:
|
||||||
|
body = str(exc)
|
||||||
|
_trace("http_error", {"round": rounds, "status_code": exc.response.status_code if exc.response else None, "body": body})
|
||||||
|
# 兼容某些模型/网关不接受额外参数:自动降级重试一次(去掉 extra_params)
|
||||||
|
if extra_params:
|
||||||
|
retry_req = {
|
||||||
|
"model": self.cfg.get("model"),
|
||||||
|
"messages": messages,
|
||||||
|
"tools": tools,
|
||||||
|
"tool_choice": "auto",
|
||||||
|
"temperature": 0.0,
|
||||||
|
}
|
||||||
|
_trace("retry_request_without_extra_params", {"round": rounds, "request": retry_req})
|
||||||
|
retry_resp = await client.post(endpoint, headers=headers, json=retry_req)
|
||||||
|
try:
|
||||||
|
retry_resp.raise_for_status()
|
||||||
|
resp = retry_resp
|
||||||
|
except httpx.HTTPStatusError:
|
||||||
|
out = {
|
||||||
|
"decision": "rejected",
|
||||||
|
"reason": f"审批智能体请求失败({retry_resp.status_code})",
|
||||||
|
"source": "approval_agent",
|
||||||
|
}
|
||||||
|
_flush_trace(out)
|
||||||
|
return out
|
||||||
|
else:
|
||||||
|
out = {
|
||||||
|
"decision": "rejected",
|
||||||
|
"reason": f"审批智能体请求失败({exc.response.status_code if exc.response else 'unknown'})",
|
||||||
|
"source": "approval_agent",
|
||||||
|
}
|
||||||
|
_flush_trace(out)
|
||||||
|
return out
|
||||||
|
choice = ((resp.json().get("choices") or [{}])[0] or {}).get("message") or {}
|
||||||
|
reasoning_content = (
|
||||||
|
choice.get("reasoning_content")
|
||||||
|
or choice.get("reasoning")
|
||||||
|
or choice.get("thinking")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
_trace(
|
||||||
|
"response",
|
||||||
|
{
|
||||||
|
"round": rounds,
|
||||||
|
"message": choice,
|
||||||
|
"reasoning_content": reasoning_content,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
tool_calls = choice.get("tool_calls") or []
|
||||||
|
content = choice.get("content")
|
||||||
|
if not tool_calls:
|
||||||
|
if content or reasoning_content:
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": str(content or ""),
|
||||||
|
"reasoning_content": str(reasoning_content or ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if rounds > max_rounds:
|
||||||
|
messages.append({"role": "user", "content": "请立刻根据当前已知信息决定:同意或拒绝,并给出理由。"})
|
||||||
|
continue
|
||||||
|
out = {"decision": "rejected", "reason": "审批智能体未产出可执行决策", "source": "approval_agent"}
|
||||||
|
_flush_trace(out)
|
||||||
|
return out
|
||||||
|
# 有 tool_calls 时追加一条完整 assistant 消息(与主智能体结构对齐)
|
||||||
|
assistant_message = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": str(content or ""),
|
||||||
|
"reasoning_content": str(reasoning_content or ""),
|
||||||
|
"tool_calls": tool_calls,
|
||||||
|
}
|
||||||
|
messages.append(assistant_message)
|
||||||
|
for call in tool_calls:
|
||||||
|
fn = (call.get("function") or {}).get("name")
|
||||||
|
raw_args = (call.get("function") or {}).get("arguments") or "{}"
|
||||||
|
try:
|
||||||
|
args = json.loads(raw_args) if isinstance(raw_args, str) else (raw_args or {})
|
||||||
|
except Exception:
|
||||||
|
args = {}
|
||||||
|
if fn == "approve_decision":
|
||||||
|
decision = str(args.get("decision") or "").strip().lower()
|
||||||
|
reason = str(args.get("reason") or "").strip() or "无理由"
|
||||||
|
if decision in {"approved", "rejected"}:
|
||||||
|
out = {"decision": decision, "reason": reason, "source": "approval_agent"}
|
||||||
|
_flush_trace(out)
|
||||||
|
return out
|
||||||
|
out = {"decision": "rejected", "reason": "审批智能体返回非法决策", "source": "approval_agent"}
|
||||||
|
_flush_trace(out)
|
||||||
|
return out
|
||||||
|
if fn == "run_command":
|
||||||
|
cmd = str(args.get("command") or "").strip()
|
||||||
|
if progress_cb:
|
||||||
|
progress_cb({"stage": "run_command", "round": rounds, "command": cmd})
|
||||||
|
tool_result = await self._run_readonly_command(cmd) if cmd else {"success": False, "error": "command 不能为空"}
|
||||||
|
_trace("tool_result", {"round": rounds, "tool": "run_command", "command": cmd, "result": tool_result})
|
||||||
|
tool_content = json.dumps(tool_result, ensure_ascii=False)
|
||||||
|
messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"content": tool_content,
|
||||||
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()) + f".{int((time.time()%1)*1000000):06d}",
|
||||||
|
"message_id": f"msg_{uuid.uuid4().hex}",
|
||||||
|
"tool_call_id": call.get("id"),
|
||||||
|
"name": "run_command",
|
||||||
|
})
|
||||||
|
out = {"decision": "rejected", "reason": "审批智能体超出最大轮数", "source": "approval_agent"}
|
||||||
|
_flush_trace(out)
|
||||||
|
return out
|
||||||
92
modules/auto_approval_service.py
Normal file
92
modules/auto_approval_service.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from modules.approval_agent import ApprovalAgent
|
||||||
|
from server.state import tool_approval_manager
|
||||||
|
|
||||||
|
|
||||||
|
def build_auto_approval_payload(
|
||||||
|
*,
|
||||||
|
web_terminal: Any,
|
||||||
|
recent_tool_actions: List[Dict[str, Any]],
|
||||||
|
function_name: str,
|
||||||
|
arguments: Dict[str, Any],
|
||||||
|
) -> str:
|
||||||
|
history = list(getattr(getattr(web_terminal, "context_manager", None), "conversation_history", []) or [])
|
||||||
|
user_input = ""
|
||||||
|
for msg in reversed(history):
|
||||||
|
if not isinstance(msg, dict) or msg.get("role") != "user":
|
||||||
|
continue
|
||||||
|
metadata = msg.get("metadata") or {}
|
||||||
|
if bool(metadata.get("is_auto_generated")):
|
||||||
|
continue
|
||||||
|
content = msg.get("content")
|
||||||
|
if isinstance(content, str) and content.strip():
|
||||||
|
user_input = content.strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
recent = recent_tool_actions[-3:] if isinstance(recent_tool_actions, list) else []
|
||||||
|
lines = [
|
||||||
|
"请按照流程审批:",
|
||||||
|
f"用户输入:{user_input or '(空)'}",
|
||||||
|
"AI运行的前三步操作:",
|
||||||
|
]
|
||||||
|
if recent:
|
||||||
|
for idx, row in enumerate(recent, start=1):
|
||||||
|
lines.append(f"{idx}. {row.get('tool_name')}: {json.dumps(row.get('arguments') or {}, ensure_ascii=False)}")
|
||||||
|
else:
|
||||||
|
lines.append("(无)")
|
||||||
|
lines += [
|
||||||
|
"请审核当前操作:",
|
||||||
|
f"{function_name}: {json.dumps(arguments or {}, ensure_ascii=False)}",
|
||||||
|
"仅判断危险性和越权风险,不判断任务必要性。",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_auto_approval(
|
||||||
|
*,
|
||||||
|
web_terminal: Any,
|
||||||
|
username: str,
|
||||||
|
approval_id: str,
|
||||||
|
conversation_id: Optional[str],
|
||||||
|
recent_tool_actions: List[Dict[str, Any]],
|
||||||
|
function_name: str,
|
||||||
|
arguments: Dict[str, Any],
|
||||||
|
sender,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
def _progress(evt: Dict[str, Any]) -> None:
|
||||||
|
sender("auto_approval_progress", {"approval_id": approval_id, "progress": evt, "conversation_id": conversation_id})
|
||||||
|
|
||||||
|
def _manual_takeover() -> Optional[Dict[str, Any]]:
|
||||||
|
row = tool_approval_manager.get(approval_id)
|
||||||
|
status = (row or {}).get("status")
|
||||||
|
if status in {"approved", "rejected"}:
|
||||||
|
return {"decision": status, "item": row, "source": "manual"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
_progress({"stage": "start", "message": "自动审批开始"})
|
||||||
|
payload = build_auto_approval_payload(
|
||||||
|
web_terminal=web_terminal,
|
||||||
|
recent_tool_actions=recent_tool_actions,
|
||||||
|
function_name=function_name,
|
||||||
|
arguments=arguments,
|
||||||
|
)
|
||||||
|
agent = ApprovalAgent(web_terminal=web_terminal)
|
||||||
|
result = await agent.review(payload_text=payload, progress_cb=_progress, cancel_check=_manual_takeover)
|
||||||
|
if not _manual_takeover():
|
||||||
|
decided = tool_approval_manager.decide(
|
||||||
|
approval_id=approval_id,
|
||||||
|
username=username,
|
||||||
|
decision=str(result.get("decision") or "rejected"),
|
||||||
|
reason=str(result.get("reason") or "").strip(),
|
||||||
|
decider="approval_agent",
|
||||||
|
)
|
||||||
|
out = {"decision": decided.get("status"), "item": decided, "source": "approval_agent"}
|
||||||
|
else:
|
||||||
|
out = _manual_takeover() or {"decision": "rejected", "reason": "人工接管失败"}
|
||||||
|
_progress({"stage": "done", "message": "自动审批结束", "decision": (out or {}).get("decision")})
|
||||||
|
return out
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ from core.tool_config import TOOL_CATEGORIES
|
|||||||
from config.model_profiles import get_registered_model_keys
|
from config.model_profiles import get_registered_model_keys
|
||||||
|
|
||||||
ALLOWED_RUN_MODES = {"fast", "thinking", "deep"}
|
ALLOWED_RUN_MODES = {"fast", "thinking", "deep"}
|
||||||
ALLOWED_PERMISSION_MODES = {"readonly", "approval", "unrestricted"}
|
ALLOWED_PERMISSION_MODES = {"readonly", "approval", "auto_approval", "unrestricted"}
|
||||||
ALLOWED_THEMES = {"classic", "light", "dark"}
|
ALLOWED_THEMES = {"classic", "light", "dark"}
|
||||||
|
|
||||||
PERSONALIZATION_FILENAME = "personalization.json"
|
PERSONALIZATION_FILENAME = "personalization.json"
|
||||||
|
|||||||
@ -60,7 +60,15 @@ class ToolApprovalManager:
|
|||||||
rows.sort(key=lambda x: x.get("created_at", 0.0))
|
rows.sort(key=lambda x: x.get("created_at", 0.0))
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def decide(self, approval_id: str, username: str, decision: str) -> Dict[str, Any]:
|
def decide(
|
||||||
|
self,
|
||||||
|
approval_id: str,
|
||||||
|
username: str,
|
||||||
|
decision: str,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
decider: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
normalized = str(decision or "").strip().lower()
|
normalized = str(decision or "").strip().lower()
|
||||||
if normalized not in {"approved", "rejected"}:
|
if normalized not in {"approved", "rejected"}:
|
||||||
raise ValueError("decision 仅支持 approved / rejected")
|
raise ValueError("decision 仅支持 approved / rejected")
|
||||||
@ -75,5 +83,8 @@ class ToolApprovalManager:
|
|||||||
item["status"] = normalized
|
item["status"] = normalized
|
||||||
item["decision"] = normalized
|
item["decision"] = normalized
|
||||||
item["decided_at"] = time.time()
|
item["decided_at"] = time.time()
|
||||||
|
if isinstance(reason, str) and reason.strip():
|
||||||
|
item["reason"] = reason.strip()
|
||||||
|
if isinstance(decider, str) and decider.strip():
|
||||||
|
item["decider"] = decider.strip()
|
||||||
return dict(item)
|
return dict(item)
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,7 @@ UPLOAD_FOLDER_NAME = "user_upload"
|
|||||||
|
|
||||||
chat_bp = Blueprint('chat', __name__)
|
chat_bp = Blueprint('chat', __name__)
|
||||||
|
|
||||||
PERMISSION_MODE_OPTIONS = ["readonly", "approval", "unrestricted"]
|
PERMISSION_MODE_OPTIONS = ["readonly", "approval", "auto_approval", "unrestricted"]
|
||||||
EXECUTION_MODE_OPTIONS = ["sandbox", "direct"]
|
EXECUTION_MODE_OPTIONS = ["sandbox", "direct"]
|
||||||
|
|
||||||
@chat_bp.route('/api/thinking-mode', methods=['POST'])
|
@chat_bp.route('/api/thinking-mode', methods=['POST'])
|
||||||
@ -615,7 +615,7 @@ def update_permission_mode(terminal: WebTerminal, workspace: UserWorkspace, user
|
|||||||
if target_mode not in PERMISSION_MODE_OPTIONS:
|
if target_mode not in PERMISSION_MODE_OPTIONS:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "无效权限模式,仅支持 readonly / approval / unrestricted"
|
"error": "无效权限模式,仅支持 readonly / approval / auto_approval / unrestricted"
|
||||||
}), 400
|
}), 400
|
||||||
try:
|
try:
|
||||||
applied_mode = terminal.set_permission_mode(target_mode, persist=True)
|
applied_mode = terminal.set_permission_mode(target_mode, persist=True)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from utils.tool_result_formatter import (
|
|||||||
from utils.context_manager import AUTO_SHALLOW_PLACEHOLDER
|
from utils.context_manager import AUTO_SHALLOW_PLACEHOLDER
|
||||||
from config import TOOL_CALL_COOLDOWN
|
from config import TOOL_CALL_COOLDOWN
|
||||||
from modules.personalization_manager import load_personalization_config, resolve_context_compression_settings
|
from modules.personalization_manager import load_personalization_config, resolve_context_compression_settings
|
||||||
|
from modules.auto_approval_service import run_auto_approval
|
||||||
from .deep_compression import run_deep_compression
|
from .deep_compression import run_deep_compression
|
||||||
|
|
||||||
|
|
||||||
@ -177,6 +178,11 @@ def _is_permission_denied_result(result_data: Dict[str, Any]) -> bool:
|
|||||||
return any(marker in joined for marker in markers)
|
return any(marker in joined for marker in markers)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_rejected_tool_text(reason: str) -> str:
|
||||||
|
clean_reason = str(reason or "").strip() or "未提供"
|
||||||
|
return f"工具调用被拒绝\n原因:{clean_reason}"
|
||||||
|
|
||||||
|
|
||||||
async def _wait_for_tool_approval(*, approval_id: str, username: str, timeout_seconds: float = 3600.0) -> Dict[str, Any]:
|
async def _wait_for_tool_approval(*, approval_id: str, username: str, timeout_seconds: float = 3600.0) -> Dict[str, Any]:
|
||||||
started = time.time()
|
started = time.time()
|
||||||
while True:
|
while True:
|
||||||
@ -193,6 +199,7 @@ async def _wait_for_tool_approval(*, approval_id: str, username: str, timeout_se
|
|||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, process_background_command_updates, maybe_mark_failure_from_message, mark_force_thinking, get_stop_flag, clear_stop_flag, workspace=None):
|
async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, process_background_command_updates, maybe_mark_failure_from_message, mark_force_thinking, get_stop_flag, clear_stop_flag, workspace=None):
|
||||||
previous_tool_loop_active = getattr(web_terminal, "_tool_loop_active", False)
|
previous_tool_loop_active = getattr(web_terminal, "_tool_loop_active", False)
|
||||||
web_terminal._tool_loop_active = True
|
web_terminal._tool_loop_active = True
|
||||||
@ -205,6 +212,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
allowed_tool_names.add(name)
|
allowed_tool_names.add(name)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
debug_log(f"构建工具白名单失败(降级继续): {exc}")
|
debug_log(f"构建工具白名单失败(降级继续): {exc}")
|
||||||
|
recent_tool_actions = list(getattr(web_terminal, "_recent_tool_actions", []) or [])
|
||||||
|
|
||||||
# 执行每个工具
|
# 执行每个工具
|
||||||
for tool_call in tool_calls:
|
for tool_call in tool_calls:
|
||||||
@ -409,6 +417,13 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
debug_log(f"执行工具: {function_name} (ID: {tool_call_id})")
|
debug_log(f"执行工具: {function_name} (ID: {tool_call_id})")
|
||||||
|
previous_tool_actions = list(recent_tool_actions)
|
||||||
|
recent_tool_actions.append({
|
||||||
|
"tool_name": function_name,
|
||||||
|
"arguments": arguments,
|
||||||
|
})
|
||||||
|
recent_tool_actions = recent_tool_actions[-3:]
|
||||||
|
setattr(web_terminal, "_recent_tool_actions", list(recent_tool_actions))
|
||||||
|
|
||||||
permission_eval = web_terminal.evaluate_tool_permission(function_name, arguments)
|
permission_eval = web_terminal.evaluate_tool_permission(function_name, arguments)
|
||||||
if not permission_eval.get("allowed", True):
|
if not permission_eval.get("allowed", True):
|
||||||
@ -470,19 +485,38 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
'message': '等待用户审批',
|
'message': '等待用户审批',
|
||||||
'conversation_id': conversation_id
|
'conversation_id': conversation_id
|
||||||
})
|
})
|
||||||
wait_result = await _wait_for_tool_approval(
|
wait_result = None
|
||||||
approval_id=approval_item.get("approval_id"),
|
permission_mode = str(permission_eval.get("mode") or "")
|
||||||
username=username,
|
if permission_mode == "auto_approval":
|
||||||
)
|
approval_id = approval_item.get("approval_id")
|
||||||
|
wait_result = await run_auto_approval(
|
||||||
|
web_terminal=web_terminal,
|
||||||
|
username=username,
|
||||||
|
approval_id=approval_id,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
recent_tool_actions=previous_tool_actions,
|
||||||
|
function_name=function_name,
|
||||||
|
arguments=arguments,
|
||||||
|
sender=sender,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
wait_result = await _wait_for_tool_approval(
|
||||||
|
approval_id=approval_item.get("approval_id"),
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
sender('tool_approval_resolved', {
|
sender('tool_approval_resolved', {
|
||||||
'approval_id': approval_item.get("approval_id"),
|
'approval_id': approval_item.get("approval_id"),
|
||||||
'decision': wait_result.get("decision"),
|
'decision': wait_result.get("decision"),
|
||||||
|
'reason': ((wait_result.get("item") or {}).get("reason") or wait_result.get("reason")),
|
||||||
'conversation_id': conversation_id,
|
'conversation_id': conversation_id,
|
||||||
})
|
})
|
||||||
if wait_result.get("decision") != "approved":
|
if wait_result.get("decision") != "approved":
|
||||||
reject_message = "操作被用户拒绝"
|
reject_message = "操作被用户拒绝"
|
||||||
if wait_result.get("reason") == "审批超时":
|
if wait_result.get("reason") == "审批超时":
|
||||||
reject_message = "审批超时,操作未执行"
|
reject_message = "审批超时,操作未执行"
|
||||||
|
reason = ((wait_result.get("item") or {}).get("reason") or wait_result.get("reason") or "").strip()
|
||||||
|
if reason:
|
||||||
|
reject_message = f"{reject_message}:{reason}"
|
||||||
reject_payload = {
|
reject_payload = {
|
||||||
"success": False,
|
"success": False,
|
||||||
"status": "rejected",
|
"status": "rejected",
|
||||||
@ -498,7 +532,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
'message': reject_message,
|
'message': reject_message,
|
||||||
'conversation_id': conversation_id
|
'conversation_id': conversation_id
|
||||||
})
|
})
|
||||||
reject_content = json.dumps(reject_payload, ensure_ascii=False)
|
reject_content = _format_rejected_tool_text(reason or reject_message)
|
||||||
web_terminal.context_manager.add_conversation(
|
web_terminal.context_manager.add_conversation(
|
||||||
"tool",
|
"tool",
|
||||||
reject_content,
|
reject_content,
|
||||||
@ -511,13 +545,15 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
"name": function_name,
|
"name": function_name,
|
||||||
"content": reject_content
|
"content": reject_content
|
||||||
})
|
})
|
||||||
web_terminal._tool_loop_active = previous_tool_loop_active
|
if permission_mode != "auto_approval":
|
||||||
return {
|
web_terminal._tool_loop_active = previous_tool_loop_active
|
||||||
"stopped": False,
|
return {
|
||||||
"approval_rejected": True,
|
"stopped": False,
|
||||||
"approval_message": reject_message,
|
"approval_rejected": True,
|
||||||
"last_tool_call_time": last_tool_call_time
|
"approval_message": reject_message,
|
||||||
}
|
"last_tool_call_time": last_tool_call_time
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
# 发送工具开始事件
|
# 发送工具开始事件
|
||||||
tool_display_id = f"tool_{iteration}_{function_name}_{time.time()}"
|
tool_display_id = f"tool_{iteration}_{function_name}_{time.time()}"
|
||||||
@ -653,7 +689,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
# 批准模式下 run_command 采用“先只读执行,触发权限拒绝后再审批并单次可写重试”。
|
# 批准模式下 run_command 采用“先只读执行,触发权限拒绝后再审批并单次可写重试”。
|
||||||
if (
|
if (
|
||||||
function_name == "run_command"
|
function_name == "run_command"
|
||||||
and permission_eval.get("mode") == "approval"
|
and permission_eval.get("mode") in {"approval", "auto_approval"}
|
||||||
and not bool(arguments.get("_approval_write_granted", False))
|
and not bool(arguments.get("_approval_write_granted", False))
|
||||||
and _is_permission_denied_result(result_data)
|
and _is_permission_denied_result(result_data)
|
||||||
):
|
):
|
||||||
@ -683,19 +719,38 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
'message': '检测到写权限受限,等待用户审批',
|
'message': '检测到写权限受限,等待用户审批',
|
||||||
'conversation_id': conversation_id
|
'conversation_id': conversation_id
|
||||||
})
|
})
|
||||||
wait_result = await _wait_for_tool_approval(
|
wait_result = None
|
||||||
approval_id=approval_item.get("approval_id"),
|
permission_mode = str(permission_eval.get("mode") or "")
|
||||||
username=username,
|
if permission_mode == "auto_approval":
|
||||||
)
|
approval_id = approval_item.get("approval_id")
|
||||||
|
wait_result = await run_auto_approval(
|
||||||
|
web_terminal=web_terminal,
|
||||||
|
username=username,
|
||||||
|
approval_id=approval_id,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
recent_tool_actions=previous_tool_actions,
|
||||||
|
function_name=function_name,
|
||||||
|
arguments=arguments,
|
||||||
|
sender=sender,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
wait_result = await _wait_for_tool_approval(
|
||||||
|
approval_id=approval_item.get("approval_id"),
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
sender('tool_approval_resolved', {
|
sender('tool_approval_resolved', {
|
||||||
'approval_id': approval_item.get("approval_id"),
|
'approval_id': approval_item.get("approval_id"),
|
||||||
'decision': wait_result.get("decision"),
|
'decision': wait_result.get("decision"),
|
||||||
|
'reason': ((wait_result.get("item") or {}).get("reason") or wait_result.get("reason")),
|
||||||
'conversation_id': conversation_id,
|
'conversation_id': conversation_id,
|
||||||
})
|
})
|
||||||
if wait_result.get("decision") != "approved":
|
if wait_result.get("decision") != "approved":
|
||||||
reject_message = "操作被用户拒绝"
|
reject_message = "操作被用户拒绝"
|
||||||
if wait_result.get("reason") == "审批超时":
|
if wait_result.get("reason") == "审批超时":
|
||||||
reject_message = "审批超时,操作未执行"
|
reject_message = "审批超时,操作未执行"
|
||||||
|
reason = ((wait_result.get("item") or {}).get("reason") or wait_result.get("reason") or "").strip()
|
||||||
|
if reason:
|
||||||
|
reject_message = f"{reject_message}:{reason}"
|
||||||
reject_payload = {
|
reject_payload = {
|
||||||
"success": False,
|
"success": False,
|
||||||
"status": "rejected",
|
"status": "rejected",
|
||||||
@ -711,7 +766,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
'message': reject_message,
|
'message': reject_message,
|
||||||
'conversation_id': conversation_id
|
'conversation_id': conversation_id
|
||||||
})
|
})
|
||||||
reject_content = json.dumps(reject_payload, ensure_ascii=False)
|
reject_content = _format_rejected_tool_text(reason or reject_message)
|
||||||
web_terminal.context_manager.add_conversation(
|
web_terminal.context_manager.add_conversation(
|
||||||
"tool",
|
"tool",
|
||||||
reject_content,
|
reject_content,
|
||||||
@ -724,13 +779,15 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
"name": function_name,
|
"name": function_name,
|
||||||
"content": reject_content
|
"content": reject_content
|
||||||
})
|
})
|
||||||
web_terminal._tool_loop_active = previous_tool_loop_active
|
if permission_mode == "approval":
|
||||||
return {
|
web_terminal._tool_loop_active = previous_tool_loop_active
|
||||||
"stopped": False,
|
return {
|
||||||
"approval_rejected": True,
|
"stopped": False,
|
||||||
"approval_message": reject_message,
|
"approval_rejected": True,
|
||||||
"last_tool_call_time": last_tool_call_time
|
"approval_message": reject_message,
|
||||||
}
|
"last_tool_call_time": last_tool_call_time
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
retry_arguments = dict(arguments or {})
|
retry_arguments = dict(arguments or {})
|
||||||
retry_arguments["_approval_write_granted"] = True
|
retry_arguments["_approval_write_granted"] = True
|
||||||
|
|||||||
@ -347,6 +347,8 @@
|
|||||||
:width="rightWidth"
|
:width="rightWidth"
|
||||||
:approvals="pendingToolApprovals"
|
:approvals="pendingToolApprovals"
|
||||||
:deciding-approval-ids="decidingApprovalIds"
|
:deciding-approval-ids="decidingApprovalIds"
|
||||||
|
:auto-approval-feed-lines="autoApprovalFeedLines"
|
||||||
|
:auto-approval-final-message="autoApprovalFinalMessage"
|
||||||
@switch-unrestricted="handleSwitchPermissionToUnrestricted"
|
@switch-unrestricted="handleSwitchPermissionToUnrestricted"
|
||||||
@approve="approveToolApproval"
|
@approve="approveToolApproval"
|
||||||
@reject="rejectToolApproval"
|
@reject="rejectToolApproval"
|
||||||
@ -720,6 +722,8 @@
|
|||||||
:width="Math.min(rightWidth || 420, 460)"
|
:width="Math.min(rightWidth || 420, 460)"
|
||||||
:approvals="pendingToolApprovals"
|
:approvals="pendingToolApprovals"
|
||||||
:deciding-approval-ids="decidingApprovalIds"
|
:deciding-approval-ids="decidingApprovalIds"
|
||||||
|
:auto-approval-feed-lines="autoApprovalFeedLines"
|
||||||
|
:auto-approval-final-message="autoApprovalFinalMessage"
|
||||||
@switch-unrestricted="handleSwitchPermissionToUnrestricted"
|
@switch-unrestricted="handleSwitchPermissionToUnrestricted"
|
||||||
@approve="approveToolApproval"
|
@approve="approveToolApproval"
|
||||||
@reject="rejectToolApproval"
|
@reject="rejectToolApproval"
|
||||||
|
|||||||
@ -515,6 +515,9 @@ export const taskPollingMethods = {
|
|||||||
case 'tool_approval_resolved':
|
case 'tool_approval_resolved':
|
||||||
this.handleToolApprovalResolved(eventData, eventIdx);
|
this.handleToolApprovalResolved(eventData, eventIdx);
|
||||||
break;
|
break;
|
||||||
|
case 'auto_approval_progress':
|
||||||
|
this.handleAutoApprovalProgress(eventData, eventIdx);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'append_payload':
|
case 'append_payload':
|
||||||
this.handleAppendPayload(eventData, eventIdx);
|
this.handleAppendPayload(eventData, eventIdx);
|
||||||
@ -1171,6 +1174,13 @@ export const taskPollingMethods = {
|
|||||||
} else {
|
} else {
|
||||||
this.pendingToolApprovals.push(approval);
|
this.pendingToolApprovals.push(approval);
|
||||||
}
|
}
|
||||||
|
if (this.approvalAutoCloseTimer) {
|
||||||
|
clearTimeout(this.approvalAutoCloseTimer);
|
||||||
|
this.approvalAutoCloseTimer = null;
|
||||||
|
}
|
||||||
|
if ((this.autoApprovalFeedLines || []).length === 0) {
|
||||||
|
this.autoApprovalFinalMessage = '';
|
||||||
|
}
|
||||||
this.rightCollapsed = false;
|
this.rightCollapsed = false;
|
||||||
if (this.rightWidth < this.minPanelWidth) {
|
if (this.rightWidth < this.minPanelWidth) {
|
||||||
this.rightWidth = this.minPanelWidth;
|
this.rightWidth = this.minPanelWidth;
|
||||||
@ -1189,10 +1199,39 @@ export const taskPollingMethods = {
|
|||||||
this.pendingToolApprovals = this.pendingToolApprovals.filter(
|
this.pendingToolApprovals = this.pendingToolApprovals.filter(
|
||||||
(item: any) => item && item.approval_id !== approvalId
|
(item: any) => item && item.approval_id !== approvalId
|
||||||
);
|
);
|
||||||
// 电脑端:审批完成后自动折叠面板
|
const decision = String(data?.decision || '').trim().toLowerCase();
|
||||||
if (!this.pendingToolApprovals.length && !this.isMobileViewport) {
|
const reason = String(data?.reason || '').trim();
|
||||||
this.rightCollapsed = true;
|
if (decision === 'approved' || decision === 'rejected') {
|
||||||
|
const decisionText = decision === 'approved' ? '批准通过' : '拒绝';
|
||||||
|
this.autoApprovalFinalMessage = `${decisionText} 原因:${reason || '未提供'}`;
|
||||||
}
|
}
|
||||||
|
// 电脑端:审批完成后延迟折叠面板(给用户留出查看结果时间)
|
||||||
|
if (!this.pendingToolApprovals.length && !this.isMobileViewport) {
|
||||||
|
if (this.approvalAutoCloseTimer) {
|
||||||
|
clearTimeout(this.approvalAutoCloseTimer);
|
||||||
|
}
|
||||||
|
this.approvalAutoCloseTimer = setTimeout(() => {
|
||||||
|
this.rightCollapsed = true;
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
this.$forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAutoApprovalProgress(data: any) {
|
||||||
|
const progress = data?.progress;
|
||||||
|
if (!progress || typeof progress !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(this.autoApprovalFeedLines)) {
|
||||||
|
this.autoApprovalFeedLines = [];
|
||||||
|
}
|
||||||
|
if (progress.stage === 'start') {
|
||||||
|
this.autoApprovalFeedLines = ['自动审批开始'];
|
||||||
|
this.autoApprovalFinalMessage = '';
|
||||||
|
} else if (progress.stage === 'run_command' && progress.command) {
|
||||||
|
this.autoApprovalFeedLines.push(String(progress.command));
|
||||||
|
}
|
||||||
|
this.autoApprovalFeedLines = this.autoApprovalFeedLines.slice(-20);
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1141,10 +1141,11 @@ export const uiMethods = {
|
|||||||
const target = String(mode || '')
|
const target = String(mode || '')
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
if (!target || target === this.currentPermissionMode) {
|
if (!target) {
|
||||||
this.closePermissionMenu();
|
this.closePermissionMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const previousMode = this.currentPermissionMode;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/permission-mode', {
|
const response = await fetch('/api/permission-mode', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -1157,7 +1158,23 @@ export const uiMethods = {
|
|||||||
if (!response.ok || !payload?.success) {
|
if (!response.ok || !payload?.success) {
|
||||||
throw new Error(payload?.message || payload?.error || '切换权限失败');
|
throw new Error(payload?.message || payload?.error || '切换权限失败');
|
||||||
}
|
}
|
||||||
this.currentPermissionMode = payload.mode || target;
|
const reportedMode = typeof payload?.mode === 'string' ? payload.mode : '';
|
||||||
|
if (!reportedMode) {
|
||||||
|
throw new Error('切换权限失败:服务端未返回最新权限');
|
||||||
|
}
|
||||||
|
this.currentPermissionMode = reportedMode;
|
||||||
|
// 二次确认,避免前端状态与后端实际权限漂移
|
||||||
|
const verifyResponse = await fetch('/api/permission-mode');
|
||||||
|
const verifyPayload = await verifyResponse.json().catch(() => ({}));
|
||||||
|
if (!verifyResponse.ok || !verifyPayload?.success || typeof verifyPayload?.mode !== 'string') {
|
||||||
|
throw new Error('切换权限后校验失败,请重试');
|
||||||
|
}
|
||||||
|
this.currentPermissionMode = verifyPayload.mode;
|
||||||
|
if (verifyPayload.mode !== target) {
|
||||||
|
throw new Error(
|
||||||
|
`权限未生效(期望:${this.getPermissionModeLabel(target)},实际:${this.getPermissionModeLabel(verifyPayload.mode)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '权限已切换',
|
title: '权限已切换',
|
||||||
message: `当前权限:${this.getPermissionModeLabel(this.currentPermissionMode)}`,
|
message: `当前权限:${this.getPermissionModeLabel(this.currentPermissionMode)}`,
|
||||||
@ -1165,6 +1182,7 @@ export const uiMethods = {
|
|||||||
duration: 1800
|
duration: 1800
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.currentPermissionMode = previousMode;
|
||||||
const msg = error instanceof Error ? error.message : String(error || '切换权限失败');
|
const msg = error instanceof Error ? error.message : String(error || '切换权限失败');
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '切换权限失败',
|
title: '切换权限失败',
|
||||||
|
|||||||
@ -143,6 +143,11 @@ export function dataState() {
|
|||||||
label: '批准',
|
label: '批准',
|
||||||
description: '对工作区文件进行修改的工具需用户批准后才会执行'
|
description: '对工作区文件进行修改的工具需用户批准后才会执行'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'auto_approval',
|
||||||
|
label: '自动审核',
|
||||||
|
description: '工作区内写入直通,高风险操作由后台审核智能体自动审批'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'unrestricted',
|
value: 'unrestricted',
|
||||||
label: '无限制',
|
label: '无限制',
|
||||||
@ -163,6 +168,9 @@ export function dataState() {
|
|||||||
],
|
],
|
||||||
pendingToolApprovals: [],
|
pendingToolApprovals: [],
|
||||||
decidingApprovalIds: [],
|
decidingApprovalIds: [],
|
||||||
|
autoApprovalFeedLines: [],
|
||||||
|
autoApprovalFinalMessage: '',
|
||||||
|
approvalAutoCloseTimer: null,
|
||||||
imageEntries: [],
|
imageEntries: [],
|
||||||
imageLoading: false,
|
imageLoading: false,
|
||||||
videoEntries: [],
|
videoEntries: [],
|
||||||
|
|||||||
@ -341,7 +341,7 @@ const props = defineProps<{
|
|||||||
blockTokenPanel?: boolean;
|
blockTokenPanel?: boolean;
|
||||||
blockCompressConversation?: boolean;
|
blockCompressConversation?: boolean;
|
||||||
blockConversationReview?: boolean;
|
blockConversationReview?: boolean;
|
||||||
currentPermissionMode: 'readonly' | 'approval' | 'unrestricted';
|
currentPermissionMode: 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
|
||||||
permissionMenuOpen: boolean;
|
permissionMenuOpen: boolean;
|
||||||
permissionOptions: Array<{ value: string; label: string; description: string }>;
|
permissionOptions: Array<{ value: string; label: string; description: string }>;
|
||||||
executionModeEnabled?: boolean;
|
executionModeEnabled?: boolean;
|
||||||
|
|||||||
@ -75,7 +75,6 @@
|
|||||||
<div><strong>工具:</strong>{{ item.tool_name }}</div>
|
<div><strong>工具:</strong>{{ item.tool_name }}</div>
|
||||||
<div v-if="item.preview?.summary"><strong>说明:</strong>{{ item.preview.summary }}</div>
|
<div v-if="item.preview?.summary"><strong>说明:</strong>{{ item.preview.summary }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="approval-actions">
|
<div class="approval-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -104,6 +103,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="autoApprovalFeedLines.length || autoApprovalFinalMessage" class="auto-approval-block">
|
||||||
|
<div class="auto-approval-block__title">自动审批记录</div>
|
||||||
|
<pre class="auto-approval-block__content">{{ autoApprovalFeedLines.join('\n') }}</pre>
|
||||||
|
<div v-if="autoApprovalFinalMessage" class="auto-approval-block__final">{{ autoApprovalFinalMessage }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
@ -118,6 +122,8 @@ const props = defineProps<{
|
|||||||
width: number;
|
width: number;
|
||||||
approvals: Array<any>;
|
approvals: Array<any>;
|
||||||
decidingApprovalIds?: string[];
|
decidingApprovalIds?: string[];
|
||||||
|
autoApprovalFeedLines?: string[];
|
||||||
|
autoApprovalFinalMessage?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -203,3 +209,34 @@ const getEditNewLines = (item: any): string[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auto-approval-block {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-color, #2a2f3a);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel-bg, rgba(0, 0, 0, 0.12));
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-approval-block__title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-approval-block__content {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-approval-block__final {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1595,7 +1595,7 @@ const personalTabs = computed(() => {
|
|||||||
const activeTab = ref<PersonalTab>('preferences');
|
const activeTab = ref<PersonalTab>('preferences');
|
||||||
|
|
||||||
type RunModeValue = 'fast' | 'thinking' | 'deep' | null;
|
type RunModeValue = 'fast' | 'thinking' | 'deep' | null;
|
||||||
type PermissionModeValue = 'readonly' | 'approval' | 'unrestricted';
|
type PermissionModeValue = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
|
||||||
type CompressionField =
|
type CompressionField =
|
||||||
| 'shallow_compress_trigger_tokens'
|
| 'shallow_compress_trigger_tokens'
|
||||||
| 'shallow_compress_keep_recent_tools'
|
| 'shallow_compress_keep_recent_tools'
|
||||||
@ -1623,6 +1623,7 @@ const permissionModeOptions: Array<{
|
|||||||
}> = [
|
}> = [
|
||||||
{ id: 'readonly', label: '只读', desc: '仅允许读取/检索类工具,修改操作将被拒绝。' },
|
{ id: 'readonly', label: '只读', desc: '仅允许读取/检索类工具,修改操作将被拒绝。' },
|
||||||
{ id: 'approval', label: '批准', desc: '对工作区文件进行修改的工具需人工批准后执行。' },
|
{ id: 'approval', label: '批准', desc: '对工作区文件进行修改的工具需人工批准后执行。' },
|
||||||
|
{ id: 'auto_approval', label: '自动审核', desc: '工作区内写入直通;高风险操作由后台审核智能体自动审批。' },
|
||||||
{ id: 'unrestricted', label: '无限制', desc: '工具按常规流程直接执行。' }
|
{ id: 'unrestricted', label: '无限制', desc: '工具按常规流程直接执行。' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useModelStore } from './model';
|
|||||||
|
|
||||||
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
|
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
|
||||||
type RunMode = 'fast' | 'thinking' | 'deep';
|
type RunMode = 'fast' | 'thinking' | 'deep';
|
||||||
type PermissionMode = 'readonly' | 'approval' | 'unrestricted';
|
type PermissionMode = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
|
||||||
|
|
||||||
interface PersonalForm {
|
interface PersonalForm {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -279,6 +279,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
default_permission_mode:
|
default_permission_mode:
|
||||||
data.default_permission_mode === 'readonly' ||
|
data.default_permission_mode === 'readonly' ||
|
||||||
data.default_permission_mode === 'approval' ||
|
data.default_permission_mode === 'approval' ||
|
||||||
|
data.default_permission_mode === 'auto_approval' ||
|
||||||
data.default_permission_mode === 'unrestricted'
|
data.default_permission_mode === 'unrestricted'
|
||||||
? data.default_permission_mode
|
? data.default_permission_mode
|
||||||
: 'unrestricted',
|
: 'unrestricted',
|
||||||
@ -540,7 +541,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
this.clearFeedback();
|
this.clearFeedback();
|
||||||
},
|
},
|
||||||
setDefaultPermissionMode(mode: PermissionMode) {
|
setDefaultPermissionMode(mode: PermissionMode) {
|
||||||
const allowed: PermissionMode[] = ['readonly', 'approval', 'unrestricted'];
|
const allowed: PermissionMode[] = ['readonly', 'approval', 'auto_approval', 'unrestricted'];
|
||||||
const target = allowed.includes(mode) ? mode : 'unrestricted';
|
const target = allowed.includes(mode) ? mode : 'unrestricted';
|
||||||
this.form = {
|
this.form = {
|
||||||
...this.form,
|
...this.form,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user