From 097dde0ebb13cda5ea39d25f9a48b740cba4ec6d Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Sat, 22 Aug 2026 14:11:05 +0800 Subject: [PATCH] fix: open account browser maximized without Chrome first-run Human open now uses start-maximized and no-first-run flags, and profile shortcuts share the same args plus the platform URL. Co-authored-by: Cursor --- SKILL.md | 4 +-- references/CLI.md | 2 +- scripts/service/account_add_commands.py | 4 ++- scripts/service/browser_service.py | 39 +++++++++++++++++++++---- scripts/util/profile_shortcut.py | 10 +++++-- tests/test_browser_open.py | 31 +++++++++++++++++++- tests/test_profile_shortcut.py | 27 +++++++++++++++++ 7 files changed, 103 insertions(+), 14 deletions(-) diff --git a/SKILL.md b/SKILL.md index 8b93238..d34d697 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.21 +version: 2.0.22 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -34,7 +34,7 @@ allowed-tools: 5. **用人话定位账号**:`account resolve --login-id <手机号/用户名>` 或 `--label` / `--query`(不必先查数字 id)。 6. **为 RPA 挑选账号**:`account pick-web` — 按平台/环境/角色,也可 `--login-id` / `--label`;业务首启推荐 `account ensure-web`(或 `pick-web --ensure`):无账号则自动 `add-web` 再返回。 7. **获取 API Key(凭据表)**:`credential pick` — 默认隐藏 secret,`--reveal` 时才返回。 -8. **打开浏览器**:`account open `(或兼容别名 `open `)— **本进程**直接拉起系统 Chrome/Edge(`--user-data-dir`),与「打开目录」同模式;不做登录判定。数据管理行内按钮均为 `interaction: direct` + `sync`,成功不弹提示。 +8. **打开浏览器**:`account open `(或兼容别名 `open `)— **本进程**直接拉起系统 Chrome/Edge(独立 `--user-data-dir`),最大化并跳过 Chrome 首次欢迎页,直接打开平台链接;与「打开目录」同为一点即走,不做登录判定。数据管理行内按钮均为 `interaction: direct` + `sync`,成功不弹提示。 9. **打开目录**:`account reveal-profile ` — 在系统文件管理器中打开该账号 `profile_dir`。 10. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。 11. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。 diff --git a/references/CLI.md b/references/CLI.md index b7437f3..39b4d91 100644 --- a/references/CLI.md +++ b/references/CLI.md @@ -54,7 +54,7 @@ | `account mark-session --session-status ` | 标记会话状态 | | `account mark-error --code --message ` | 标记错误 | | `account mark-used ` | 标记已使用 | -| `account open ` | 本进程直接打开系统 Chrome/Edge(`--user-data-dir=profile`;无额外 python 子进程) | +| `account open ` | 本进程直接打开系统 Chrome/Edge(独立 profile;最大化;跳过首次欢迎页;打开平台 URL) | | `account reveal-profile ` | 在系统文件管理器中打开该账号 `profile_dir` | ### D. 加密备份与迁移(v2) diff --git a/scripts/service/account_add_commands.py b/scripts/service/account_add_commands.py index 564c7ab..2297278 100644 --- a/scripts/service/account_add_commands.py +++ b/scripts/service/account_add_commands.py @@ -162,7 +162,9 @@ def create_web_account_record( finally: conn.close() - shortcut_path = create_profile_browser_shortcut(resolved_profile_dir, safe_label, log) + shortcut_path = create_profile_browser_shortcut( + resolved_profile_dir, safe_label, log, url=resolved_url, + ) log.info("account_add_web_success id=%s platform=%s profile_dir=%s", new_id, key, resolved_profile_dir) ok_payload = { diff --git a/scripts/service/browser_service.py b/scripts/service/browser_service.py index 7868186..8c5bf15 100644 --- a/scripts/service/browser_service.py +++ b/scripts/service/browser_service.py @@ -75,6 +75,35 @@ def resolve_chromium_channel(): return "chrome" +# 给人核对/手工登录用:跳过 Chrome 首次欢迎页并最大化。不要带 Playwright 的 --enable-automation。 +HUMAN_CHROME_FLAGS = ( + "--start-maximized", + "--no-first-run", + "--no-default-browser-check", + "--disable-search-engine-choice-screen", +) + + +def human_chrome_launch_args(profile_dir, url=None): + """chrome.exe 之后的参数(Popen 列表形式,路径不额外加引号)。""" + profile_abs = os.path.abspath(profile_dir) + args = [f"--user-data-dir={profile_abs}", *HUMAN_CHROME_FLAGS] + cleaned = (url or "").strip() + if cleaned: + args.append(cleaned) + return args + + +def human_chrome_shortcut_arguments(profile_dir, url=None): + """Windows .lnk Arguments 字符串(user-data-dir 带引号)。""" + profile_abs = os.path.abspath(profile_dir) + parts = [f'--user-data-dir="{profile_abs}"', *HUMAN_CHROME_FLAGS] + cleaned = (url or "").strip() + if cleaned: + parts.append(cleaned) + return " ".join(parts) + + def _print_browser_install_hint(): print("❌ 未检测到 Google Chrome 或 Microsoft Edge,请先安装:") print(" • Chrome: https://www.google.com/chrome/") @@ -164,9 +193,9 @@ def cmd_reveal_profile(account_id): def cmd_open(account_id): """ - 打开该账号的持久化浏览器,仅用于肉眼核对。 + 打开该账号的持久化浏览器,仅用于肉眼核对 / 手工登录 / 抓 DOM。 与 reveal-profile 同模式:本进程直接拉起系统 Chrome/Edge 后立即返回。 - 不写数据库、不判定是否已登录、不起 Playwright / 额外 python 子进程。 + 最大化、跳过 Chrome 首次欢迎页;不写数据库、不判定是否已登录、不起 Playwright。 """ log = get_skill_logger() init_db() @@ -202,10 +231,8 @@ def cmd_open(account_id): finally: conn.close() - # 与 profile_shortcut 一致:独立 user-data-dir 保存登录态 - cmd = [browser_exe, f"--user-data-dir={profile_abs}"] - if url: - cmd.append(url) + # 与 profile_shortcut 同一套人用参数:独立 user-data-dir 保存登录态 + cmd = [browser_exe, *human_chrome_launch_args(profile_abs, url)] log.info( "browser_open_launch exe=%s profile_dir=%s url=%s platform=%s", diff --git a/scripts/util/profile_shortcut.py b/scripts/util/profile_shortcut.py index 581ff75..2e72c57 100644 --- a/scripts/util/profile_shortcut.py +++ b/scripts/util/profile_shortcut.py @@ -9,7 +9,10 @@ import subprocess import sys from typing import Optional -from service.browser_service import resolve_chromium_executable +from service.browser_service import ( + human_chrome_shortcut_arguments, + resolve_chromium_executable, +) _WIN_LNK_FORBIDDEN = re.compile(r'[<>:"/\\|?*\x00-\x1f]') @@ -28,9 +31,10 @@ def create_profile_browser_shortcut( profile_dir: str, account_label: str, log: Optional[logging.Logger] = None, + url: Optional[str] = None, ) -> Optional[str]: """ - 在 profile_dir 内创建 {account_label}.lnk,目标为系统 Chrome/Edge + --user-data-dir。 + 在 profile_dir 内创建 {account_label}.lnk,与「打开浏览器」同一套人用参数。 非 Windows 或未安装 Chrome/Edge 时静默跳过,返回 None。 成功返回 .lnk 绝对路径。 @@ -52,7 +56,7 @@ def create_profile_browser_shortcut( lnk_name = f"{_lnk_safe_filename(account_label)}.lnk" lnk_path = os.path.join(profile_abs, lnk_name) - arguments = f'--user-data-dir="{profile_abs}"' + arguments = human_chrome_shortcut_arguments(profile_abs, url) working_dir = os.path.dirname(browser_exe) ps = ( diff --git a/tests/test_browser_open.py b/tests/test_browser_open.py index 267ab5f..56331be 100644 --- a/tests/test_browser_open.py +++ b/tests/test_browser_open.py @@ -77,13 +77,42 @@ class TestCmdOpenDirectBrowser: assert payload["data"]["browser_exe"] == fake_exe assert spawned["cmd"][0] == fake_exe assert spawned["cmd"][1].startswith("--user-data-dir=") - assert spawned["cmd"][2] == "https://example.com" + assert "--start-maximized" in spawned["cmd"] + assert "--no-first-run" in spawned["cmd"] + assert "--no-default-browser-check" in spawned["cmd"] + assert "--disable-search-engine-choice-screen" in spawned["cmd"] + assert spawned["cmd"][-1] == "https://example.com" + assert "--enable-automation" not in spawned["cmd"] # must NOT spawn python / open_child_runner joined = " ".join(spawned["cmd"]) assert "open_child_runner" not in joined assert "python" not in joined.lower() +class TestHumanChromeLaunchArgs: + def test_flags_then_url(self, tmp_path): + from service.browser_service import HUMAN_CHROME_FLAGS, human_chrome_launch_args + + profile = str(tmp_path / "prof") + args = human_chrome_launch_args(profile, "https://chat.deepseek.com") + assert args[0].startswith("--user-data-dir=") + for flag in HUMAN_CHROME_FLAGS: + assert flag in args + assert args[-1] == "https://chat.deepseek.com" + assert "--enable-automation" not in args + + def test_shortcut_arguments_quote_profile_and_include_url(self, tmp_path): + from service.browser_service import human_chrome_shortcut_arguments + + profile = tmp_path / "my prof" + profile.mkdir() + text = human_chrome_shortcut_arguments(str(profile), "https://example.com") + assert '--user-data-dir="' in text + assert "--start-maximized" in text + assert "--no-first-run" in text + assert text.endswith("https://example.com") + + class TestResolveChromiumChannel: """resolve_chromium_channel returns a channel string when browsers exist.""" diff --git a/tests/test_profile_shortcut.py b/tests/test_profile_shortcut.py index 5f3e396..a1a57b2 100644 --- a/tests/test_profile_shortcut.py +++ b/tests/test_profile_shortcut.py @@ -31,6 +31,33 @@ class TestCreateProfileBrowserShortcut: out = create_profile_browser_shortcut(str(tmp_path), "demo") assert out is None + def test_windows_lnk_uses_human_chrome_args(self, tmp_path, monkeypatch) -> None: + captured = {} + + def fake_run(argv, **kwargs): + captured["argv"] = list(argv) + class Result: + returncode = 0 + return Result() + + monkeypatch.setattr("util.profile_shortcut.sys.platform", "win32") + monkeypatch.setattr( + "util.profile_shortcut.resolve_chromium_executable", + lambda: r"C:\Program Files\Google\Chrome\Application\chrome.exe", + ) + monkeypatch.setattr("util.profile_shortcut.subprocess.run", fake_run) + monkeypatch.setattr("util.profile_shortcut.os.path.isfile", lambda path: True) + + out = create_profile_browser_shortcut( + str(tmp_path), "demo", url="https://chat.deepseek.com", + ) + assert out is not None + ps = captured["argv"][-1] + assert "--start-maximized" in ps + assert "--no-first-run" in ps + assert "https://chat.deepseek.com" in ps + assert "--enable-automation" not in ps + @pytest.mark.skipif(sys.platform != "win32", reason="Windows .lnk only") def test_creates_lnk_on_windows(self, tmp_path, monkeypatch) -> None: from service.browser_service import resolve_chromium_executable