Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f54204a61 | |||
| f48e8aa75d | |||
| f5bab01421 |
17
SKILL.md
17
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.13
|
version: 2.0.16
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
@@ -34,13 +34,14 @@ 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>`)— 仅查看,不做站点侧登录判定。
|
8. **浏览器打开查看**:`account open <id>`(或兼容别名 `open <id>`)— 拉起 Profile 后立即返回,不做站点侧登录判定。数据管理「账号信息」行内 **打开浏览器** / **打开文件位置** 均为 `interaction: direct` + `sync`(见 `assets/actions.json`)。
|
||||||
9. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
9. **打开 Profile 目录**:`account reveal-profile <id>` — 在系统文件管理器中打开该账号 `profile_dir`。
|
||||||
10. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
10. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
||||||
11. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。
|
11. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
||||||
12. **按 lease 取明文密码**:`account get-credential <id> --reveal --lease-token <token>` — 结合 pick-web 返回的 lease 解密 `local_encrypted` 等路径。
|
12. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。
|
||||||
13. **业务 skill 同进程登录**:`from service.rpa_helpers import ensure_logged_in` — 详见 [`references/RPA_HELPERS.md`](references/RPA_HELPERS.md)。
|
13. **按 lease 取明文密码**:`account get-credential <id> --reveal --lease-token <token>` — 结合 pick-web 返回的 lease 解密 `local_encrypted` 等路径。
|
||||||
14. **有则用、无则建(网页账号)**:`account ensure-web --platform <p>` — 登记账号 ≠ 已登录;登录仍由业务技能 / `ensure_logged_in` 处理。
|
14. **业务 skill 同进程登录**:`from service.rpa_helpers import ensure_logged_in` — 详见 [`references/RPA_HELPERS.md`](references/RPA_HELPERS.md)。
|
||||||
|
15. **有则用、无则建(网页账号)**:`account ensure-web --platform <p>` — 登记账号 ≠ 已登录;登录仍由业务技能 / `ensure_logged_in` 处理。
|
||||||
|
|
||||||
## CLI 调用
|
## CLI 调用
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# assets
|
# assets
|
||||||
|
|
||||||
|
- `actions.json`:匠厂宿主 Skill Action 清单(数据管理「账号信息」行内:打开文件位置 / 打开浏览器)。
|
||||||
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
||||||
- `schemas/`:`get` / `list-json` / `pick-web` 单条对象的轻量 JSON Schema(`additionalProperties: true` 允许扩展字段)。
|
- `schemas/`:`get` / `list-json` / `pick-web` 单条对象的轻量 JSON Schema(`additionalProperties: true` 允许扩展字段)。
|
||||||
|
|
||||||
|
|||||||
64
assets/actions.json
Normal file
64
assets/actions.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"skill": "account-manager",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"id": "reveal-profile",
|
||||||
|
"label": "打开文件位置",
|
||||||
|
"description": "在系统文件管理器中打开该账号的浏览器 Profile 目录。",
|
||||||
|
"placements": ["row"],
|
||||||
|
"executionProfile": "sync",
|
||||||
|
"interaction": "direct",
|
||||||
|
"bind": {
|
||||||
|
"tables": ["accounts"],
|
||||||
|
"inputMapping": { "accountId": "$row.$pk" }
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["accountId"],
|
||||||
|
"properties": {
|
||||||
|
"accountId": {
|
||||||
|
"type": "number",
|
||||||
|
"title": "账号 ID",
|
||||||
|
"description": "accounts 表主键",
|
||||||
|
"readOnly": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
"type": "cli",
|
||||||
|
"command": "account",
|
||||||
|
"args": ["reveal-profile", "{{accountId}}"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "open-browser",
|
||||||
|
"label": "打开浏览器",
|
||||||
|
"description": "用该账号持久化 Profile 打开 Chrome/Edge,仅供肉眼核对(不做登录判定)。一点即开,不等待关闭。",
|
||||||
|
"placements": ["row"],
|
||||||
|
"executionProfile": "sync",
|
||||||
|
"interaction": "direct",
|
||||||
|
"bind": {
|
||||||
|
"tables": ["accounts"],
|
||||||
|
"inputMapping": { "accountId": "$row.$pk" }
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["accountId"],
|
||||||
|
"properties": {
|
||||||
|
"accountId": {
|
||||||
|
"type": "number",
|
||||||
|
"title": "账号 ID",
|
||||||
|
"description": "accounts 表主键",
|
||||||
|
"readOnly": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
"type": "cli",
|
||||||
|
"command": "account",
|
||||||
|
"args": ["open", "{{accountId}}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -54,7 +54,8 @@
|
|||||||
| `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>` | 打开持久化浏览器 profile(仅查看) |
|
| `account open <id>` | 打开持久化浏览器 profile(仅查看;**拉起后立即返回**,不等待关闭) |
|
||||||
|
| `account reveal-profile <id>` | 在系统文件管理器中打开该账号 `profile_dir` |
|
||||||
|
|
||||||
### D. 加密备份与迁移(v2)
|
### D. 加密备份与迁移(v2)
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
| `lease list [--active]` | 列出租约 |
|
| `lease list [--active]` | 列出租约 |
|
||||||
| `lease cleanup` | 清理过期租约 |
|
| `lease cleanup` | 清理过期租约 |
|
||||||
| `account open <id>` / `open <id>` | 浏览器查看 |
|
| `account open <id>` / `open <id>` | 浏览器查看 |
|
||||||
|
| `account reveal-profile <id>` | 打开 Profile 目录 |
|
||||||
|
|
||||||
### 兼容入口(旧版别名)
|
### 兼容入口(旧版别名)
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `ERROR:需要 playwright Python 包` | 未安装 `playwright` 包或本机无 Chrome/Edge |
|
| `ERROR:需要 playwright Python 包` | 未安装 `playwright` 包或本机无 Chrome/Edge |
|
||||||
| `ERROR:PROFILE_DIR_MISSING` | 账号记录中 `profile_dir` 为空 |
|
| `ERROR:PROFILE_DIR_MISSING` | 账号记录中 `profile_dir` 为空 |
|
||||||
|
| `ERROR:REVEAL_PROFILE_FAILED` | `account reveal-profile` 无法打开目录(系统调用失败) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ CLI entry point: argv dispatch for account-manager v2.
|
|||||||
|
|
||||||
Subcommand tree:
|
Subcommand tree:
|
||||||
platform list/get/ensure
|
platform list/get/ensure
|
||||||
account add-web|ensure-web|add-secret|get|get-credential|list|resolve|pick-web|mark-session|mark-error|mark-used|open
|
account add-web|ensure-web|add-secret|get|get-credential|list|resolve|pick-web|mark-session|mark-error|mark-used|open|reveal-profile
|
||||||
init-master-key|export-credentials|import-credentials
|
init-master-key|export-credentials|import-credentials
|
||||||
credential pick|resolve
|
credential pick|resolve
|
||||||
lease release|list|cleanup
|
lease release|list|cleanup
|
||||||
@@ -49,7 +49,7 @@ from service.account_service import (
|
|||||||
cmd_platform_get,
|
cmd_platform_get,
|
||||||
cmd_platform_list,
|
cmd_platform_list,
|
||||||
)
|
)
|
||||||
from service.browser_service import cmd_open
|
from service.browser_service import cmd_open, cmd_reveal_profile
|
||||||
from util.constants import LOG_LOGGER_NAME, SKILL_SLUG
|
from util.constants import LOG_LOGGER_NAME, SKILL_SLUG
|
||||||
from util.logging_config import get_skill_logger, setup_skill_logging
|
from util.logging_config import get_skill_logger, setup_skill_logging
|
||||||
from util.platforms import _platform_list_cn_for_help, resolve_platform_key
|
from util.platforms import _platform_list_cn_for_help, resolve_platform_key
|
||||||
@@ -104,6 +104,7 @@ _USAGE = """account-manager v2 — Credential & Browser Profile Manager
|
|||||||
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>
|
account open <id>
|
||||||
|
account reveal-profile <id>
|
||||||
account init-master-key [--print|--from-env|--rotate]
|
account init-master-key [--print|--from-env|--rotate]
|
||||||
account export-credentials --plaintext
|
account export-credentials --plaintext
|
||||||
account import-credentials [--replace-all] < backup.json
|
account import-credentials [--replace-all] < backup.json
|
||||||
@@ -118,8 +119,9 @@ _USAGE = """account-manager v2 — Credential & Browser Profile Manager
|
|||||||
lease list [--active]
|
lease list [--active]
|
||||||
lease cleanup
|
lease cleanup
|
||||||
|
|
||||||
浏览器:
|
浏览器 / Profile:
|
||||||
account open <id>
|
account open <id>
|
||||||
|
account reveal-profile <id> # 在文件管理器中打开 profile_dir
|
||||||
open <id> # 兼容别名
|
open <id> # 兼容别名
|
||||||
|
|
||||||
兼容入口(旧版):
|
兼容入口(旧版):
|
||||||
@@ -322,6 +324,11 @@ def main() -> None:
|
|||||||
print("ERROR:CLI_BAD_ARGS account open 需要 <id>。")
|
print("ERROR:CLI_BAD_ARGS account open 需要 <id>。")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
cmd_open(av[3])
|
cmd_open(av[3])
|
||||||
|
elif sub == "reveal-profile":
|
||||||
|
if len(av) < 4:
|
||||||
|
print("ERROR:CLI_BAD_ARGS account reveal-profile 需要 <id>。")
|
||||||
|
sys.exit(1)
|
||||||
|
cmd_reveal_profile(av[3])
|
||||||
elif sub == "init-master-key":
|
elif sub == "init-master-key":
|
||||||
cmd_account_init_master_key(av)
|
cmd_account_init_master_key(av)
|
||||||
elif sub == "export-credentials":
|
elif sub == "export-credentials":
|
||||||
|
|||||||
@@ -114,10 +114,85 @@ def resolve_account_browser_url(account: dict, conn=None) -> str:
|
|||||||
return (PLATFORMS.get(pk, {}).get("default_url") or "").strip()
|
return (PLATFORMS.get(pk, {}).get("default_url") or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_reveal_profile(account_id):
|
||||||
|
"""
|
||||||
|
在系统文件管理器中打开该账号的 profile_dir。
|
||||||
|
不写数据库、不启动浏览器。
|
||||||
|
"""
|
||||||
|
log = get_skill_logger()
|
||||||
|
init_db()
|
||||||
|
log.info("reveal_profile_start account_id=%r", account_id)
|
||||||
|
|
||||||
|
target = get_account_by_id(account_id)
|
||||||
|
if not target:
|
||||||
|
log.warning("reveal_profile_error account_id=%r not_found", account_id)
|
||||||
|
print("ERROR:ACCOUNT_NOT_FOUND")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
profile_dir = (target.get("profile_dir") or "").strip()
|
||||||
|
if not profile_dir:
|
||||||
|
log.warning("reveal_profile_error account_id=%s profile_dir_missing", account_id)
|
||||||
|
print("ERROR:PROFILE_DIR_MISSING 账号中 profile_dir 为空。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
profile_abs = os.path.abspath(profile_dir)
|
||||||
|
os.makedirs(profile_abs, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# explorer 即使成功也常返回非 0;用 startfile 打开目录更稳妥
|
||||||
|
os.startfile(profile_abs) # type: ignore[attr-defined]
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
subprocess.run(["open", profile_abs], check=False)
|
||||||
|
else:
|
||||||
|
subprocess.run(["xdg-open", profile_abs], check=False)
|
||||||
|
except OSError as exc:
|
||||||
|
log.error("reveal_profile_error account_id=%s error=%s", account_id, str(exc))
|
||||||
|
print(f"ERROR:REVEAL_PROFILE_FAILED {exc}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
log.info("reveal_profile_ok account_id=%s profile_dir=%s", account_id, profile_abs)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"account_id": int(target["id"]) if target.get("id") is not None else account_id,
|
||||||
|
"profile_dir": profile_abs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _spawn_browser_child(cmd: list[str], *, cwd: str, env: dict) -> subprocess.Popen:
|
||||||
|
"""Detach Playwright child so `account open` can return immediately."""
|
||||||
|
popen_kwargs: dict = {
|
||||||
|
"cwd": cwd,
|
||||||
|
"env": env,
|
||||||
|
"stdin": subprocess.DEVNULL,
|
||||||
|
"stdout": subprocess.DEVNULL,
|
||||||
|
"stderr": subprocess.DEVNULL,
|
||||||
|
"close_fds": True,
|
||||||
|
}
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# Keep browser window; detach from parent console / Skill Action wait.
|
||||||
|
flags = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
||||||
|
create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||||
|
if create_no_window:
|
||||||
|
flags |= create_no_window
|
||||||
|
popen_kwargs["creationflags"] = flags
|
||||||
|
else:
|
||||||
|
popen_kwargs["start_new_session"] = True
|
||||||
|
return subprocess.Popen(cmd, **popen_kwargs)
|
||||||
|
|
||||||
|
|
||||||
def cmd_open(account_id):
|
def cmd_open(account_id):
|
||||||
"""
|
"""
|
||||||
打开该账号的持久化浏览器,仅用于肉眼核对。
|
打开该账号的持久化浏览器,仅用于肉眼核对。
|
||||||
不写数据库、不判定是否已登录。
|
不写数据库、不判定是否已登录。
|
||||||
|
拉起浏览器子进程后立即返回(不等待窗口关闭),便于 Skill Action sync/direct。
|
||||||
"""
|
"""
|
||||||
log = get_skill_logger()
|
log = get_skill_logger()
|
||||||
init_db()
|
init_db()
|
||||||
@@ -127,26 +202,24 @@ def cmd_open(account_id):
|
|||||||
if not target:
|
if not target:
|
||||||
log.warning("browser_open_error account_id=%r not_found", account_id)
|
log.warning("browser_open_error account_id=%r not_found", account_id)
|
||||||
print("ERROR:ACCOUNT_NOT_FOUND")
|
print("ERROR:ACCOUNT_NOT_FOUND")
|
||||||
return
|
sys.exit(1)
|
||||||
|
|
||||||
channel = resolve_chromium_channel()
|
channel = resolve_chromium_channel()
|
||||||
if not channel:
|
if not channel:
|
||||||
_print_browser_install_hint()
|
_print_browser_install_hint()
|
||||||
return
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import playwright # noqa: F401
|
import playwright # noqa: F401
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print(
|
print("ERROR:需要 playwright Python 包。")
|
||||||
"ERROR:需要 playwright Python 包。"
|
sys.exit(1)
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
profile_dir = (target.get("profile_dir") or "").strip()
|
profile_dir = (target.get("profile_dir") or "").strip()
|
||||||
if not profile_dir:
|
if not profile_dir:
|
||||||
log.warning("browser_open_error account_id=%s profile_dir_missing", account_id)
|
log.warning("browser_open_error account_id=%s profile_dir_missing", account_id)
|
||||||
print("ERROR:PROFILE_DIR_MISSING 账号中 profile_dir 为空。")
|
print("ERROR:PROFILE_DIR_MISSING 账号中 profile_dir 为空。")
|
||||||
return
|
sys.exit(1)
|
||||||
os.makedirs(profile_dir, exist_ok=True)
|
os.makedirs(profile_dir, exist_ok=True)
|
||||||
|
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
@@ -164,28 +237,49 @@ def cmd_open(account_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge"
|
browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge"
|
||||||
print(f"正在打开 [{target['account_label']}] 的 {browser_name}(仅查看,不写入数据库)")
|
|
||||||
print(f"地址:{url}")
|
|
||||||
print("本命令不做登录检测。")
|
|
||||||
|
|
||||||
cfg = {"channel": channel, "profile_dir": profile_dir, "url": url}
|
cfg = {"channel": channel, "profile_dir": profile_dir, "url": url}
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
||||||
) as jf:
|
) as jf:
|
||||||
json.dump(cfg, jf, ensure_ascii=False)
|
json.dump(cfg, jf, ensure_ascii=False)
|
||||||
cfg_path = jf.name
|
cfg_path = jf.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
child = _spawn_browser_child(
|
||||||
[sys.executable, "-m", "service.open_child_runner", cfg_path],
|
[sys.executable, "-m", "service.open_child_runner", cfg_path],
|
||||||
cwd=_SCRIPTS_ROOT,
|
cwd=_SCRIPTS_ROOT,
|
||||||
env=subprocess_env_with_trace(),
|
env=subprocess_env_with_trace(),
|
||||||
)
|
)
|
||||||
log.info("browser_open_close account_id=%s channel=%s", account_id, channel)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("browser_open_error account_id=%s error=%s", account_id, str(e))
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
try:
|
try:
|
||||||
os.unlink(cfg_path)
|
os.unlink(cfg_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
log.error("browser_open_error account_id=%s error=%s", account_id, str(e))
|
||||||
|
print(f"ERROR:BROWSER_OPEN_FAILED {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"browser_open_spawned account_id=%s channel=%s pid=%s",
|
||||||
|
account_id,
|
||||||
|
channel,
|
||||||
|
child.pid,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"account_id": int(target["id"]) if target.get("id") is not None else account_id,
|
||||||
|
"account_label": target.get("account_label"),
|
||||||
|
"browser": browser_name,
|
||||||
|
"channel": channel,
|
||||||
|
"profile_dir": os.path.abspath(profile_dir),
|
||||||
|
"url": url,
|
||||||
|
"detached": True,
|
||||||
|
"pid": child.pid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
"""Playwright 仅打开浏览器子进程:由 browser_service.cmd_open 启动,argv[1] 为 JSON 配置路径。"""
|
"""Playwright 仅打开浏览器子进程:由 browser_service.cmd_open 启动,argv[1] 为 JSON 配置路径。
|
||||||
|
|
||||||
|
父进程拉起后即返回;本子进程自行维护浏览器生命周期,读完配置后删除临时 JSON。
|
||||||
|
"""
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
@@ -12,8 +16,14 @@ from util.playwright_stealth import (
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with open(sys.argv[1], encoding="utf-8") as f:
|
cfg_path = sys.argv[1]
|
||||||
|
with open(cfg_path, encoding="utf-8") as f:
|
||||||
c = json.load(f)
|
c = json.load(f)
|
||||||
|
try:
|
||||||
|
os.unlink(cfg_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
args, ignore_automation = persistent_context_launch_parts()
|
args, ignore_automation = persistent_context_launch_parts()
|
||||||
lc_kw = dict(
|
lc_kw = dict(
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ class TestPlaywrightLaunchHelpers:
|
|||||||
args, _ignore = persistent_context_launch_parts()
|
args, _ignore = persistent_context_launch_parts()
|
||||||
assert "--start-maximized" in args
|
assert "--start-maximized" in args
|
||||||
|
|
||||||
def test_open_child_runner_launch_keywords_documented(self):
|
def test_open_child_runner_cleans_config_after_read(self):
|
||||||
"""launch_persistent_context kwargs contract (static read of source)."""
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
path = os.path.join(
|
path = os.path.join(
|
||||||
@@ -28,10 +27,63 @@ class TestPlaywrightLaunchHelpers:
|
|||||||
path = os.path.abspath(path)
|
path = os.path.abspath(path)
|
||||||
with open(path, encoding="utf-8") as f:
|
with open(path, encoding="utf-8") as f:
|
||||||
src = f.read()
|
src = f.read()
|
||||||
|
assert "os.unlink(cfg_path)" in src
|
||||||
|
assert "wait_for_event" in src
|
||||||
assert "launch_persistent_context" in src
|
assert "launch_persistent_context" in src
|
||||||
assert "headless=False" in src
|
assert "headless=False" in src
|
||||||
assert "no_viewport=True" in src
|
assert "no_viewport=True" in src
|
||||||
assert '"channel"' in src or "'channel'" in src
|
|
||||||
|
|
||||||
|
class TestCmdOpenDetached:
|
||||||
|
def test_cmd_open_spawns_and_returns(self, clean_db, tmp_path, capsys, monkeypatch):
|
||||||
|
from db.accounts_repo import insert_account
|
||||||
|
from service import browser_service
|
||||||
|
from util.platforms import seed_platforms
|
||||||
|
|
||||||
|
seed_platforms(clean_db)
|
||||||
|
profile = tmp_path / "prof"
|
||||||
|
aid = insert_account(
|
||||||
|
clean_db,
|
||||||
|
platform_key="sohu",
|
||||||
|
account_label="open-test",
|
||||||
|
login_id="1",
|
||||||
|
login_id_type="phone",
|
||||||
|
tenant_id=None,
|
||||||
|
environment="production",
|
||||||
|
role="default",
|
||||||
|
provider_code="sohu",
|
||||||
|
profile_dir=str(profile),
|
||||||
|
url="https://example.com",
|
||||||
|
extra_json=None,
|
||||||
|
now=1_700_000_000,
|
||||||
|
auth_strategy="password_auto",
|
||||||
|
)
|
||||||
|
clean_db.commit()
|
||||||
|
|
||||||
|
monkeypatch.setattr(browser_service, "resolve_chromium_channel", lambda: "chrome")
|
||||||
|
|
||||||
|
class FakeProc:
|
||||||
|
pid = 4242
|
||||||
|
|
||||||
|
spawned = {}
|
||||||
|
|
||||||
|
def fake_spawn(cmd, *, cwd, env):
|
||||||
|
spawned["cmd"] = cmd
|
||||||
|
spawned["cwd"] = cwd
|
||||||
|
return FakeProc()
|
||||||
|
|
||||||
|
monkeypatch.setattr(browser_service, "_spawn_browser_child", fake_spawn)
|
||||||
|
|
||||||
|
browser_service.cmd_open(str(aid))
|
||||||
|
out = capsys.readouterr().out.strip()
|
||||||
|
import json
|
||||||
|
|
||||||
|
payload = json.loads(out.splitlines()[-1])
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert payload["data"]["detached"] is True
|
||||||
|
assert payload["data"]["pid"] == 4242
|
||||||
|
assert "-m" in spawned["cmd"]
|
||||||
|
assert "service.open_child_runner" in spawned["cmd"]
|
||||||
|
|
||||||
|
|
||||||
class TestResolveChromiumChannel:
|
class TestResolveChromiumChannel:
|
||||||
|
|||||||
75
tests/test_reveal_profile.py
Normal file
75
tests/test_reveal_profile.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""account reveal-profile:打开 profile 目录(不启动浏览器)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_account(conn, *, profile_dir: str) -> int:
|
||||||
|
from db.accounts_repo import insert_account
|
||||||
|
from util.platforms import seed_platforms
|
||||||
|
|
||||||
|
seed_platforms(conn)
|
||||||
|
aid = insert_account(
|
||||||
|
conn,
|
||||||
|
platform_key="sohu",
|
||||||
|
account_label="reveal-test",
|
||||||
|
login_id="13800000000",
|
||||||
|
login_id_type="phone",
|
||||||
|
tenant_id=None,
|
||||||
|
environment="production",
|
||||||
|
role="default",
|
||||||
|
provider_code="sohu",
|
||||||
|
profile_dir=profile_dir,
|
||||||
|
url="https://mp.sohu.com",
|
||||||
|
extra_json=None,
|
||||||
|
now=1_700_000_000,
|
||||||
|
auth_strategy="password_auto",
|
||||||
|
session_persistent=1,
|
||||||
|
device_fingerprint=None,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return aid
|
||||||
|
|
||||||
|
|
||||||
|
def test_reveal_profile_opens_dir(clean_db, tmp_path, capsys):
|
||||||
|
from service.browser_service import cmd_reveal_profile
|
||||||
|
|
||||||
|
profile = tmp_path / "profiles" / "sohu" / "reveal-test"
|
||||||
|
aid = _insert_account(clean_db, profile_dir=str(profile))
|
||||||
|
|
||||||
|
with mock.patch("service.browser_service.os.startfile") as startfile:
|
||||||
|
with mock.patch("service.browser_service.sys.platform", "win32"):
|
||||||
|
cmd_reveal_profile(str(aid))
|
||||||
|
|
||||||
|
startfile.assert_called_once()
|
||||||
|
called_path = startfile.call_args[0][0]
|
||||||
|
assert os.path.isdir(called_path)
|
||||||
|
assert os.path.abspath(str(profile)) == called_path
|
||||||
|
|
||||||
|
out = capsys.readouterr().out.strip()
|
||||||
|
payload = json.loads(out.splitlines()[-1])
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert payload["data"]["account_id"] == aid
|
||||||
|
assert payload["data"]["profile_dir"] == called_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_reveal_profile_missing_account(clean_db):
|
||||||
|
from service.browser_service import cmd_reveal_profile
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as ei:
|
||||||
|
cmd_reveal_profile("999999")
|
||||||
|
assert ei.value.code == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_reveal_profile_empty_dir(clean_db):
|
||||||
|
from service.browser_service import cmd_reveal_profile
|
||||||
|
|
||||||
|
aid = _insert_account(clean_db, profile_dir="")
|
||||||
|
with pytest.raises(SystemExit) as ei:
|
||||||
|
cmd_reveal_profile(str(aid))
|
||||||
|
assert ei.value.code == 1
|
||||||
@@ -32,6 +32,7 @@ def test_usage_includes_all_account_subcommands():
|
|||||||
"get-credential",
|
"get-credential",
|
||||||
"pick-web",
|
"pick-web",
|
||||||
"resolve",
|
"resolve",
|
||||||
|
"reveal-profile",
|
||||||
):
|
):
|
||||||
assert cmd in usage, f"USAGE 缺少 {cmd}"
|
assert cmd in usage, f"USAGE 缺少 {cmd}"
|
||||||
assert "--ensure" in usage
|
assert "--ensure" in usage
|
||||||
|
|||||||
Reference in New Issue
Block a user