feat: open browser with stable CDP port for scrape attach
All checks were successful
技能自动化发布 / release (push) Successful in 11s

This commit is contained in:
2026-09-16 15:40:50 +08:00
parent 71b20e6e4a
commit 1ba7a0f4a1
4 changed files with 44 additions and 5 deletions

View File

@@ -3,6 +3,10 @@
本文件对应技能市场 **「更新日志」** Tab。 本文件对应技能市场 **「更新日志」** Tab。
发布 CI 会按当前 tag 版本提取匹配的 `##` 小节。 发布 CI 会按当前 tag 版本提取匹配的 `##` 小节。
## 2.0.30
- 「打开浏览器」为本账号 Profile 附加稳定本机调试口(与科力普等采集技能约定同一端口算法),便于采集挂接已打开的原生窗口;仍使用 `--restore-last-session` 恢复登录态
## 2.0.29 ## 2.0.29
- 「打开浏览器」有可恢复会话时改用 `--restore-last-session` 强制恢复标签与 Session Cookie;不再裸写受 Secure Preferences HMAC 保护的 `restore_on_startup`(该写法会被 Chrome 否掉并落到新标签页) - 「打开浏览器」有可恢复会话时改用 `--restore-last-session` 强制恢复标签与 Session Cookie;不再裸写受 Secure Preferences HMAC 保护的 `restore_on_startup`(该写法会被 Chrome 否掉并落到新标签页)

View File

@@ -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.29 version: 2.0.30
author: 深圳匠厂科技有限公司 author: 深圳匠厂科技有限公司
metadata: metadata:
openclaw: openclaw:

View File

@@ -91,25 +91,46 @@ HUMAN_CHROME_FLAGS = (
#(该项受 Secure Preferences HMAC 保护,裸写会被 Chrome 否掉)。 #(该项受 Secure Preferences HMAC 保护,裸写会被 Chrome 否掉)。
RESTORE_LAST_SESSION_FLAG = "--restore-last-session" RESTORE_LAST_SESSION_FLAG = "--restore-last-session"
# 与 scrape-product-colipu 等业务技能约定同一算法,便于 CDP 挂接已打开的账号窗口。
_CDP_PORT_BASE = 9330
_CDP_PORT_SPAN = 60
def human_chrome_launch_args(profile_dir, url=None, restore_session=False):
def cdp_port_for_profile(profile_dir: str) -> int:
"""按 profile 绝对路径派生稳定本机调试端口(9330–9389)。"""
import hashlib
key = os.path.abspath(profile_dir).replace("/", "\\").lower().encode("utf-8")
digest = hashlib.sha1(key).hexdigest()
return _CDP_PORT_BASE + (int(digest[:8], 16) % _CDP_PORT_SPAN)
def human_chrome_launch_args(profile_dir, url=None, restore_session=False, *, enable_cdp=True):
"""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: if restore_session:
args.append(RESTORE_LAST_SESSION_FLAG) args.append(RESTORE_LAST_SESSION_FLAG)
if enable_cdp:
port = cdp_port_for_profile(profile_abs)
args.append(f"--remote-debugging-port={port}")
args.append("--remote-debugging-address=127.0.0.1")
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, restore_session=False): def human_chrome_shortcut_arguments(profile_dir, url=None, restore_session=False, *, enable_cdp=True):
"""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: if restore_session:
parts.append(RESTORE_LAST_SESSION_FLAG) parts.append(RESTORE_LAST_SESSION_FLAG)
if enable_cdp:
port = cdp_port_for_profile(profile_abs)
parts.append(f"--remote-debugging-port={port}")
parts.append("--remote-debugging-address=127.0.0.1")
cleaned = (url or "").strip() cleaned = (url or "").strip()
if cleaned: if cleaned:
parts.append(cleaned) parts.append(cleaned)
@@ -329,21 +350,23 @@ def cmd_open(account_id):
# 首次/空 Profile:仍打开平台首页便于登录。 # 首次/空 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
cdp_port = cdp_port_for_profile(profile_abs)
cmd = [ cmd = [
browser_exe, browser_exe,
*human_chrome_launch_args( *human_chrome_launch_args(
profile_abs, launch_url, restore_session=has_session profile_abs, launch_url, restore_session=has_session, enable_cdp=True
), ),
] ]
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 "
"restore_last_session=%s platform=%s", "restore_last_session=%s cdp_port=%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, has_session,
cdp_port,
target.get("platform_key"), target.get("platform_key"),
) )
@@ -396,6 +419,7 @@ def cmd_open(account_id):
"pid": child.pid, "pid": child.pid,
"has_session": has_session, "has_session": has_session,
"restore_last_session": has_session, "restore_last_session": has_session,
"cdp_port": cdp_port,
"session_restore_on_startup": prefs_result.get("restore_after"), "session_restore_on_startup": prefs_result.get("restore_after"),
}, },
}, },

View File

@@ -193,6 +193,7 @@ class TestHumanChromeLaunchArgs:
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 "--restore-last-session" not in args
assert any(a.startswith("--remote-debugging-port=") for a 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
@@ -201,6 +202,7 @@ class TestHumanChromeLaunchArgs:
args = human_chrome_launch_args(str(tmp_path / "prof"), None, restore_session=True) args = human_chrome_launch_args(str(tmp_path / "prof"), None, restore_session=True)
assert "--restore-last-session" in args assert "--restore-last-session" in args
assert any(a.startswith("--remote-debugging-port=") for a in args)
assert not any(a.startswith("http") for a 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):
@@ -213,6 +215,7 @@ class TestHumanChromeLaunchArgs:
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 "--restore-last-session" not in text
assert "--remote-debugging-port=" in text
assert text.endswith("https://example.com") assert text.endswith("https://example.com")
def test_shortcut_restore_session(self, tmp_path): def test_shortcut_restore_session(self, tmp_path):
@@ -220,8 +223,16 @@ class TestHumanChromeLaunchArgs:
text = human_chrome_shortcut_arguments(str(tmp_path / "p"), None, restore_session=True) text = human_chrome_shortcut_arguments(str(tmp_path / "p"), None, restore_session=True)
assert "--restore-last-session" in text assert "--restore-last-session" in text
assert "--remote-debugging-port=" in text
assert "http" not in text assert "http" not in text
def test_cdp_port_stable_for_profile(self, tmp_path):
from service.browser_service import cdp_port_for_profile
p = str(tmp_path / "prof")
assert cdp_port_for_profile(p) == cdp_port_for_profile(p)
assert 9330 <= cdp_port_for_profile(p) < 9390
class TestResolveChromiumChannel: class TestResolveChromiumChannel:
"""resolve_chromium_channel returns a channel string when browsers exist.""" """resolve_chromium_channel returns a channel string when browsers exist."""