diff --git a/app/agent/tools/impl/browse_webpage.py b/app/agent/tools/impl/browse_webpage.py index 2c7f690f2..5759f81e4 100644 --- a/app/agent/tools/impl/browse_webpage.py +++ b/app/agent/tools/impl/browse_webpage.py @@ -109,7 +109,7 @@ class BrowseWebpageInput(BaseModel): description="Content type for 'get_content' action: 'text' for readable text, 'html' for raw HTML", ) timeout: Optional[int] = Field( - DEFAULT_TIMEOUT, ge=1, le=MAX_TIMEOUT, + DEFAULT_TIMEOUT, description="Timeout in seconds for the action (default: 30, range: 1-300)" ) cookies: Optional[str] = Field( @@ -416,6 +416,13 @@ class BrowseWebpageTool(MoviePilotTool): except Exception as e: error_summary = summarize_error(e) logger.error(f"CloakBrowser 执行失败: {error_summary}", exc_info=True) + if str(e) == "关闭浏览器标签页失败": + return self._json_response({ + "success": False, + "execution_outcome": "unknown", + "error": "关闭浏览器标签页的实际状态未知。", + "recovery": "先使用 list_tabs 或 snapshot 核验当前页面,再决定是否继续操作;不要直接重试关闭动作。", + }) if browser_action == BrowserAction.SCREENSHOT: return self._screenshot_failure("screenshot_failed", "浏览器截图执行失败") return self._error_response("browser_operation_failed", f"CloakBrowser 执行失败: {error_summary}", "检查当前会话和页面状态后再重试。") diff --git a/docs/refactor/agent-codex-parity-plan.md b/docs/refactor/agent-codex-parity-plan.md index 6806b228d..a2d196373 100644 --- a/docs/refactor/agent-codex-parity-plan.md +++ b/docs/refactor/agent-codex-parity-plan.md @@ -115,4 +115,5 @@ uv run --locked --no-sync python -m scripts.evaluation \ - 浏览器与子代理状态阶段:浏览器所有成功/失败 JSON 统一带 `execution_outcome`;子代理任务的 running/cancelled 状态带明确的 `pending/failed` 语义和继续观察、取消后重新派发的恢复建议。当前阶段与文件、Shell、浏览器回归共 112 项通过;真实浏览器仍受用户保存权限限制,子代理终端共享仍需原生句柄闭环。 - 浏览器观察回执阶段:`goto`、`snapshot`、`get_content` 及交互动作统一显式返回 `success`,从而稳定映射为 `execution_outcome=succeeded/failed`;页面内容为空与浏览器执行失败可以区分。当前跨浏览器、文件、Shell、子代理回归共 125 项通过。 - 浏览器会话可靠性阶段:直接 `run()` 的参数/异常现在返回结构化恢复合同并限制 timeout 为 1–300 秒;点击等待遵守调用 deadline;关闭标签页正确调整 active index,关闭异常不再伪报成功;生成新 snapshot 前清理旧 ref,动态页面不会把过期 ref 静默指向新元素。浏览器、文件、Shell、子代理相关回归共 155 项通过。 +- 浏览器状态不确定性补强:关闭标签页的 provider 异常按 `execution_outcome=unknown` 返回,要求先 `list_tabs/snapshot` 核验,避免把可能已发生的动作当作失败后重复执行;补充了 active index 和关闭异常的真实回归。 - 浏览器环境复核:启动本机 Chrome 后用同一官方 OAuth 配置重跑 `/tmp/moviepilot-agent-round-luna-oauth-native-browser-chrome-20260912.json`,原生已加载 Browser plugin、node_repl 并选择 Chrome;真实回环导航仍被保存的浏览器权限拒绝。该结果排除了“CLI 未登录或未启动 Chrome”的解释,下一次只有在用户允许对应回环地址后才继续导航、点击和 oracle 配对。 diff --git a/tests/test_browser_helper.py b/tests/test_browser_helper.py index 207414a70..bb0056ffc 100644 --- a/tests/test_browser_helper.py +++ b/tests/test_browser_helper.py @@ -550,6 +550,29 @@ def test_close_tab_before_active_page_keeps_active_index() -> None: assert first_page.closed is True +def test_close_tab_failure_is_reported_as_unknown_state() -> None: + """关闭动作异常时实际状态可能已发生,必须要求先核验而非盲目重试。""" + page = _FakePage("close-error") + page.close = MagicMock(side_effect=RuntimeError("provider failure")) + context = _FakeContext([page, _FakePage("remaining")]) + session = _BrowserSessionState("close-error", context, context.pages, active_index=0) + tool = BrowseWebpageTool(session_id="session-1", user_id="10001") + + with patch.object( + BrowserSessionHelper, + "with_session", + side_effect=lambda callback, **_kwargs: callback(session), + ): + result = tool._execute_browser_action( + browser_action=BrowserAction.CLOSE_TAB, url=None, selector=None, ref=None, value=None, + script=None, content_type="text", timeout=3, cookies=None, user_agent=None, + session_key="close-error", tab_index=0, allow_private_network=False, + ) + payload = json.loads(result) + assert payload["execution_outcome"] == "unknown" + assert "list_tabs" in payload["recovery"] + + def test_browse_webpage_get_cookies_returns_current_domain_cookie_and_ua(): """管理员 Cookie 动作应只返回当前页面域名的会话字段。""" page = _FakePage()