From 41232c8295f001fb40f25ee5376f4ccf72963cec Mon Sep 17 00:00:00 2001 From: ymq Date: Wed, 19 Aug 2026 14:09:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=B1=E9=99=A9=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E6=94=B9=E4=B8=BA=E4=BC=9A=E8=AF=9D=E7=BA=A7?= =?UTF-8?q?=E2=80=94=E2=80=94=E4=BF=9D=E7=95=99=E5=8D=95=E6=AC=A1=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=EF=BC=8C=E6=96=B0=E5=A2=9E=E3=80=8C=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E3=80=8D=E9=80=89=E9=A1=B9(approve=5Fall)?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E5=90=8E=E6=9C=AC=E4=BC=9A=E8=AF=9D=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=AF=A2=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_service/agent_loop_v2.py | 73 ++++++++++++++++++++++++++++--- pipeline_service/gateway.py | 4 +- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/pipeline_service/agent_loop_v2.py b/pipeline_service/agent_loop_v2.py index b277131..32ccce9 100644 --- a/pipeline_service/agent_loop_v2.py +++ b/pipeline_service/agent_loop_v2.py @@ -64,6 +64,7 @@ class AgentExecutor: role: str = "", # 产线内角色(可选,如 develop/design;驾驶舱 agent 为空) base_url: str = "", # 请求 base_url(scheme://host/),供 slash 命令生成 widget 绝对 URL generic: bool = False, # True = 纯通用会话(不解析项目、不挂产线能力) + session=None, # GatewaySession(会话级状态:approve_all/pending_confirm) ): self.config = config self.project_id = project_id @@ -72,6 +73,7 @@ class AgentExecutor: self.pipeline_id = "" # loaded from project context(产线能力包 key) self.role = role # 产线内角色(技能/工具/prompt 按角色加载) self.generic = generic # 是否纯通用会话 + self._session = session # GatewaySession(approve_all/pending_confirm 会话级状态) self.space = GENERAL_SPACE # 项目空间键:generic→'general',产线→真实 pipeline_id self.workspace_dir = workspace_dir or WORKSPACE_BASE self.model_name = model_name or config.model_name @@ -146,6 +148,39 @@ class AgentExecutor: self._msgs.extend(hist_msgs) 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 yield json.dumps({"type": "progress", "message": "思考中..."}, ensure_ascii=False) + "\n" @@ -179,8 +214,10 @@ class AgentExecutor: except Exception: 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({ "type": "confirm", "tool": tool_name, "params": tool_params, }, ensure_ascii=False) + "\n" @@ -239,8 +276,10 @@ class AgentExecutor: tool_name = act.get("tool", "") tool_params = act.get("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({ "type": "confirm", "tool": tool_name, @@ -1268,13 +1307,37 @@ class AgentExecutor: ) 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: """检查工具是否需要用户确认。 - 已关闭确认机制:agent 会话应能理解和接受全部确认,执行任务时 - 不再打断用户要求确认(危险命令也自动执行,不再弹「请回复确认」)。 - 如需恢复按命令危险度确认,改回读取 tool.requires_confirmation + _is_dangerous_command。 + 会话级「全部确认」(approve_all)时直接放行;否则 run_command 只对 + 危险命令确认,普通命令自动执行,其他 requires_confirmation 工具一律确认。 """ + 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 diff --git a/pipeline_service/gateway.py b/pipeline_service/gateway.py index f67289d..ed35a04 100644 --- a/pipeline_service/gateway.py +++ b/pipeline_service/gateway.py @@ -36,6 +36,8 @@ class GatewaySession: project_name: str = "" created_at: float = 0.0 updated_at: float = 0.0 + approve_all: bool = False # 会话级"全部确认":用户评估后授权,本会话不再询问危险命令 + pending_confirm: Optional[dict] = None # 待确认工具 {"tool":..., "params":...}(危险命令等待用户确认) class Gateway: @@ -155,7 +157,7 @@ class Gateway: config.model_name = ctx["default_llm_name"] executor = AgentExecutor( 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. 转发标准化事件流 async for chunk in executor.run(content, history=history):