fix: use --restore-last-session instead of unsigned Preferences restore_on_startup
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
@@ -3,6 +3,10 @@
|
|||||||
本文件对应技能市场 **「更新日志」** Tab。
|
本文件对应技能市场 **「更新日志」** Tab。
|
||||||
发布 CI 会按当前 tag 版本提取匹配的 `##` 小节。
|
发布 CI 会按当前 tag 版本提取匹配的 `##` 小节。
|
||||||
|
|
||||||
|
## 2.0.29
|
||||||
|
|
||||||
|
- 「打开浏览器」有可恢复会话时改用 `--restore-last-session` 强制恢复标签与 Session Cookie;不再裸写受 Secure Preferences HMAC 保护的 `restore_on_startup`(该写法会被 Chrome 否掉并落到新标签页)
|
||||||
|
|
||||||
## 2.0.28
|
## 2.0.28
|
||||||
|
|
||||||
- 「打开浏览器」识别现代 Chrome `Default/Sessions/Session_*`(及 `Tabs_*`),有可恢复会话时不再强制带平台首页 URL,避免冲掉 Session Cookie 登录态
|
- 「打开浏览器」识别现代 Chrome `Default/Sessions/Session_*`(及 `Tabs_*`),有可恢复会话时不再强制带平台首页 URL,避免冲掉 Session Cookie 登录态
|
||||||
|
|||||||
2
SKILL.md
2
SKILL.md
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 账号与凭据管理
|
name: 账号与凭据管理
|
||||||
description: "通用账号 & 凭据 & 登录流程管理器。管理网页/RPA 账号、API Key/Token;支持 9 种登录策略(账密自动、扫码、2FA、SSO 等);提供加密本地存储(Fernet);提供 rpa_helpers 库供业务 skill 同进程调用 ensure_logged_in 完成多平台自动登录。涵盖物流、LLM、内容、开发、API 五类平台。"
|
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: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
|
|||||||
@@ -87,21 +87,29 @@ HUMAN_CHROME_FLAGS = (
|
|||||||
"--disable-search-engine-choice-screen",
|
"--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 列表形式,路径不额外加引号)。"""
|
"""chrome.exe 之后的参数(Popen 列表形式,路径不额外加引号)。"""
|
||||||
profile_abs = os.path.abspath(profile_dir)
|
profile_abs = os.path.abspath(profile_dir)
|
||||||
args = [f"--user-data-dir={profile_abs}", *HUMAN_CHROME_FLAGS]
|
args = [f"--user-data-dir={profile_abs}", *HUMAN_CHROME_FLAGS]
|
||||||
|
if restore_session:
|
||||||
|
args.append(RESTORE_LAST_SESSION_FLAG)
|
||||||
cleaned = (url or "").strip()
|
cleaned = (url or "").strip()
|
||||||
if cleaned:
|
if cleaned:
|
||||||
args.append(cleaned)
|
args.append(cleaned)
|
||||||
return args
|
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 带引号)。"""
|
"""Windows .lnk Arguments 字符串(user-data-dir 带引号)。"""
|
||||||
profile_abs = os.path.abspath(profile_dir)
|
profile_abs = os.path.abspath(profile_dir)
|
||||||
parts = [f'--user-data-dir="{profile_abs}"', *HUMAN_CHROME_FLAGS]
|
parts = [f'--user-data-dir="{profile_abs}"', *HUMAN_CHROME_FLAGS]
|
||||||
|
if restore_session:
|
||||||
|
parts.append(RESTORE_LAST_SESSION_FLAG)
|
||||||
cleaned = (url or "").strip()
|
cleaned = (url or "").strip()
|
||||||
if cleaned:
|
if cleaned:
|
||||||
parts.append(cleaned)
|
parts.append(cleaned)
|
||||||
@@ -307,7 +315,7 @@ def cmd_open(account_id):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 独立 Profile 默认不会「继续上次会话」,Session Cookie 关窗即丢;打开前写入 Preferences
|
# 清理 exit_type 等(不写 MAC 保护的 restore_on_startup);有会话时靠 --restore-last-session
|
||||||
prefs_result = ensure_session_restore(profile_abs)
|
prefs_result = ensure_session_restore(profile_abs)
|
||||||
log.info("browser_open_prefs %s", prefs_result)
|
log.info("browser_open_prefs %s", prefs_result)
|
||||||
|
|
||||||
@@ -317,21 +325,26 @@ def cmd_open(account_id):
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# 有可恢复会话时不带 URL;首次/空 Profile 仍打开平台首页便于登录。
|
# 有可恢复会话:不带 URL + --restore-last-session(恢复标签与 Session Cookie)。
|
||||||
# 须识别现代 Chrome 的 Default/Sessions/Session_*(仅认旧文件名会导致每次都开首页冲掉 Session Cookie)。
|
# 首次/空 Profile:仍打开平台首页便于登录。
|
||||||
has_session = profile_has_restorable_session(profile_abs)
|
has_session = profile_has_restorable_session(profile_abs)
|
||||||
launch_url = None if has_session else url
|
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(
|
log.info(
|
||||||
"browser_open_launch exe=%s profile_dir=%s launch_url=%s has_session=%s "
|
"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,
|
browser_exe,
|
||||||
profile_abs,
|
profile_abs,
|
||||||
launch_url or "(restore)",
|
launch_url or "(restore)",
|
||||||
has_session,
|
has_session,
|
||||||
|
has_session,
|
||||||
target.get("platform_key"),
|
target.get("platform_key"),
|
||||||
prefs_result.get("restore_after"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -381,6 +394,8 @@ def cmd_open(account_id):
|
|||||||
"profile_dir": profile_abs,
|
"profile_dir": profile_abs,
|
||||||
"url": url,
|
"url": url,
|
||||||
"pid": child.pid,
|
"pid": child.pid,
|
||||||
|
"has_session": has_session,
|
||||||
|
"restore_last_session": has_session,
|
||||||
"session_restore_on_startup": prefs_result.get("restore_after"),
|
"session_restore_on_startup": prefs_result.get("restore_after"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -8,8 +13,6 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from util.logging_config import get_skill_logger
|
from util.logging_config import get_skill_logger
|
||||||
|
|
||||||
RESTORE_CONTINUE_WHERE_LEFT_OFF = 1
|
|
||||||
|
|
||||||
# Chromium ≤ roughly mid-era: session files lived under Default/
|
# Chromium ≤ roughly mid-era: session files lived under Default/
|
||||||
_LEGACY_SESSION_FILES = (
|
_LEGACY_SESSION_FILES = (
|
||||||
"Current Session",
|
"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]:
|
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 "")
|
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", {})
|
profile = data.setdefault("profile", {})
|
||||||
if str(profile.get("exit_type") or "").lower() == "crashed":
|
if str(profile.get("exit_type") or "").lower() == "crashed":
|
||||||
profile["exit_type"] = "Normal"
|
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["has_seen_welcome_page"] = True
|
||||||
browser["check_default_browser"] = False
|
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]:
|
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]:
|
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()
|
log = get_skill_logger()
|
||||||
profile_abs = os.path.abspath(profile_dir)
|
profile_abs = os.path.abspath(profile_dir)
|
||||||
prefs = preferences_path(profile_abs)
|
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 "")
|
exit_after = str((data.get("profile") or {}).get("exit_type") or "")
|
||||||
local = _patch_local_state(profile_abs, log)
|
local = _patch_local_state(profile_abs, log)
|
||||||
log.info(
|
log.info(
|
||||||
"chrome_session_restore_ensured profile_dir=%s prefs=%s existed=%s "
|
"chrome_session_restore_prepared profile_dir=%s prefs=%s existed=%s "
|
||||||
"restore_on_startup %s->%s exit_type %s->%s local_state=%s",
|
"restore_on_startup_unchanged=%s exit_type %s->%s local_state=%s",
|
||||||
profile_abs,
|
profile_abs,
|
||||||
prefs,
|
prefs,
|
||||||
existed,
|
existed,
|
||||||
before,
|
|
||||||
after,
|
after,
|
||||||
exit_before or "-",
|
exit_before or "-",
|
||||||
exit_after 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_before": exit_before,
|
||||||
"exit_type_after": exit_after,
|
"exit_type_after": exit_after,
|
||||||
"local_state": local,
|
"local_state": local,
|
||||||
|
"prefs_restore_on_startup_touched": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,37 @@ class TestPlaywrightLaunchHelpers:
|
|||||||
assert "--start-maximized" in args
|
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:
|
class TestProfileHasRestorableSession:
|
||||||
def test_empty_profile_false(self, tmp_path):
|
def test_empty_profile_false(self, tmp_path):
|
||||||
from service.chrome_profile_prefs import profile_has_restorable_session
|
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(
|
def test_cmd_open_skips_url_when_modern_sessions_exist(
|
||||||
self, clean_db, tmp_path, capsys, monkeypatch
|
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
|
from service import browser_service
|
||||||
|
|
||||||
profile = tmp_path / "prof"
|
profile = tmp_path / "prof"
|
||||||
@@ -145,8 +176,11 @@ class TestCmdOpenDirectBrowser:
|
|||||||
payload = json.loads(out.splitlines()[-1])
|
payload = json.loads(out.splitlines()[-1])
|
||||||
|
|
||||||
assert payload["success"] is True
|
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 "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:
|
class TestHumanChromeLaunchArgs:
|
||||||
@@ -158,9 +192,17 @@ class TestHumanChromeLaunchArgs:
|
|||||||
assert args[0].startswith("--user-data-dir=")
|
assert args[0].startswith("--user-data-dir=")
|
||||||
for flag in HUMAN_CHROME_FLAGS:
|
for flag in HUMAN_CHROME_FLAGS:
|
||||||
assert flag in args
|
assert flag in args
|
||||||
|
assert "--restore-last-session" not in args
|
||||||
assert args[-1] == "https://chat.deepseek.com"
|
assert args[-1] == "https://chat.deepseek.com"
|
||||||
assert "--enable-automation" not in args
|
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):
|
def test_shortcut_arguments_quote_profile_and_include_url(self, tmp_path):
|
||||||
from service.browser_service import human_chrome_shortcut_arguments
|
from service.browser_service import human_chrome_shortcut_arguments
|
||||||
|
|
||||||
@@ -170,8 +212,16 @@ class TestHumanChromeLaunchArgs:
|
|||||||
assert '--user-data-dir="' in text
|
assert '--user-data-dir="' in text
|
||||||
assert "--start-maximized" in text
|
assert "--start-maximized" in text
|
||||||
assert "--no-first-run" in text
|
assert "--no-first-run" in text
|
||||||
|
assert "--restore-last-session" not in text
|
||||||
assert text.endswith("https://example.com")
|
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:
|
class TestResolveChromiumChannel:
|
||||||
"""resolve_chromium_channel returns a channel string when browsers exist."""
|
"""resolve_chromium_channel returns a channel string when browsers exist."""
|
||||||
|
|||||||
Reference in New Issue
Block a user