feat: 危险命令确认改为会话级——保留单次确认,新增「全部确认」选项(approve_all)授权后本会话不再询问

This commit is contained in:
ymq 2026-08-19 14:09:27 +08:00
parent bda9f31a1b
commit 41232c8295
2 changed files with 71 additions and 6 deletions

View File

@ -64,6 +64,7 @@ class AgentExecutor:
role: str = "", # 产线内角色(可选,如 develop/design;驾驶舱 agent 为空) role: str = "", # 产线内角色(可选,如 develop/design;驾驶舱 agent 为空)
base_url: str = "", # 请求 base_url(scheme://host/),供 slash 命令生成 widget 绝对 URL base_url: str = "", # 请求 base_url(scheme://host/),供 slash 命令生成 widget 绝对 URL
generic: bool = False, # True = 纯通用会话(不解析项目、不挂产线能力) generic: bool = False, # True = 纯通用会话(不解析项目、不挂产线能力)
session=None, # GatewaySession(会话级状态:approve_all/pending_confirm)
): ):
self.config = config self.config = config
self.project_id = project_id self.project_id = project_id
@ -72,6 +73,7 @@ class AgentExecutor:
self.pipeline_id = "" # loaded from project context(产线能力包 key) self.pipeline_id = "" # loaded from project context(产线能力包 key)
self.role = role # 产线内角色(技能/工具/prompt 按角色加载) self.role = role # 产线内角色(技能/工具/prompt 按角色加载)
self.generic = generic # 是否纯通用会话 self.generic = generic # 是否纯通用会话
self._session = session # GatewaySession(approve_all/pending_confirm 会话级状态)
self.space = GENERAL_SPACE # 项目空间键:generic→'general',产线→真实 pipeline_id self.space = GENERAL_SPACE # 项目空间键:generic→'general',产线→真实 pipeline_id
self.workspace_dir = workspace_dir or WORKSPACE_BASE self.workspace_dir = workspace_dir or WORKSPACE_BASE
self.model_name = model_name or config.model_name self.model_name = model_name or config.model_name
@ -146,6 +148,39 @@ class AgentExecutor:
self._msgs.extend(hist_msgs) self._msgs.extend(hist_msgs)
self._msgs.append({"role": "user", "content": user_input}) self._msgs.append({"role": "user", "content": user_input})
# Step 4.5: 危险命令确认回复短路(pending_confirm + 确认/全部确认/取消)
confirm_decision = self._detect_confirm_decision(user_input)
if confirm_decision and self._session and self._session.pending_confirm:
pending = self._session.pending_confirm
if confirm_decision == "cancel":
self._session.pending_confirm = None
yield json.dumps({"type": "reply", "message": "已取消该操作。"}, ensure_ascii=False) + "\n"
await self._save_turn(user_input, "已取消该操作。")
return
if confirm_decision == "approve_all":
self._session.approve_all = True
self._session.pending_confirm = None
pending_tool = pending.get("tool", "")
pending_params = pending.get("params") or {}
# 回填 assistant tool_call → 执行 → 回填 tool result,随后继续 Tool Loop
fake_id = f"call_{int(time.time() * 1000)}"
self._msgs.append({
"role": "assistant",
"content": None,
"tool_calls": [{
"id": fake_id,
"type": "function",
"function": {"name": pending_tool, "arguments": json.dumps(pending_params, ensure_ascii=False)},
}],
})
yield json.dumps({"type": "tool_call", "tool": pending_tool, "params": pending_params}, ensure_ascii=False) + "\n"
result = await self._execute_tool(pending_tool, pending_params)
if not result.startswith("未知工具") and not result.startswith("ERROR"):
self._tool_call_count += 1
yield json.dumps({"type": "tool_result", "tool": pending_tool, "result": result[:500]}, ensure_ascii=False) + "\n"
self._msgs.append({"role": "tool", "tool_call_id": fake_id, "content": str(result)})
# 不 return,继续进入 Tool Loop,让 LLM 基于工具结果继续后续步骤
# Step 5: Tool Loop # Step 5: Tool Loop
yield json.dumps({"type": "progress", "message": "思考中..."}, ensure_ascii=False) + "\n" yield json.dumps({"type": "progress", "message": "思考中..."}, ensure_ascii=False) + "\n"
@ -179,8 +214,10 @@ class AgentExecutor:
except Exception: except Exception:
tool_params = {} tool_params = {}
# 需要确认的工具 # 需要确认的工具:记录待确认并暂停,等待用户确认/全部确认/取消
if self._needs_confirmation(tool_name, tool_params): if self._needs_confirmation(tool_name, tool_params):
if self._session:
self._session.pending_confirm = {"tool": tool_name, "params": tool_params}
yield json.dumps({ yield json.dumps({
"type": "confirm", "tool": tool_name, "params": tool_params, "type": "confirm", "tool": tool_name, "params": tool_params,
}, ensure_ascii=False) + "\n" }, ensure_ascii=False) + "\n"
@ -239,8 +276,10 @@ class AgentExecutor:
tool_name = act.get("tool", "") tool_name = act.get("tool", "")
tool_params = act.get("params", {}) tool_params = act.get("params", {})
# 需要确认的工具 # 需要确认的工具:记录待确认并暂停,等待用户确认/全部确认/取消
if self._needs_confirmation(tool_name, tool_params): if self._needs_confirmation(tool_name, tool_params):
if self._session:
self._session.pending_confirm = {"tool": tool_name, "params": tool_params}
yield json.dumps({ yield json.dumps({
"type": "confirm", "type": "confirm",
"tool": tool_name, "tool": tool_name,
@ -1268,13 +1307,37 @@ class AgentExecutor:
) )
return any(d in c for d in dangerous) return any(d in c for d in dangerous)
def _detect_confirm_decision(self, user_input: str) -> str:
"""检测用户对危险命令确认的回复。返回 approve / approve_all / cancel / ''。"""
t = (user_input or "").strip().lower()
if not t:
return ""
for kw in ("全部确认", "确认全部", "全部同意", "都确认", "都同意", "一律确认", "approve all", "yes to all", "always"):
if kw in t:
return "approve_all"
for kw in ("取消", "放弃", "不执行", "别执行", "cancel", "abort", "不要"):
if kw in t:
return "cancel"
for kw in ("确认", "同意", "执行", "可以", "approve", "confirm", "yes", "ok", "继续"):
if kw in t:
return "approve"
return ""
def _needs_confirmation(self, tool_name: str, params: dict = None) -> bool: def _needs_confirmation(self, tool_name: str, params: dict = None) -> bool:
"""检查工具是否需要用户确认。 """检查工具是否需要用户确认。
已关闭确认机制:agent 会话应能理解和接受全部确认,执行任务时 会话级「全部确认」(approve_all)时直接放行;否则 run_command 只对
不再打断用户要求确认(危险命令也自动执行,不再弹「请回复确认」)。 危险命令确认,普通命令自动执行,其他 requires_confirmation 工具一律确认。
如需恢复按命令危险度确认,改回读取 tool.requires_confirmation + _is_dangerous_command。
""" """
if self._session and getattr(self._session, "approve_all", False):
return False
if self._tool_registry:
tool = self._tool_registry.get(tool_name)
if tool and tool.requires_confirmation:
if tool_name == "run_command":
cmd = (params or {}).get("command", "")
return self._is_dangerous_command(cmd)
return True
return False return False

View File

@ -36,6 +36,8 @@ class GatewaySession:
project_name: str = "" project_name: str = ""
created_at: float = 0.0 created_at: float = 0.0
updated_at: float = 0.0 updated_at: float = 0.0
approve_all: bool = False # 会话级"全部确认":用户评估后授权,本会话不再询问危险命令
pending_confirm: Optional[dict] = None # 待确认工具 {"tool":..., "params":...}(危险命令等待用户确认)
class Gateway: class Gateway:
@ -155,7 +157,7 @@ class Gateway:
config.model_name = ctx["default_llm_name"] config.model_name = ctx["default_llm_name"]
executor = AgentExecutor( executor = AgentExecutor(
config=config, project_id=ctx["pid"], user_id=user_id, role=role, config=config, project_id=ctx["pid"], user_id=user_id, role=role,
base_url=base_url, generic=generic) base_url=base_url, generic=generic, session=sess)
# 4. 转发标准化事件流 # 4. 转发标准化事件流
async for chunk in executor.run(content, history=history): async for chunk in executor.run(content, history=history):