From 71b20e6e4a5ca965086d2dca1222142c2d72eb0d Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Wed, 16 Sep 2026 14:38:03 +0800 Subject: [PATCH] fix: use --restore-last-session instead of unsigned Preferences restore_on_startup --- CHANGELOG.md | 4 ++ SKILL.md | 2 +- scripts/service/browser_service.py | 31 ++++++++++---- scripts/service/chrome_profile_prefs.py | 26 ++++++------ tests/test_browser_open.py | 54 ++++++++++++++++++++++++- 5 files changed, 93 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b412e1e..c502288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ 本文件对应技能市场 **「更新日志」** Tab。 发布 CI 会按当前 tag 版本提取匹配的 `##` 小节。 +## 2.0.29 + +- 「打开浏览器」有可恢复会话时改用 `--restore-last-session` 强制恢复标签与 Session Cookie;不再裸写受 Secure Preferences HMAC 保护的 `restore_on_startup`(该写法会被 Chrome 否掉并落到新标签页) + ## 2.0.28 - 「打开浏览器」识别现代 Chrome `Default/Sessions/Session_*`(及 `Tabs_*`),有可恢复会话时不再强制带平台首页 URL,避免冲掉 Session Cookie 登录态 diff --git a/SKILL.md b/SKILL.md index 27aef18..9aa42c1 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 账号与凭据管理 description: "通用账号 & 凭据 & 登录流程管理器。管理网页/RPA 账号、API Key/Token;支持 9 种登录策略(账密自动、扫码、2FA、SSO 等);提供加密本地存储(Fernet);提供 rpa_helpers 库供业务 skill 同进程调用 ensure_logged_in 完成多平台自动登录。涵盖物流、LLM、内容、开发、API 五类平台。" -version: 2.0.28 +version: 2.0.29 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/scripts/service/browser_service.py b/scripts/service/browser_service.py index ae9a335..5ee252b 100644 --- a/scripts/service/browser_service.py +++ b/scripts/service/browser_service.py @@ -87,21 +87,29 @@ HUMAN_CHROME_FLAGS = ( "--disable-search-engine-choice-screen", ) +# 强制恢复上次会话(含 Session Cookie)。不依赖 Preferences.restore_on_startup +#(该项受 Secure Preferences HMAC 保护,裸写会被 Chrome 否掉)。 +RESTORE_LAST_SESSION_FLAG = "--restore-last-session" -def human_chrome_launch_args(profile_dir, url=None): + +def human_chrome_launch_args(profile_dir, url=None, restore_session=False): """chrome.exe 之后的参数(Popen 列表形式,路径不额外加引号)。""" profile_abs = os.path.abspath(profile_dir) args = [f"--user-data-dir={profile_abs}", *HUMAN_CHROME_FLAGS] + if restore_session: + args.append(RESTORE_LAST_SESSION_FLAG) cleaned = (url or "").strip() if cleaned: args.append(cleaned) return args -def human_chrome_shortcut_arguments(profile_dir, url=None): +def human_chrome_shortcut_arguments(profile_dir, url=None, restore_session=False): """Windows .lnk Arguments 字符串(user-data-dir 带引号)。""" profile_abs = os.path.abspath(profile_dir) parts = [f'--user-data-dir="{profile_abs}"', *HUMAN_CHROME_FLAGS] + if restore_session: + parts.append(RESTORE_LAST_SESSION_FLAG) cleaned = (url or "").strip() if cleaned: parts.append(cleaned) @@ -307,7 +315,7 @@ def cmd_open(account_id): ) return - # 独立 Profile 默认不会「继续上次会话」,Session Cookie 关窗即丢;打开前写入 Preferences + # 清理 exit_type 等(不写 MAC 保护的 restore_on_startup);有会话时靠 --restore-last-session prefs_result = ensure_session_restore(profile_abs) log.info("browser_open_prefs %s", prefs_result) @@ -317,21 +325,26 @@ def cmd_open(account_id): finally: conn.close() - # 有可恢复会话时不带 URL;首次/空 Profile 仍打开平台首页便于登录。 - # 须识别现代 Chrome 的 Default/Sessions/Session_*(仅认旧文件名会导致每次都开首页冲掉 Session Cookie)。 + # 有可恢复会话:不带 URL + --restore-last-session(恢复标签与 Session Cookie)。 + # 首次/空 Profile:仍打开平台首页便于登录。 has_session = profile_has_restorable_session(profile_abs) launch_url = None if has_session else url - cmd = [browser_exe, *human_chrome_launch_args(profile_abs, launch_url)] + cmd = [ + browser_exe, + *human_chrome_launch_args( + profile_abs, launch_url, restore_session=has_session + ), + ] log.info( "browser_open_launch exe=%s profile_dir=%s launch_url=%s has_session=%s " - "platform=%s restore_on_startup=%s", + "restore_last_session=%s platform=%s", browser_exe, profile_abs, launch_url or "(restore)", has_session, + has_session, target.get("platform_key"), - prefs_result.get("restore_after"), ) try: @@ -381,6 +394,8 @@ def cmd_open(account_id): "profile_dir": profile_abs, "url": url, "pid": child.pid, + "has_session": has_session, + "restore_last_session": has_session, "session_restore_on_startup": prefs_result.get("restore_after"), }, }, diff --git a/scripts/service/chrome_profile_prefs.py b/scripts/service/chrome_profile_prefs.py index dc32490..4cb3b19 100644 --- a/scripts/service/chrome_profile_prefs.py +++ b/scripts/service/chrome_profile_prefs.py @@ -1,4 +1,9 @@ -"""Chrome/Edge 独立 Profile:启用「继续上次会话」,让 Session Cookie 可跨次打开保留。""" +"""Chrome/Edge 独立 Profile:启动前清理退出标记,便于会话文件可被恢复。 + +注意:``session.restore_on_startup`` 受 Secure Preferences HMAC 保护, +裸写 Preferences 会被 Chrome 校验否掉并回落到新标签页。真正强制恢复会话 +应在启动参数使用 ``--restore-last-session``(见 browser_service)。 +""" from __future__ import annotations @@ -8,8 +13,6 @@ from typing import Any, Dict from util.logging_config import get_skill_logger -RESTORE_CONTINUE_WHERE_LEFT_OFF = 1 - # Chromium ≤ roughly mid-era: session files lived under Default/ _LEGACY_SESSION_FILES = ( "Current Session", @@ -55,13 +58,10 @@ def local_state_path(profile_dir: str) -> str: def _patch_preferences_dict(data: Dict[str, Any]) -> Dict[str, Any]: - before = int((data.get("session") or {}).get("restore_on_startup") or 0) + """只改不受 Secure Preferences MAC 保护的退出标记;不动 restore_on_startup。""" + restore_before = int((data.get("session") or {}).get("restore_on_startup") or 0) exit_before = str((data.get("profile") or {}).get("exit_type") or "") - session = data.setdefault("session", {}) - session["restore_on_startup"] = RESTORE_CONTINUE_WHERE_LEFT_OFF - session.pop("startup_urls", None) - profile = data.setdefault("profile", {}) if str(profile.get("exit_type") or "").lower() == "crashed": profile["exit_type"] = "Normal" @@ -71,7 +71,7 @@ def _patch_preferences_dict(data: Dict[str, Any]) -> Dict[str, Any]: browser["has_seen_welcome_page"] = True browser["check_default_browser"] = False - return {"restore_before": before, "exit_type_before": exit_before} + return {"restore_before": restore_before, "exit_type_before": exit_before} def _patch_local_state(profile_dir: str, log) -> Dict[str, Any]: @@ -102,7 +102,7 @@ def _patch_local_state(profile_dir: str, log) -> Dict[str, Any]: def ensure_session_restore(profile_dir: str) -> Dict[str, Any]: - """启动浏览器前写入 session.restore_on_startup=1;清 startup_urls;Local State 干净退出。""" + """启动前清理 exit_type / exited_cleanly(不受 MAC 保护);不写 restore_on_startup。""" log = get_skill_logger() profile_abs = os.path.abspath(profile_dir) prefs = preferences_path(profile_abs) @@ -140,12 +140,11 @@ def ensure_session_restore(profile_dir: str) -> Dict[str, Any]: exit_after = str((data.get("profile") or {}).get("exit_type") or "") local = _patch_local_state(profile_abs, log) log.info( - "chrome_session_restore_ensured profile_dir=%s prefs=%s existed=%s " - "restore_on_startup %s->%s exit_type %s->%s local_state=%s", + "chrome_session_restore_prepared profile_dir=%s prefs=%s existed=%s " + "restore_on_startup_unchanged=%s exit_type %s->%s local_state=%s", profile_abs, prefs, existed, - before, after, exit_before or "-", exit_after or "-", @@ -160,4 +159,5 @@ def ensure_session_restore(profile_dir: str) -> Dict[str, Any]: "exit_type_before": exit_before, "exit_type_after": exit_after, "local_state": local, + "prefs_restore_on_startup_touched": False, } diff --git a/tests/test_browser_open.py b/tests/test_browser_open.py index 569ebed..7d6ee4a 100644 --- a/tests/test_browser_open.py +++ b/tests/test_browser_open.py @@ -18,6 +18,37 @@ class TestPlaywrightLaunchHelpers: assert "--start-maximized" in args +class TestEnsureSessionRestoreDoesNotTouchProtectedPref: + def test_leaves_restore_on_startup_unchanged(self, tmp_path, fake_env): + from pathlib import Path + + from service.chrome_profile_prefs import ensure_session_restore, preferences_path + from util.logging_config import setup_skill_logging + + setup_skill_logging("account-manager", "openclaw.skill.account_manager_test") + + profile = tmp_path / "prof" + prefs = Path(preferences_path(str(profile))) + prefs.parent.mkdir(parents=True, exist_ok=True) + prefs.write_text( + json.dumps( + { + "session": {"restore_on_startup": 5}, + "profile": {"exit_type": "Crashed", "exited_cleanly": False}, + } + ), + encoding="utf-8", + ) + + result = ensure_session_restore(str(profile)) + assert result["ok"] is True + assert result["prefs_restore_on_startup_touched"] is False + data = json.loads(prefs.read_text(encoding="utf-8")) + assert data["session"]["restore_on_startup"] == 5 + assert data["profile"]["exit_type"] == "Normal" + assert data["profile"]["exited_cleanly"] is True + + class TestProfileHasRestorableSession: def test_empty_profile_false(self, tmp_path): from service.chrome_profile_prefs import profile_has_restorable_session @@ -130,7 +161,7 @@ class TestCmdOpenDirectBrowser: def test_cmd_open_skips_url_when_modern_sessions_exist( self, clean_db, tmp_path, capsys, monkeypatch ): - """Modern Chrome Sessions/ must restore without forcing homepage URL.""" + """Modern Chrome Sessions/ must restore via --restore-last-session, no homepage URL.""" from service import browser_service profile = tmp_path / "prof" @@ -145,8 +176,11 @@ class TestCmdOpenDirectBrowser: payload = json.loads(out.splitlines()[-1]) assert payload["success"] is True + assert payload["data"]["has_session"] is True + assert payload["data"]["restore_last_session"] is True assert "https://example.com" not in spawned["cmd"] - assert spawned["cmd"][-1].startswith("--") + assert "--restore-last-session" in spawned["cmd"] + assert spawned["cmd"][-1] == "--restore-last-session" or spawned["cmd"][-1].startswith("--") class TestHumanChromeLaunchArgs: @@ -158,9 +192,17 @@ class TestHumanChromeLaunchArgs: assert args[0].startswith("--user-data-dir=") for flag in HUMAN_CHROME_FLAGS: assert flag in args + assert "--restore-last-session" not in args assert args[-1] == "https://chat.deepseek.com" assert "--enable-automation" not in args + def test_restore_session_flag_without_url(self, tmp_path): + from service.browser_service import human_chrome_launch_args + + args = human_chrome_launch_args(str(tmp_path / "prof"), None, restore_session=True) + assert "--restore-last-session" in args + assert not any(a.startswith("http") for a in args) + def test_shortcut_arguments_quote_profile_and_include_url(self, tmp_path): from service.browser_service import human_chrome_shortcut_arguments @@ -170,8 +212,16 @@ class TestHumanChromeLaunchArgs: assert '--user-data-dir="' in text assert "--start-maximized" in text assert "--no-first-run" in text + assert "--restore-last-session" not in text assert text.endswith("https://example.com") + def test_shortcut_restore_session(self, tmp_path): + from service.browser_service import human_chrome_shortcut_arguments + + text = human_chrome_shortcut_arguments(str(tmp_path / "p"), None, restore_session=True) + assert "--restore-last-session" in text + assert "http" not in text + class TestResolveChromiumChannel: """resolve_chromium_channel returns a channel string when browsers exist."""