Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 097dde0ebb |
4
SKILL.md
4
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.21
|
version: 2.0.22
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
@@ -34,7 +34,7 @@ allowed-tools:
|
|||||||
5. **用人话定位账号**:`account resolve --login-id <手机号/用户名>` 或 `--label` / `--query`(不必先查数字 id)。
|
5. **用人话定位账号**:`account resolve --login-id <手机号/用户名>` 或 `--label` / `--query`(不必先查数字 id)。
|
||||||
6. **为 RPA 挑选账号**:`account pick-web` — 按平台/环境/角色,也可 `--login-id` / `--label`;业务首启推荐 `account ensure-web`(或 `pick-web --ensure`):无账号则自动 `add-web` 再返回。
|
6. **为 RPA 挑选账号**:`account pick-web` — 按平台/环境/角色,也可 `--login-id` / `--label`;业务首启推荐 `account ensure-web`(或 `pick-web --ensure`):无账号则自动 `add-web` 再返回。
|
||||||
7. **获取 API Key(凭据表)**:`credential pick` — 默认隐藏 secret,`--reveal` 时才返回。
|
7. **获取 API Key(凭据表)**:`credential pick` — 默认隐藏 secret,`--reveal` 时才返回。
|
||||||
8. **打开浏览器**:`account open <id>`(或兼容别名 `open <id>`)— **本进程**直接拉起系统 Chrome/Edge(`--user-data-dir`),与「打开目录」同模式;不做登录判定。数据管理行内按钮均为 `interaction: direct` + `sync`,成功不弹提示。
|
8. **打开浏览器**:`account open <id>`(或兼容别名 `open <id>`)— **本进程**直接拉起系统 Chrome/Edge(独立 `--user-data-dir`),最大化并跳过 Chrome 首次欢迎页,直接打开平台链接;与「打开目录」同为一点即走,不做登录判定。数据管理行内按钮均为 `interaction: direct` + `sync`,成功不弹提示。
|
||||||
9. **打开目录**:`account reveal-profile <id>` — 在系统文件管理器中打开该账号 `profile_dir`。
|
9. **打开目录**:`account reveal-profile <id>` — 在系统文件管理器中打开该账号 `profile_dir`。
|
||||||
10. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
10. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
||||||
11. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
11. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
| `account mark-session <id> --session-status <status>` | 标记会话状态 |
|
| `account mark-session <id> --session-status <status>` | 标记会话状态 |
|
||||||
| `account mark-error <id> --code <code> --message <msg>` | 标记错误 |
|
| `account mark-error <id> --code <code> --message <msg>` | 标记错误 |
|
||||||
| `account mark-used <id>` | 标记已使用 |
|
| `account mark-used <id>` | 标记已使用 |
|
||||||
| `account open <id>` | 本进程直接打开系统 Chrome/Edge(`--user-data-dir=profile`;无额外 python 子进程) |
|
| `account open <id>` | 本进程直接打开系统 Chrome/Edge(独立 profile;最大化;跳过首次欢迎页;打开平台 URL) |
|
||||||
| `account reveal-profile <id>` | 在系统文件管理器中打开该账号 `profile_dir` |
|
| `account reveal-profile <id>` | 在系统文件管理器中打开该账号 `profile_dir` |
|
||||||
|
|
||||||
### D. 加密备份与迁移(v2)
|
### D. 加密备份与迁移(v2)
|
||||||
|
|||||||
@@ -162,7 +162,9 @@ def create_web_account_record(
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
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)
|
log.info("account_add_web_success id=%s platform=%s profile_dir=%s", new_id, key, resolved_profile_dir)
|
||||||
ok_payload = {
|
ok_payload = {
|
||||||
|
|||||||
@@ -75,6 +75,35 @@ def resolve_chromium_channel():
|
|||||||
return "chrome"
|
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():
|
def _print_browser_install_hint():
|
||||||
print("❌ 未检测到 Google Chrome 或 Microsoft Edge,请先安装:")
|
print("❌ 未检测到 Google Chrome 或 Microsoft Edge,请先安装:")
|
||||||
print(" • Chrome: https://www.google.com/chrome/")
|
print(" • Chrome: https://www.google.com/chrome/")
|
||||||
@@ -164,9 +193,9 @@ def cmd_reveal_profile(account_id):
|
|||||||
|
|
||||||
def cmd_open(account_id):
|
def cmd_open(account_id):
|
||||||
"""
|
"""
|
||||||
打开该账号的持久化浏览器,仅用于肉眼核对。
|
打开该账号的持久化浏览器,仅用于肉眼核对 / 手工登录 / 抓 DOM。
|
||||||
与 reveal-profile 同模式:本进程直接拉起系统 Chrome/Edge 后立即返回。
|
与 reveal-profile 同模式:本进程直接拉起系统 Chrome/Edge 后立即返回。
|
||||||
不写数据库、不判定是否已登录、不起 Playwright / 额外 python 子进程。
|
最大化、跳过 Chrome 首次欢迎页;不写数据库、不判定是否已登录、不起 Playwright。
|
||||||
"""
|
"""
|
||||||
log = get_skill_logger()
|
log = get_skill_logger()
|
||||||
init_db()
|
init_db()
|
||||||
@@ -202,10 +231,8 @@ def cmd_open(account_id):
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# 与 profile_shortcut 一致:独立 user-data-dir 保存登录态
|
# 与 profile_shortcut 同一套人用参数:独立 user-data-dir 保存登录态
|
||||||
cmd = [browser_exe, f"--user-data-dir={profile_abs}"]
|
cmd = [browser_exe, *human_chrome_launch_args(profile_abs, url)]
|
||||||
if url:
|
|
||||||
cmd.append(url)
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"browser_open_launch exe=%s profile_dir=%s url=%s platform=%s",
|
"browser_open_launch exe=%s profile_dir=%s url=%s platform=%s",
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from typing import Optional
|
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]')
|
_WIN_LNK_FORBIDDEN = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
||||||
|
|
||||||
@@ -28,9 +31,10 @@ def create_profile_browser_shortcut(
|
|||||||
profile_dir: str,
|
profile_dir: str,
|
||||||
account_label: str,
|
account_label: str,
|
||||||
log: Optional[logging.Logger] = None,
|
log: Optional[logging.Logger] = None,
|
||||||
|
url: Optional[str] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
在 profile_dir 内创建 {account_label}.lnk,目标为系统 Chrome/Edge + --user-data-dir。
|
在 profile_dir 内创建 {account_label}.lnk,与「打开浏览器」同一套人用参数。
|
||||||
|
|
||||||
非 Windows 或未安装 Chrome/Edge 时静默跳过,返回 None。
|
非 Windows 或未安装 Chrome/Edge 时静默跳过,返回 None。
|
||||||
成功返回 .lnk 绝对路径。
|
成功返回 .lnk 绝对路径。
|
||||||
@@ -52,7 +56,7 @@ def create_profile_browser_shortcut(
|
|||||||
|
|
||||||
lnk_name = f"{_lnk_safe_filename(account_label)}.lnk"
|
lnk_name = f"{_lnk_safe_filename(account_label)}.lnk"
|
||||||
lnk_path = os.path.join(profile_abs, lnk_name)
|
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)
|
working_dir = os.path.dirname(browser_exe)
|
||||||
|
|
||||||
ps = (
|
ps = (
|
||||||
|
|||||||
@@ -77,13 +77,42 @@ class TestCmdOpenDirectBrowser:
|
|||||||
assert payload["data"]["browser_exe"] == fake_exe
|
assert payload["data"]["browser_exe"] == fake_exe
|
||||||
assert spawned["cmd"][0] == fake_exe
|
assert spawned["cmd"][0] == fake_exe
|
||||||
assert spawned["cmd"][1].startswith("--user-data-dir=")
|
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
|
# must NOT spawn python / open_child_runner
|
||||||
joined = " ".join(spawned["cmd"])
|
joined = " ".join(spawned["cmd"])
|
||||||
assert "open_child_runner" not in joined
|
assert "open_child_runner" not in joined
|
||||||
assert "python" not in joined.lower()
|
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:
|
class TestResolveChromiumChannel:
|
||||||
"""resolve_chromium_channel returns a channel string when browsers exist."""
|
"""resolve_chromium_channel returns a channel string when browsers exist."""
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,33 @@ class TestCreateProfileBrowserShortcut:
|
|||||||
out = create_profile_browser_shortcut(str(tmp_path), "demo")
|
out = create_profile_browser_shortcut(str(tmp_path), "demo")
|
||||||
assert out is None
|
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")
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows .lnk only")
|
||||||
def test_creates_lnk_on_windows(self, tmp_path, monkeypatch) -> None:
|
def test_creates_lnk_on_windows(self, tmp_path, monkeypatch) -> None:
|
||||||
from service.browser_service import resolve_chromium_executable
|
from service.browser_service import resolve_chromium_executable
|
||||||
|
|||||||
Reference in New Issue
Block a user