diff --git a/SKILL.md b/SKILL.md index 9bcc213..b303b98 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.13 +version: 2.0.14 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -34,13 +34,14 @@ 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 `)— 仅查看,不做站点侧登录判定。 -9. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。 -10. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。 -11. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。 -12. **按 lease 取明文密码**:`account get-credential --reveal --lease-token ` — 结合 pick-web 返回的 lease 解密 `local_encrypted` 等路径。 -13. **业务 skill 同进程登录**:`from service.rpa_helpers import ensure_logged_in` — 详见 [`references/RPA_HELPERS.md`](references/RPA_HELPERS.md)。 -14. **有则用、无则建(网页账号)**:`account ensure-web --platform

` — 登记账号 ≠ 已登录;登录仍由业务技能 / `ensure_logged_in` 处理。 +8. **浏览器打开查看**:`account open `(或兼容别名 `open `)— 仅查看,不做站点侧登录判定。数据管理「账号信息」行内亦提供 **打开浏览器** / **打开文件位置**(见 `assets/actions.json`)。 +9. **打开 Profile 目录**:`account reveal-profile ` — 在系统文件管理器中打开该账号 `profile_dir`。 +10. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。 +11. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。 +12. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。 +13. **按 lease 取明文密码**:`account get-credential --reveal --lease-token ` — 结合 pick-web 返回的 lease 解密 `local_encrypted` 等路径。 +14. **业务 skill 同进程登录**:`from service.rpa_helpers import ensure_logged_in` — 详见 [`references/RPA_HELPERS.md`](references/RPA_HELPERS.md)。 +15. **有则用、无则建(网页账号)**:`account ensure-web --platform

` — 登记账号 ≠ 已登录;登录仍由业务技能 / `ensure_logged_in` 处理。 ## CLI 调用 diff --git a/assets/README.md b/assets/README.md index 8c663ad..b50a978 100644 --- a/assets/README.md +++ b/assets/README.md @@ -1,5 +1,6 @@ # assets +- `actions.json`:匠厂宿主 Skill Action 清单(数据管理「账号信息」行内:打开文件位置 / 打开浏览器)。 - `examples/`:CLI 成功输出形状示例(虚构路径与数据)。 - `schemas/`:`get` / `list-json` / `pick-web` 单条对象的轻量 JSON Schema(`additionalProperties: true` 允许扩展字段)。 diff --git a/assets/actions.json b/assets/actions.json new file mode 100644 index 0000000..586c44d --- /dev/null +++ b/assets/actions.json @@ -0,0 +1,65 @@ +{ + "schemaVersion": 1, + "skill": "account-manager", + "actions": [ + { + "id": "reveal-profile", + "label": "打开文件位置", + "description": "在系统文件管理器中打开该账号的浏览器 Profile 目录。", + "placements": ["row"], + "executionProfile": "sync", + "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": "async", + "confirmation": { + "message": "将用该账号的浏览器 Profile 打开 Chrome/Edge,仅供查看,不会自动登录或改库。" + }, + "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}}"] + } + } + ] +} diff --git a/references/CLI.md b/references/CLI.md index 84d6f2a..7657c4c 100644 --- a/references/CLI.md +++ b/references/CLI.md @@ -55,6 +55,7 @@ | `account mark-error --code --message ` | 标记错误 | | `account mark-used ` | 标记已使用 | | `account open ` | 打开持久化浏览器 profile(仅查看) | +| `account reveal-profile ` | 在系统文件管理器中打开该账号 `profile_dir` | ### D. 加密备份与迁移(v2) @@ -75,6 +76,7 @@ | `lease list [--active]` | 列出租约 | | `lease cleanup` | 清理过期租约 | | `account open ` / `open ` | 浏览器查看 | +| `account reveal-profile ` | 打开 Profile 目录 | ### 兼容入口(旧版别名) diff --git a/references/ERRORS.md b/references/ERRORS.md index e8f4282..5c5ecb5 100644 --- a/references/ERRORS.md +++ b/references/ERRORS.md @@ -87,6 +87,7 @@ |---|---| | `ERROR:需要 playwright Python 包` | 未安装 `playwright` 包或本机无 Chrome/Edge | | `ERROR:PROFILE_DIR_MISSING` | 账号记录中 `profile_dir` 为空 | +| `ERROR:REVEAL_PROFILE_FAILED` | `account reveal-profile` 无法打开目录(系统调用失败) | --- diff --git a/scripts/cli/app.py b/scripts/cli/app.py index b29de3b..7d81556 100644 --- a/scripts/cli/app.py +++ b/scripts/cli/app.py @@ -4,7 +4,7 @@ CLI entry point: argv dispatch for account-manager v2. Subcommand tree: 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 credential pick|resolve lease release|list|cleanup @@ -49,7 +49,7 @@ from service.account_service import ( cmd_platform_get, 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.logging_config import get_skill_logger, setup_skill_logging 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 --code --message account mark-used account open + account reveal-profile account init-master-key [--print|--from-env|--rotate] account export-credentials --plaintext account import-credentials [--replace-all] < backup.json @@ -118,8 +119,9 @@ _USAGE = """account-manager v2 — Credential & Browser Profile Manager lease list [--active] lease cleanup -浏览器: +浏览器 / Profile: account open + account reveal-profile # 在文件管理器中打开 profile_dir open # 兼容别名 兼容入口(旧版): @@ -322,6 +324,11 @@ def main() -> None: print("ERROR:CLI_BAD_ARGS account open 需要 。") sys.exit(1) cmd_open(av[3]) + elif sub == "reveal-profile": + if len(av) < 4: + print("ERROR:CLI_BAD_ARGS account reveal-profile 需要 。") + sys.exit(1) + cmd_reveal_profile(av[3]) elif sub == "init-master-key": cmd_account_init_master_key(av) elif sub == "export-credentials": diff --git a/scripts/service/browser_service.py b/scripts/service/browser_service.py index a8d6075..c3ac6fb 100644 --- a/scripts/service/browser_service.py +++ b/scripts/service/browser_service.py @@ -114,6 +114,58 @@ def resolve_account_browser_url(account: dict, conn=None) -> str: 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 cmd_open(account_id): """ 打开该账号的持久化浏览器,仅用于肉眼核对。 diff --git a/tests/test_reveal_profile.py b/tests/test_reveal_profile.py new file mode 100644 index 0000000..470e5a7 --- /dev/null +++ b/tests/test_reveal_profile.py @@ -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 diff --git a/tests/test_usage.py b/tests/test_usage.py index b1ca568..3998f4f 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -32,6 +32,7 @@ def test_usage_includes_all_account_subcommands(): "get-credential", "pick-web", "resolve", + "reveal-profile", ): assert cmd in usage, f"USAGE 缺少 {cmd}" assert "--ensure" in usage