Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bac647673 | |||
| 8d36e193b4 | |||
| e441b0c58e |
22
SKILL.md
22
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.7
|
version: 2.0.11
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
@@ -29,15 +29,17 @@ allowed-tools:
|
|||||||
|
|
||||||
1. **登记网页/RPA 账号**:`account add-web` — 平台 + 登录标识 + 自动生成 profile_dir;可选 `--auth-strategy` / `--session-persistent` / `--device-fingerprint`。
|
1. **登记网页/RPA 账号**:`account add-web` — 平台 + 登录标识 + 自动生成 profile_dir;可选 `--auth-strategy` / `--session-persistent` / `--device-fingerprint`。
|
||||||
2. **登记凭据(API Key / Token / 加密密码)**:`account add-secret` — `none` / `env` / `windows_credential` / `local_encrypted`。
|
2. **登记凭据(API Key / Token / 加密密码)**:`account add-secret` — `none` / `env` / `windows_credential` / `local_encrypted`。
|
||||||
3. **查看/列出账号**:`account get/list` — JSON 输出。
|
3. **查看/列出账号**:`account get/list` — JSON 输出;list 可按 `--login-id` / `--label` 筛选。
|
||||||
4. **为 RPA 挑选账号**:`account pick-web` — 按环境/租户/角色筛选,可带 lease。
|
4. **用人话定位账号**:`account resolve --login-id <手机号/用户名>` 或 `--label` / `--query`(不必先查数字 id)。
|
||||||
5. **获取 API Key(凭据表)**:`credential pick` — 默认隐藏 secret,`--reveal` 时才返回。
|
5. **为 RPA 挑选账号**:`account pick-web` — 按平台/环境/角色,也可 `--login-id` / `--label`;业务首启推荐 `account ensure-web`(或 `pick-web --ensure`):无账号则自动 `add-web` 再返回。
|
||||||
6. **浏览器打开查看**:`account open <id>`(或兼容别名 `open <id>`)— 仅查看,不做站点侧登录判定。
|
6. **获取 API Key(凭据表)**:`credential pick` — 默认隐藏 secret,`--reveal` 时才返回。
|
||||||
7. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
7. **浏览器打开查看**:`account open <id>`(或兼容别名 `open <id>`)— 仅查看,不做站点侧登录判定。
|
||||||
8. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
8. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
||||||
9. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。
|
9. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
||||||
10. **按 lease 取明文密码**:`account get-credential <id> --reveal --lease-token <token>` — 结合 pick-web 返回的 lease 解密 `local_encrypted` 等路径。
|
10. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。
|
||||||
11. **业务 skill 同进程登录**:`from service.rpa_helpers import ensure_logged_in` — 详见 [`references/RPA_HELPERS.md`](references/RPA_HELPERS.md)。
|
11. **按 lease 取明文密码**:`account get-credential <id> --reveal --lease-token <token>` — 结合 pick-web 返回的 lease 解密 `local_encrypted` 等路径。
|
||||||
|
12. **业务 skill 同进程登录**:`from service.rpa_helpers import ensure_logged_in` — 详见 [`references/RPA_HELPERS.md`](references/RPA_HELPERS.md)。
|
||||||
|
13. **有则用、无则建(网页账号)**:`account ensure-web --platform <p>` — 登记账号 ≠ 已登录;登录仍由业务技能 / `ensure_logged_in` 处理。
|
||||||
|
|
||||||
## CLI 调用
|
## CLI 调用
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,11 @@
|
|||||||
| `account add-web --platform <p> [--login-id <id>] [--login-id-type <type>] [...]` | 创建网页/RPA 账号;可选 `--auth-strategy` / `--session-persistent 0\|1` / `--device-fingerprint <fp>` |
|
| `account add-web --platform <p> [--login-id <id>] [--login-id-type <type>] [...]` | 创建网页/RPA 账号;可选 `--auth-strategy` / `--session-persistent 0\|1` / `--device-fingerprint <fp>` |
|
||||||
| `account add-secret --platform <p> --credential-type <t> --secret-storage env\|windows_credential\|none\|local_encrypted` | 创建凭据;`local_encrypted` **必须** `--secret-stdin` |
|
| `account add-secret --platform <p> --credential-type <t> --secret-storage env\|windows_credential\|none\|local_encrypted` | 创建凭据;`local_encrypted` **必须** `--secret-stdin` |
|
||||||
| `account get <id> [--with-credentials]` | 输出账号 JSON(若带 `--with-credentials` 则附加关联凭据摘要) |
|
| `account get <id> [--with-credentials]` | 输出账号 JSON(若带 `--with-credentials` 则附加关联凭据摘要) |
|
||||||
|
| `account resolve [--id <id>] [--platform <p>] [--login-id <手机号/用户名>] [--label <备注>] [--query <…>]` | **用人话定位一条账号**(成功输出同 `get`);多条匹配返回 `ERROR:AMBIGUOUS_ACCOUNT` |
|
||||||
| `account get-credential <account_id> [--reveal --lease-token <token>]` | v2:**按账号 id** 取活跃凭据;`--reveal` 必须配合合法 lease |
|
| `account get-credential <account_id> [--reveal --lease-token <token>]` | v2:**按账号 id** 取活跃凭据;`--reveal` 必须配合合法 lease |
|
||||||
| `account list [--platform <p>] [--environment <env>] [--tenant-id <t>] [--role <role>] [--limit N]` | 输出账号 JSON **数组** |
|
| `account list [--platform <p>] [...] [--login-id <id>] [--label <label>] [--limit N]` | 输出账号 JSON **数组**;可按登录标识/备注筛选 |
|
||||||
| `account pick-web --platform <p> [--environment <env>] [--tenant-id <t>] [--role <role>] [--lease] [--ttl-sec 900] [--holder <h>] [--purpose <purpose>]` | 挑选网页自动化候选;返回 JSON 含 `auth_strategy` / `session_persistent` / `device_fingerprint` |
|
| `account pick-web --platform <p> [...] [--login-id <…>] [--label <…>] [--lease] [...] [--ensure]` | 挑选网页自动化候选;可按手机号/用户名/备注精确筛选;`--ensure` 等同 `ensure-web` |
|
||||||
|
| `account ensure-web --platform <p> [--login-id <…>] [--label <…>] [...] [--lease]` | **有则 pick、无则 add-web 再 pick**;指定 `--login-id`/`--label` 时按该标识筛选/创建;均被租约占用时**不新建**,返回 `ERROR:LEASE_CONFLICT` |
|
||||||
| `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>` | 标记已使用 |
|
||||||
|
|||||||
@@ -50,7 +50,9 @@
|
|||||||
| `ERROR:NO_ACCOUNTS_FOUND` | 列表无结果 |
|
| `ERROR:NO_ACCOUNTS_FOUND` | 列表无结果 |
|
||||||
| `ERROR:ACCOUNT_NOT_FOUND` | 指定 id 不存在 |
|
| `ERROR:ACCOUNT_NOT_FOUND` | 指定 id 不存在 |
|
||||||
| `ERROR:ACCOUNT_DISABLED` | 账号状态为 disabled |
|
| `ERROR:ACCOUNT_DISABLED` | 账号状态为 disabled |
|
||||||
| `ERROR:NO_ACCOUNT` | `pick-web` 无符合条件的可用账号(含均被 active 未过期 lease 占用) |
|
| `ERROR:NO_ACCOUNT` | `pick-web` / `resolve` 无符合条件的可用账号(含均被 active 未过期 lease 占用) |
|
||||||
|
| `ERROR:AMBIGUOUS_ACCOUNT` | `resolve` / 带 `--login-id`/`--label` 的 `pick-web`:匹配到多条,需改用 `--id` 或补 `--platform` |
|
||||||
|
| `ERROR:LEASE_CONFLICT` | `ensure-web` / `pick-web --ensure`:已有账号但均被租约占用(**不会**自动再建);或同账号重复 acquire lease |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
| **A. CLI subprocess(v1 起)** | 任意业务 skill;一次性取账号 JSON / 凭据 / lease | `subprocess.run([sys.executable, f"{skill_root}/scripts/main.py", "account", "pick-web", ...])` |
|
| **A. CLI subprocess(v1 起)** | 任意业务 skill;一次性取账号 JSON / 凭据 / lease | `subprocess.run([sys.executable, f"{skill_root}/scripts/main.py", "account", "pick-web", ...])` |
|
||||||
| **B. rpa_helpers 同进程 import(v2)** | 需要 Playwright 上下文内完成登录与页面操作 | `sys.path.insert(0, f"{account_manager_root}/scripts"); from service.rpa_helpers import ensure_logged_in` |
|
| **B. rpa_helpers 同进程 import(v2)** | 需要 Playwright 上下文内完成登录与页面操作 | `sys.path.insert(0, f"{account_manager_root}/scripts"); from service.rpa_helpers import ensure_logged_in` |
|
||||||
|
|
||||||
常见组合:**CLI** 负责 pick / lease / `get-credential --reveal`;**rpa_helpers** 负责 `launch_browser_with_profile` + `ensure_logged_in`。详见 [`RPA_HELPERS.md`](RPA_HELPERS.md)。
|
常见组合:**CLI** 负责 `ensure-web`(或 `pick-web --ensure`)/ lease / `get-credential --reveal`;**rpa_helpers** 负责 `launch_browser_with_profile` + `ensure_logged_in`。详见 [`RPA_HELPERS.md`](RPA_HELPERS.md)。
|
||||||
|
|
||||||
|
业务技能首启推荐:`account ensure-web --platform <p> [--label ...] [--lease --holder <skill>]`,避免因库中尚无账号而 `NO_ACCOUNT`。登记账号 ≠ 站点已登录。
|
||||||
|
|
||||||
|
指定某人读账号时:`account resolve --platform <p> --login-id <手机号或用户名>`,或 `pick-web --platform <p> --login-id <…> --lease`;多条匹配返回 `ERROR:AMBIGUOUS_ACCOUNT`。
|
||||||
|
|
||||||
## 推荐调用方式
|
## 推荐调用方式
|
||||||
|
|
||||||
@@ -26,6 +30,8 @@
|
|||||||
| 取单条账号 JSON | `account get <id>` | **单行 JSON 对象**(无固定 `success` 字段) |
|
| 取单条账号 JSON | `account get <id>` | **单行 JSON 对象**(无固定 `success` 字段) |
|
||||||
| 批量账号 JSON | `account list ...` | **单行 JSON 数组** |
|
| 批量账号 JSON | `account list ...` | **单行 JSON 数组** |
|
||||||
| 网页自动化候选 | `account pick-web ...` | **单行 JSON 对象** |
|
| 网页自动化候选 | `account pick-web ...` | **单行 JSON 对象** |
|
||||||
|
| 有则用无则建 | `account ensure-web ...` / `pick-web --ensure` | **同 pick-web**;本次新建时多 `account_ensured: true` |
|
||||||
|
| 用人话定位账号 | `account resolve --login-id …` / `--label …` / `--query …` | **同 account get**(单行账号对象) |
|
||||||
| 取加密凭据明文 | `account get-credential <id> --reveal --lease-token <t>` | **单行 JSON**,含 `plaintext`(若可解密) |
|
| 取加密凭据明文 | `account get-credential <id> --reveal --lease-token <t>` | **单行 JSON**,含 `plaintext`(若可解密) |
|
||||||
| 备份加密凭据 | `account export-credentials --plaintext` | **JSON 数组**(stderr 有警告行) |
|
| 备份加密凭据 | `account export-credentials --plaintext` | **JSON 数组**(stderr 有警告行) |
|
||||||
| 导入加密凭据 | `account import-credentials [--replace-all]` | **通常为空**(计数在 stderr) |
|
| 导入加密凭据 | `account import-credentials [--replace-all]` | **通常为空**(计数在 stderr) |
|
||||||
|
|||||||
@@ -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|add-secret|get|get-credential|list|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
|
||||||
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
|
||||||
@@ -23,6 +23,7 @@ from service.account_crypto_commands import (
|
|||||||
from service.account_service import (
|
from service.account_service import (
|
||||||
cmd_account_add_secret,
|
cmd_account_add_secret,
|
||||||
cmd_account_add_web,
|
cmd_account_add_web,
|
||||||
|
cmd_account_ensure_web,
|
||||||
cmd_account_get,
|
cmd_account_get,
|
||||||
cmd_account_get_credential,
|
cmd_account_get_credential,
|
||||||
cmd_account_list,
|
cmd_account_list,
|
||||||
@@ -30,6 +31,7 @@ from service.account_service import (
|
|||||||
cmd_account_mark_session,
|
cmd_account_mark_session,
|
||||||
cmd_account_mark_used,
|
cmd_account_mark_used,
|
||||||
cmd_account_pick_web,
|
cmd_account_pick_web,
|
||||||
|
cmd_account_resolve,
|
||||||
cmd_credential_pick,
|
cmd_credential_pick,
|
||||||
cmd_credential_resolve,
|
cmd_credential_resolve,
|
||||||
cmd_delete_by_id,
|
cmd_delete_by_id,
|
||||||
@@ -77,11 +79,23 @@ _USAGE = """account-manager v2 — Credential & Browser Profile Manager
|
|||||||
[--environment <env>] [--role <role>]
|
[--environment <env>] [--role <role>]
|
||||||
account get <id> [--with-credentials]
|
account get <id> [--with-credentials]
|
||||||
account get-credential <account_id> [--reveal --lease-token <token>]
|
account get-credential <account_id> [--reveal --lease-token <token>]
|
||||||
|
account resolve [--id <id>] [--platform <p>] [--login-id <手机号/用户名>]
|
||||||
|
[--label <备注>] [--query <id或login_id或备注>]
|
||||||
|
# 用人话定位一条账号(成功输出同 account get)
|
||||||
account list [--platform <p>] [--environment <env>]
|
account list [--platform <p>] [--environment <env>]
|
||||||
[--tenant-id <t>] [--role <role>] [--limit N]
|
[--tenant-id <t>] [--role <role>]
|
||||||
|
[--login-id <id>] [--label <label>] [--limit N]
|
||||||
account pick-web --platform <p> [--environment <env>] [--tenant-id <t>]
|
account pick-web --platform <p> [--environment <env>] [--tenant-id <t>]
|
||||||
[--role <role>] [--lease] [--ttl-sec 900]
|
[--role <role>] [--login-id <手机号/用户名>] [--label <备注>]
|
||||||
|
[--lease] [--ttl-sec 900]
|
||||||
[--holder <holder>] [--purpose <purpose>]
|
[--holder <holder>] [--purpose <purpose>]
|
||||||
|
[--ensure] # 同 ensure-web:无账号时自动登记再 pick
|
||||||
|
account ensure-web --platform <p> [--login-id <手机号/用户名>] [--label <备注>]
|
||||||
|
[--environment <env>] [--tenant-id <t>] [--role <role>]
|
||||||
|
[--lease] [--ttl-sec 900] [--holder <h>] [--purpose <p>]
|
||||||
|
# 有账号则 pick;零账号则按参数 add-web 后再 pick
|
||||||
|
# 有账号但均被租约占用时不新建,返回 LEASE_CONFLICT
|
||||||
|
# 指定 --login-id/--label 时按该人读标识筛选/创建
|
||||||
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>
|
||||||
@@ -246,12 +260,14 @@ def main() -> None:
|
|||||||
# ---- account subcommand ----
|
# ---- account subcommand ----
|
||||||
elif cmd == "account":
|
elif cmd == "account":
|
||||||
if len(av) < 3:
|
if len(av) < 3:
|
||||||
print("ERROR:CLI_BAD_ARGS account 需要子命令(add-web / get / …)。")
|
print("ERROR:CLI_BAD_ARGS account 需要子命令(add-web / ensure-web / get / …)。")
|
||||||
print(_USAGE)
|
print(_USAGE)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sub = av[2]
|
sub = av[2]
|
||||||
if sub == "add-web":
|
if sub == "add-web":
|
||||||
_cmd_add_web(av)
|
_cmd_add_web(av)
|
||||||
|
elif sub == "ensure-web":
|
||||||
|
_cmd_ensure_web(av)
|
||||||
elif sub == "add-secret":
|
elif sub == "add-secret":
|
||||||
_cmd_add_secret(av)
|
_cmd_add_secret(av)
|
||||||
elif sub == "get-credential":
|
elif sub == "get-credential":
|
||||||
@@ -268,6 +284,8 @@ def main() -> None:
|
|||||||
cmd_account_get(aid, include_credentials=with_creds)
|
cmd_account_get(aid, include_credentials=with_creds)
|
||||||
elif sub == "list":
|
elif sub == "list":
|
||||||
_cmd_account_list(av)
|
_cmd_account_list(av)
|
||||||
|
elif sub == "resolve":
|
||||||
|
_cmd_account_resolve(av)
|
||||||
elif sub == "pick-web":
|
elif sub == "pick-web":
|
||||||
_cmd_pick_web_new(av)
|
_cmd_pick_web_new(av)
|
||||||
elif sub == "mark-session":
|
elif sub == "mark-session":
|
||||||
@@ -524,35 +542,112 @@ def _cmd_account_list(argv: list[str]) -> None:
|
|||||||
env = _get_opt(argv, "--environment")
|
env = _get_opt(argv, "--environment")
|
||||||
tenant = _get_opt(argv, "--tenant-id")
|
tenant = _get_opt(argv, "--tenant-id")
|
||||||
role = _get_opt(argv, "--role")
|
role = _get_opt(argv, "--role")
|
||||||
|
login_id = _get_opt(argv, "--login-id")
|
||||||
|
label = _get_opt(argv, "--label")
|
||||||
limit = _get_int_opt(argv, "--limit", 200)
|
limit = _get_int_opt(argv, "--limit", 200)
|
||||||
cmd_account_list(
|
cmd_account_list(
|
||||||
platform_input=plat,
|
platform_input=plat,
|
||||||
environment=env,
|
environment=env,
|
||||||
tenant_id=tenant,
|
tenant_id=tenant,
|
||||||
role=role,
|
role=role,
|
||||||
|
login_id=login_id,
|
||||||
|
label=label,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_account_resolve(argv: list[str]) -> None:
|
||||||
|
cmd_account_resolve(
|
||||||
|
account_id=_get_opt(argv, "--id"),
|
||||||
|
platform_input=_get_opt(argv, "--platform"),
|
||||||
|
login_id=_get_opt(argv, "--login-id"),
|
||||||
|
label=_get_opt(argv, "--label"),
|
||||||
|
query=_get_opt(argv, "--query"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _cmd_pick_web_new(argv: list[str]) -> None:
|
def _cmd_pick_web_new(argv: list[str]) -> None:
|
||||||
plat = _get_opt(argv, "--platform", "")
|
plat = _get_opt(argv, "--platform", "")
|
||||||
env = _get_opt(argv, "--environment")
|
env = _get_opt(argv, "--environment")
|
||||||
tenant = _get_opt(argv, "--tenant-id")
|
tenant = _get_opt(argv, "--tenant-id")
|
||||||
role = _get_opt(argv, "--role")
|
role = _get_opt(argv, "--role")
|
||||||
|
login_id = _get_opt(argv, "--login-id")
|
||||||
|
label = _get_opt(argv, "--label")
|
||||||
do_lease = _has_flag(argv, "--lease")
|
do_lease = _has_flag(argv, "--lease")
|
||||||
ttl = _get_int_opt(argv, "--ttl-sec", 900)
|
ttl = _get_int_opt(argv, "--ttl-sec", 900)
|
||||||
holder = _get_opt(argv, "--holder")
|
holder = _get_opt(argv, "--holder")
|
||||||
purpose = _get_opt(argv, "--purpose")
|
purpose = _get_opt(argv, "--purpose")
|
||||||
|
do_ensure = _has_flag(argv, "--ensure")
|
||||||
|
|
||||||
if not plat:
|
if not plat:
|
||||||
print("ERROR:CLI_BAD_ARGS account pick-web 需要 --platform <platform>。")
|
print("ERROR:CLI_BAD_ARGS account pick-web 需要 --platform <platform>。")
|
||||||
return
|
return
|
||||||
platform_meta = _parse_platform_caller_meta(argv)
|
platform_meta = _parse_platform_caller_meta(argv)
|
||||||
|
if do_ensure:
|
||||||
|
_cmd_ensure_web(argv)
|
||||||
|
return
|
||||||
cmd_account_pick_web(
|
cmd_account_pick_web(
|
||||||
platform_input=plat,
|
platform_input=plat,
|
||||||
environment=env,
|
environment=env,
|
||||||
tenant_id=tenant,
|
tenant_id=tenant,
|
||||||
role=role,
|
role=role,
|
||||||
|
login_id=login_id,
|
||||||
|
label=label,
|
||||||
|
do_lease=do_lease,
|
||||||
|
ttl_sec=ttl,
|
||||||
|
holder=holder,
|
||||||
|
purpose=purpose,
|
||||||
|
platform_meta=platform_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_ensure_web(argv: list[str]) -> None:
|
||||||
|
plat = _get_opt(argv, "--platform", "")
|
||||||
|
login_id = _get_opt(argv, "--login-id")
|
||||||
|
login_id_type = _get_opt(argv, "--login-id-type", "unknown")
|
||||||
|
label = _get_opt(argv, "--label")
|
||||||
|
tenant_id = _get_opt(argv, "--tenant-id")
|
||||||
|
environment = _get_opt(argv, "--environment")
|
||||||
|
role = _get_opt(argv, "--role")
|
||||||
|
provider_code = _get_opt(argv, "--provider-code")
|
||||||
|
url = _get_opt(argv, "--url")
|
||||||
|
extra_json_str = _get_opt(argv, "--extra-json")
|
||||||
|
profile_dir_override = _get_opt(argv, "--profile-dir")
|
||||||
|
auth_strategy = _get_opt(argv, "--auth-strategy")
|
||||||
|
session_persistent_raw = _get_opt(argv, "--session-persistent")
|
||||||
|
device_fingerprint = _get_opt(argv, "--device-fingerprint")
|
||||||
|
do_lease = _has_flag(argv, "--lease")
|
||||||
|
ttl = _get_int_opt(argv, "--ttl-sec", 900)
|
||||||
|
holder = _get_opt(argv, "--holder")
|
||||||
|
purpose = _get_opt(argv, "--purpose")
|
||||||
|
|
||||||
|
session_persistent: Optional[int] = None
|
||||||
|
if session_persistent_raw is not None:
|
||||||
|
try:
|
||||||
|
session_persistent = int(session_persistent_raw)
|
||||||
|
except ValueError:
|
||||||
|
print("ERROR:CLI_BAD_ARGS --session-persistent 必须是整数(0 或 1)。")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not plat:
|
||||||
|
print("ERROR:CLI_BAD_ARGS account ensure-web 需要 --platform <platform>。")
|
||||||
|
return
|
||||||
|
platform_meta = _parse_platform_caller_meta(argv)
|
||||||
|
cmd_account_ensure_web(
|
||||||
|
platform_input=plat,
|
||||||
|
login_id=login_id,
|
||||||
|
login_id_type=login_id_type or "unknown",
|
||||||
|
label=label,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
environment=environment,
|
||||||
|
role=role,
|
||||||
|
provider_code=provider_code,
|
||||||
|
url=url,
|
||||||
|
extra_json_str=extra_json_str,
|
||||||
|
profile_dir_override=profile_dir_override,
|
||||||
|
auth_strategy=auth_strategy,
|
||||||
|
session_persistent=session_persistent,
|
||||||
|
device_fingerprint=device_fingerprint,
|
||||||
do_lease=do_lease,
|
do_lease=do_lease,
|
||||||
ttl_sec=ttl,
|
ttl_sec=ttl,
|
||||||
holder=holder,
|
holder=holder,
|
||||||
|
|||||||
@@ -229,6 +229,26 @@ def get_account_by_id(account_id) -> Optional[dict]:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _append_human_ref_filters(
|
||||||
|
conditions: list[str],
|
||||||
|
params: list,
|
||||||
|
*,
|
||||||
|
login_id: Optional[str] = None,
|
||||||
|
label: Optional[str] = None,
|
||||||
|
table_alias: str = "",
|
||||||
|
) -> None:
|
||||||
|
"""Optional case-insensitive exact match on login_id / account_label."""
|
||||||
|
prefix = f"{table_alias}." if table_alias else ""
|
||||||
|
lid = (login_id or "").strip()
|
||||||
|
if lid:
|
||||||
|
conditions.append(f"LOWER({prefix}login_id) = LOWER(?)")
|
||||||
|
params.append(lid)
|
||||||
|
lab = (label or "").strip()
|
||||||
|
if lab:
|
||||||
|
conditions.append(f"LOWER({prefix}account_label) = LOWER(?)")
|
||||||
|
params.append(lab)
|
||||||
|
|
||||||
|
|
||||||
def find_accounts(
|
def find_accounts(
|
||||||
platform_key: Optional[str] = None,
|
platform_key: Optional[str] = None,
|
||||||
environment: Optional[str] = None,
|
environment: Optional[str] = None,
|
||||||
@@ -236,6 +256,8 @@ def find_accounts(
|
|||||||
role: Optional[str] = None,
|
role: Optional[str] = None,
|
||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
provider_code: Optional[str] = None,
|
provider_code: Optional[str] = None,
|
||||||
|
login_id: Optional[str] = None,
|
||||||
|
label: Optional[str] = None,
|
||||||
limit: int = 200,
|
limit: int = 200,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Find accounts by optional filters. Returns list of dicts."""
|
"""Find accounts by optional filters. Returns list of dicts."""
|
||||||
@@ -263,6 +285,9 @@ def find_accounts(
|
|||||||
if provider_code:
|
if provider_code:
|
||||||
conditions.append("provider_code = ?")
|
conditions.append("provider_code = ?")
|
||||||
params.append(provider_code)
|
params.append(provider_code)
|
||||||
|
_append_human_ref_filters(
|
||||||
|
conditions, params, login_id=login_id, label=label
|
||||||
|
)
|
||||||
|
|
||||||
where = ""
|
where = ""
|
||||||
if conditions:
|
if conditions:
|
||||||
@@ -284,11 +309,48 @@ def find_accounts(
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def count_active_accounts(
|
||||||
|
platform_key: str,
|
||||||
|
environment: Optional[str] = None,
|
||||||
|
tenant_id: Optional[str] = None,
|
||||||
|
role: Optional[str] = None,
|
||||||
|
login_id: Optional[str] = None,
|
||||||
|
label: Optional[str] = None,
|
||||||
|
) -> int:
|
||||||
|
"""Count active accounts matching filters (ignores lease occupancy)."""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
conn.row_factory = None
|
||||||
|
cur = conn.cursor()
|
||||||
|
conditions = ["status = 'active'", "platform_key = ?"]
|
||||||
|
params: list = [platform_key]
|
||||||
|
if environment:
|
||||||
|
conditions.append("environment = ?")
|
||||||
|
params.append(environment)
|
||||||
|
if tenant_id:
|
||||||
|
conditions.append("tenant_id = ?")
|
||||||
|
params.append(tenant_id)
|
||||||
|
if role:
|
||||||
|
conditions.append("role = ?")
|
||||||
|
params.append(role)
|
||||||
|
_append_human_ref_filters(
|
||||||
|
conditions, params, login_id=login_id, label=label
|
||||||
|
)
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM accounts WHERE {where}", params)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return int(row[0] or 0) if row else 0
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def find_account_ids(
|
def find_account_ids(
|
||||||
platform_key: str,
|
platform_key: str,
|
||||||
environment: Optional[str] = None,
|
environment: Optional[str] = None,
|
||||||
tenant_id: Optional[str] = None,
|
tenant_id: Optional[str] = None,
|
||||||
role: Optional[str] = None,
|
role: Optional[str] = None,
|
||||||
|
login_id: Optional[str] = None,
|
||||||
|
label: Optional[str] = None,
|
||||||
) -> list[int]:
|
) -> list[int]:
|
||||||
"""Return account IDs matching filters, excluding those with active leases."""
|
"""Return account IDs matching filters, excluding those with active leases."""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
@@ -306,6 +368,9 @@ def find_account_ids(
|
|||||||
if role:
|
if role:
|
||||||
conditions.append("a.role = ?")
|
conditions.append("a.role = ?")
|
||||||
params.append(role)
|
params.append(role)
|
||||||
|
_append_human_ref_filters(
|
||||||
|
conditions, params, login_id=login_id, label=label, table_alias="a"
|
||||||
|
)
|
||||||
|
|
||||||
# Exclude accounts with active non-expired leases
|
# Exclude accounts with active non-expired leases
|
||||||
now = _now_unix()
|
now = _now_unix()
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ from util.profile_shortcut import create_profile_browser_shortcut
|
|||||||
from util.runtime_paths import get_default_profile_dir_for_web
|
from util.runtime_paths import get_default_profile_dir_for_web
|
||||||
|
|
||||||
|
|
||||||
def cmd_account_add_web(
|
def create_web_account_record(
|
||||||
platform_input: str,
|
platform_input: str,
|
||||||
login_id: Optional[str],
|
login_id: Optional[str],
|
||||||
login_id_type: str,
|
login_id_type: str,
|
||||||
@@ -59,15 +59,16 @@ def cmd_account_add_web(
|
|||||||
session_persistent: Optional[int] = None,
|
session_persistent: Optional[int] = None,
|
||||||
device_fingerprint: Optional[str] = None,
|
device_fingerprint: Optional[str] = None,
|
||||||
platform_meta: Optional[PlatformCallerMeta] = None,
|
platform_meta: Optional[PlatformCallerMeta] = None,
|
||||||
) -> None:
|
platform_key: Optional[str] = None,
|
||||||
"""account add-web — create a web/RPA account record."""
|
) -> Optional[dict]:
|
||||||
|
"""Create a web/RPA account. On error prints JSON and returns None; on success returns payload (no stdout)."""
|
||||||
log = get_skill_logger()
|
log = get_skill_logger()
|
||||||
log.info("account_add_web_attempt platform=%s login_id_type=%s env=%s role=%s",
|
log.info("account_add_web_attempt platform=%s login_id_type=%s env=%s role=%s",
|
||||||
platform_input, login_id_type, environment, role)
|
platform_input, login_id_type, environment, role)
|
||||||
|
|
||||||
key = _resolve_or_auto_register(platform_input, platform_meta)
|
key = platform_key or _resolve_or_auto_register(platform_input, platform_meta)
|
||||||
if not key:
|
if not key:
|
||||||
return
|
return None
|
||||||
|
|
||||||
init_db()
|
init_db()
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
@@ -85,7 +86,7 @@ def cmd_account_add_web(
|
|||||||
extra = json.loads(extra_json_str)
|
extra = json.loads(extra_json_str)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
_say(_err_json("ERROR:CLI_BAD_ARGS", "extra_json 不是合法的 JSON 字符串。"))
|
_say(_err_json("ERROR:CLI_BAD_ARGS", "extra_json 不是合法的 JSON 字符串。"))
|
||||||
return
|
return None
|
||||||
|
|
||||||
if profile_dir_override:
|
if profile_dir_override:
|
||||||
resolved_profile_dir = os.path.abspath(profile_dir_override)
|
resolved_profile_dir = os.path.abspath(profile_dir_override)
|
||||||
@@ -113,7 +114,7 @@ def cmd_account_add_web(
|
|||||||
"ERR_AUTH_STRATEGY_NOT_SUPPORTED",
|
"ERR_AUTH_STRATEGY_NOT_SUPPORTED",
|
||||||
f"不支持的 auth_strategy:{auth_strategy}",
|
f"不支持的 auth_strategy:{auth_strategy}",
|
||||||
))
|
))
|
||||||
return
|
return None
|
||||||
resolved_auth = auth_strategy
|
resolved_auth = auth_strategy
|
||||||
|
|
||||||
if session_persistent is None:
|
if session_persistent is None:
|
||||||
@@ -124,7 +125,7 @@ def cmd_account_add_web(
|
|||||||
"ERR_INVALID_SESSION_PERSISTENT",
|
"ERR_INVALID_SESSION_PERSISTENT",
|
||||||
"session_persistent 必须是 0 或 1。",
|
"session_persistent 必须是 0 或 1。",
|
||||||
))
|
))
|
||||||
return
|
return None
|
||||||
sp = session_persistent
|
sp = session_persistent
|
||||||
|
|
||||||
if device_fingerprint is not None and resolved_auth != AUTH_STRATEGY_DEVICE_BOUND:
|
if device_fingerprint is not None and resolved_auth != AUTH_STRATEGY_DEVICE_BOUND:
|
||||||
@@ -132,7 +133,7 @@ def cmd_account_add_web(
|
|||||||
"ERR_DEVICE_FINGERPRINT_NOT_APPLICABLE",
|
"ERR_DEVICE_FINGERPRINT_NOT_APPLICABLE",
|
||||||
"device_fingerprint 仅在 auth_strategy=device_bound 时可设置。",
|
"device_fingerprint 仅在 auth_strategy=device_bound 时可设置。",
|
||||||
))
|
))
|
||||||
return
|
return None
|
||||||
|
|
||||||
new_id = insert_account(
|
new_id = insert_account(
|
||||||
conn=conn,
|
conn=conn,
|
||||||
@@ -157,7 +158,7 @@ def cmd_account_add_web(
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
log.exception("account_add_web_failure")
|
log.exception("account_add_web_failure")
|
||||||
_say(_err_json("ERROR:DB_ERROR", "Database insert failed."))
|
_say(_err_json("ERROR:DB_ERROR", "Database insert failed."))
|
||||||
return
|
return None
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -182,7 +183,47 @@ def cmd_account_add_web(
|
|||||||
}
|
}
|
||||||
if shortcut_path:
|
if shortcut_path:
|
||||||
ok_payload["profile_shortcut"] = shortcut_path
|
ok_payload["profile_shortcut"] = shortcut_path
|
||||||
_say(_ok_json(ok_payload))
|
return ok_payload
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_account_add_web(
|
||||||
|
platform_input: str,
|
||||||
|
login_id: Optional[str],
|
||||||
|
login_id_type: str,
|
||||||
|
label: Optional[str],
|
||||||
|
tenant_id: Optional[str],
|
||||||
|
environment: str,
|
||||||
|
role: str,
|
||||||
|
provider_code: Optional[str],
|
||||||
|
url: Optional[str],
|
||||||
|
extra_json_str: Optional[str],
|
||||||
|
profile_dir_override: Optional[str],
|
||||||
|
*,
|
||||||
|
auth_strategy: Optional[str] = None,
|
||||||
|
session_persistent: Optional[int] = None,
|
||||||
|
device_fingerprint: Optional[str] = None,
|
||||||
|
platform_meta: Optional[PlatformCallerMeta] = None,
|
||||||
|
) -> None:
|
||||||
|
"""account add-web — create a web/RPA account record."""
|
||||||
|
ok_payload = create_web_account_record(
|
||||||
|
platform_input,
|
||||||
|
login_id,
|
||||||
|
login_id_type,
|
||||||
|
label,
|
||||||
|
tenant_id,
|
||||||
|
environment,
|
||||||
|
role,
|
||||||
|
provider_code,
|
||||||
|
url,
|
||||||
|
extra_json_str,
|
||||||
|
profile_dir_override,
|
||||||
|
auth_strategy=auth_strategy,
|
||||||
|
session_persistent=session_persistent,
|
||||||
|
device_fingerprint=device_fingerprint,
|
||||||
|
platform_meta=platform_meta,
|
||||||
|
)
|
||||||
|
if ok_payload:
|
||||||
|
_say(_ok_json(ok_payload))
|
||||||
|
|
||||||
|
|
||||||
def cmd_account_add_secret(
|
def cmd_account_add_secret(
|
||||||
|
|||||||
174
scripts/service/account_ensure_commands.py
Normal file
174
scripts/service/account_ensure_commands.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""account ensure-web — pick if present, otherwise add-web then pick."""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from db.accounts_repo import count_active_accounts
|
||||||
|
from db.connection import get_conn, init_db
|
||||||
|
from service._svc_common import PlatformCallerMeta, _err_json, _resolve_or_auto_register, _say
|
||||||
|
from service.account_add_commands import create_web_account_record
|
||||||
|
from service.account_query_commands import cmd_account_pick_web
|
||||||
|
from util.logging_config import get_skill_logger
|
||||||
|
from util.platforms import seed_platforms
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_account_ensure_web(
|
||||||
|
platform_input: str,
|
||||||
|
*,
|
||||||
|
login_id: Optional[str] = None,
|
||||||
|
login_id_type: str = "unknown",
|
||||||
|
label: Optional[str] = None,
|
||||||
|
tenant_id: Optional[str] = None,
|
||||||
|
environment: Optional[str] = None,
|
||||||
|
role: Optional[str] = None,
|
||||||
|
provider_code: Optional[str] = None,
|
||||||
|
url: Optional[str] = None,
|
||||||
|
extra_json_str: Optional[str] = None,
|
||||||
|
profile_dir_override: Optional[str] = None,
|
||||||
|
auth_strategy: Optional[str] = None,
|
||||||
|
session_persistent: Optional[int] = None,
|
||||||
|
device_fingerprint: Optional[str] = None,
|
||||||
|
do_lease: bool = False,
|
||||||
|
ttl_sec: int = 900,
|
||||||
|
holder: Optional[str] = None,
|
||||||
|
purpose: Optional[str] = None,
|
||||||
|
platform_meta: Optional[PlatformCallerMeta] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Pick a web account; if none exist for filters, create one then pick.
|
||||||
|
|
||||||
|
Does NOT create a new account when matching accounts exist but are all leased.
|
||||||
|
Success stdout matches ``account pick-web`` (plus ``account_ensured`` when created).
|
||||||
|
"""
|
||||||
|
log = get_skill_logger()
|
||||||
|
log.info(
|
||||||
|
"account_ensure_web_attempt platform=%s env=%s tenant=%s role=%s lease=%s",
|
||||||
|
platform_input,
|
||||||
|
environment,
|
||||||
|
tenant_id,
|
||||||
|
role,
|
||||||
|
do_lease,
|
||||||
|
)
|
||||||
|
|
||||||
|
key = _resolve_or_auto_register(platform_input, platform_meta)
|
||||||
|
if not key:
|
||||||
|
return
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
seed_platforms(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
lid = (login_id or "").strip() or None
|
||||||
|
lab = (label or "").strip() or None
|
||||||
|
|
||||||
|
existing_count = count_active_accounts(
|
||||||
|
platform_key=key,
|
||||||
|
environment=environment,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
role=role,
|
||||||
|
login_id=lid,
|
||||||
|
label=lab,
|
||||||
|
)
|
||||||
|
created = False
|
||||||
|
if existing_count == 0:
|
||||||
|
create_env = (environment or "").strip() or "production"
|
||||||
|
create_role = (role or "").strip() or "default"
|
||||||
|
create_label = lab or None
|
||||||
|
if not create_label:
|
||||||
|
create_label = f"{key} 默认"
|
||||||
|
if lid:
|
||||||
|
create_label = f"{key} {lid}"
|
||||||
|
|
||||||
|
created_payload = create_web_account_record(
|
||||||
|
platform_input,
|
||||||
|
lid,
|
||||||
|
login_id_type,
|
||||||
|
create_label,
|
||||||
|
tenant_id,
|
||||||
|
create_env,
|
||||||
|
create_role,
|
||||||
|
provider_code,
|
||||||
|
url,
|
||||||
|
extra_json_str,
|
||||||
|
profile_dir_override,
|
||||||
|
auth_strategy=auth_strategy,
|
||||||
|
session_persistent=session_persistent,
|
||||||
|
device_fingerprint=device_fingerprint,
|
||||||
|
platform_meta=platform_meta,
|
||||||
|
platform_key=key,
|
||||||
|
)
|
||||||
|
if not created_payload:
|
||||||
|
return
|
||||||
|
created = True
|
||||||
|
log.info(
|
||||||
|
"account_ensure_web_created id=%s platform=%s login_id=%s profile_dir=%s",
|
||||||
|
created_payload.get("id"),
|
||||||
|
key,
|
||||||
|
lid,
|
||||||
|
created_payload.get("profile_dir"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.info(
|
||||||
|
"account_ensure_web_reuse platform=%s existing_active=%s login_id=%s label=%s",
|
||||||
|
key,
|
||||||
|
existing_count,
|
||||||
|
lid,
|
||||||
|
lab,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Capture pick output so we can annotate / remap lease-busy errors.
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
buf = io.StringIO()
|
||||||
|
old_out = sys.stdout
|
||||||
|
sys.stdout = buf
|
||||||
|
try:
|
||||||
|
cmd_account_pick_web(
|
||||||
|
platform_input,
|
||||||
|
environment=environment,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
role=role,
|
||||||
|
login_id=lid,
|
||||||
|
label=lab,
|
||||||
|
do_lease=do_lease,
|
||||||
|
ttl_sec=ttl_sec,
|
||||||
|
holder=holder,
|
||||||
|
purpose=purpose,
|
||||||
|
platform_meta=platform_meta,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
sys.stdout = old_out
|
||||||
|
|
||||||
|
raw = buf.getvalue().strip()
|
||||||
|
if not raw:
|
||||||
|
_say(_err_json("ERROR:NO_ACCOUNT", "ensure 后未能挑选到账号。"))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
_say(raw)
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(payload, dict) and payload.get("success") is False:
|
||||||
|
err = (payload.get("error") or {}) if isinstance(payload.get("error"), dict) else {}
|
||||||
|
code = str(err.get("code") or "")
|
||||||
|
if code == "ERROR:NO_ACCOUNT" and existing_count > 0 and not created:
|
||||||
|
scope = "该筛选条件下" if (lid or lab) else "该平台下"
|
||||||
|
_say(_err_json(
|
||||||
|
"ERROR:LEASE_CONFLICT",
|
||||||
|
f"{scope}已有账号,但均被其他任务占用。请稍后重试或释放租约;不会自动新建账号。",
|
||||||
|
))
|
||||||
|
return
|
||||||
|
_say(raw)
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(payload, dict) and created:
|
||||||
|
payload["account_ensured"] = True
|
||||||
|
_say(json.dumps(payload, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
|
_say(raw)
|
||||||
@@ -193,12 +193,21 @@ def cmd_account_list(
|
|||||||
environment: Optional[str] = None,
|
environment: Optional[str] = None,
|
||||||
tenant_id: Optional[str] = None,
|
tenant_id: Optional[str] = None,
|
||||||
role: Optional[str] = None,
|
role: Optional[str] = None,
|
||||||
|
login_id: Optional[str] = None,
|
||||||
|
label: Optional[str] = None,
|
||||||
limit: int = 200,
|
limit: int = 200,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""account list — output JSON array of accounts matching filters."""
|
"""account list — output JSON array of accounts matching filters."""
|
||||||
log = get_skill_logger()
|
log = get_skill_logger()
|
||||||
log.info("cli_start subcommand=account_list platform=%s env=%s tenant=%s role=%s",
|
log.info(
|
||||||
platform_input, environment, tenant_id, role)
|
"cli_start subcommand=account_list platform=%s env=%s tenant=%s role=%s login_id=%s label=%s",
|
||||||
|
platform_input,
|
||||||
|
environment,
|
||||||
|
tenant_id,
|
||||||
|
role,
|
||||||
|
login_id,
|
||||||
|
label,
|
||||||
|
)
|
||||||
init_db()
|
init_db()
|
||||||
key = None
|
key = None
|
||||||
if platform_input and platform_input not in ("all", "全部", ""):
|
if platform_input and platform_input not in ("all", "全部", ""):
|
||||||
@@ -211,11 +220,115 @@ def cmd_account_list(
|
|||||||
environment=environment,
|
environment=environment,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
role=role,
|
role=role,
|
||||||
|
login_id=login_id,
|
||||||
|
label=label,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
_say(json.dumps(accounts, ensure_ascii=False))
|
_say(json.dumps(accounts, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_account_resolve(
|
||||||
|
*,
|
||||||
|
account_id: Optional[str] = None,
|
||||||
|
platform_input: Optional[str] = None,
|
||||||
|
login_id: Optional[str] = None,
|
||||||
|
label: Optional[str] = None,
|
||||||
|
query: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Resolve one account by id, login_id, label, or free-form query.
|
||||||
|
|
||||||
|
Success: same JSON shape as ``account get``.
|
||||||
|
Errors: ACCOUNT_NOT_FOUND / NO_ACCOUNT / AMBIGUOUS_ACCOUNT / CLI_BAD_ARGS.
|
||||||
|
"""
|
||||||
|
log = get_skill_logger()
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
aid_raw = (account_id or "").strip()
|
||||||
|
q = (query or "").strip()
|
||||||
|
lid = (login_id or "").strip()
|
||||||
|
lab = (label or "").strip()
|
||||||
|
|
||||||
|
# Free-form: digits → id; otherwise treat as login_id (then label fallback).
|
||||||
|
if q and not aid_raw and not lid and not lab:
|
||||||
|
if q.isdigit():
|
||||||
|
aid_raw = q
|
||||||
|
else:
|
||||||
|
lid = q
|
||||||
|
|
||||||
|
if aid_raw:
|
||||||
|
try:
|
||||||
|
aid = int(aid_raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
_say(_err_json("ERROR:CLI_BAD_ARGS", f"账号 id 必须是整数:{aid_raw}"))
|
||||||
|
return
|
||||||
|
acc = get_account_by_id(aid)
|
||||||
|
if not acc:
|
||||||
|
_say(_err_json("ERROR:ACCOUNT_NOT_FOUND", f"账号 id={aid} 不存在。"))
|
||||||
|
return
|
||||||
|
log.info("account_resolve_by_id id=%s", aid)
|
||||||
|
_say(json.dumps(acc, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not lid and not lab:
|
||||||
|
_say(_err_json(
|
||||||
|
"ERROR:CLI_BAD_ARGS",
|
||||||
|
"account resolve 需要 --id、<数字 query>、--login-id 或 --label(也可用 --query 用户名/手机号)。",
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
|
key = None
|
||||||
|
if platform_input and platform_input not in ("all", "全部", ""):
|
||||||
|
key = _resolve_or_fail(platform_input)
|
||||||
|
if not key:
|
||||||
|
return
|
||||||
|
elif not platform_input:
|
||||||
|
# login_id/label without platform: search all platforms
|
||||||
|
key = None
|
||||||
|
|
||||||
|
matches = find_accounts(
|
||||||
|
platform_key=key,
|
||||||
|
login_id=lid or None,
|
||||||
|
label=lab or None,
|
||||||
|
status="active",
|
||||||
|
limit=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If query was used as login_id and miss, retry as label.
|
||||||
|
if not matches and q and not login_id and not label and not q.isdigit():
|
||||||
|
matches = find_accounts(
|
||||||
|
platform_key=key,
|
||||||
|
label=q,
|
||||||
|
status="active",
|
||||||
|
limit=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
ref = lid or lab or q
|
||||||
|
_say(_err_json(
|
||||||
|
"ERROR:NO_ACCOUNT",
|
||||||
|
f"未找到匹配的账号(ref={ref!r}"
|
||||||
|
+ (f", platform={key}" if key else "")
|
||||||
|
+ ")。",
|
||||||
|
))
|
||||||
|
return
|
||||||
|
if len(matches) > 1:
|
||||||
|
ids = [m.get("id") for m in matches[:10]]
|
||||||
|
_say(_err_json(
|
||||||
|
"ERROR:AMBIGUOUS_ACCOUNT",
|
||||||
|
f"匹配到多条账号,请改用 --id 或补充 --platform。候选 id={ids}",
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
|
acc = matches[0]
|
||||||
|
log.info(
|
||||||
|
"account_resolve_ok id=%s login_id=%s label=%s",
|
||||||
|
acc.get("id"),
|
||||||
|
acc.get("login_id"),
|
||||||
|
acc.get("account_label"),
|
||||||
|
)
|
||||||
|
_say(json.dumps(acc, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
def cmd_list_json(platform_input: str, limit: int = 200) -> None:
|
def cmd_list_json(platform_input: str, limit: int = 200) -> None:
|
||||||
"""Legacy: list-json <platform|all> [--limit N]"""
|
"""Legacy: list-json <platform|all> [--limit N]"""
|
||||||
log = get_skill_logger()
|
log = get_skill_logger()
|
||||||
@@ -238,6 +351,8 @@ def cmd_account_pick_web(
|
|||||||
environment: Optional[str] = None,
|
environment: Optional[str] = None,
|
||||||
tenant_id: Optional[str] = None,
|
tenant_id: Optional[str] = None,
|
||||||
role: Optional[str] = None,
|
role: Optional[str] = None,
|
||||||
|
login_id: Optional[str] = None,
|
||||||
|
label: Optional[str] = None,
|
||||||
do_lease: bool = False,
|
do_lease: bool = False,
|
||||||
ttl_sec: int = 900,
|
ttl_sec: int = 900,
|
||||||
holder: Optional[str] = None,
|
holder: Optional[str] = None,
|
||||||
@@ -246,8 +361,16 @@ def cmd_account_pick_web(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Pick a web/RPA account for use. Optionally acquire a lease."""
|
"""Pick a web/RPA account for use. Optionally acquire a lease."""
|
||||||
log = get_skill_logger()
|
log = get_skill_logger()
|
||||||
log.info("account_pick_web_attempt platform=%s env=%s tenant=%s role=%s lease=%s",
|
log.info(
|
||||||
platform_input, environment, tenant_id, role, do_lease)
|
"account_pick_web_attempt platform=%s env=%s tenant=%s role=%s login_id=%s label=%s lease=%s",
|
||||||
|
platform_input,
|
||||||
|
environment,
|
||||||
|
tenant_id,
|
||||||
|
role,
|
||||||
|
login_id,
|
||||||
|
label,
|
||||||
|
do_lease,
|
||||||
|
)
|
||||||
|
|
||||||
key = _resolve_or_auto_register(platform_input, platform_meta)
|
key = _resolve_or_auto_register(platform_input, platform_meta)
|
||||||
if not key:
|
if not key:
|
||||||
@@ -260,15 +383,45 @@ def cmd_account_pick_web(
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
lid = (login_id or "").strip() or None
|
||||||
|
lab = (label or "").strip() or None
|
||||||
|
|
||||||
|
# Human-ref filters should be unique; ambiguous active matches → explicit error.
|
||||||
|
if lid or lab:
|
||||||
|
active_matches = find_accounts(
|
||||||
|
platform_key=key,
|
||||||
|
environment=environment,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
role=role,
|
||||||
|
status="active",
|
||||||
|
login_id=lid,
|
||||||
|
label=lab,
|
||||||
|
limit=20,
|
||||||
|
)
|
||||||
|
if len(active_matches) > 1:
|
||||||
|
ids = [m.get("id") for m in active_matches[:10]]
|
||||||
|
_say(_err_json(
|
||||||
|
"ERROR:AMBIGUOUS_ACCOUNT",
|
||||||
|
f"login_id/label 匹配到多条账号,请改用 account get <id>。候选 id={ids}",
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
candidate_ids = find_account_ids(
|
candidate_ids = find_account_ids(
|
||||||
platform_key=key,
|
platform_key=key,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
role=role,
|
role=role,
|
||||||
|
login_id=lid,
|
||||||
|
label=lab,
|
||||||
)
|
)
|
||||||
if not candidate_ids:
|
if not candidate_ids:
|
||||||
_say(_err_json("ERROR:NO_ACCOUNT",
|
hint = ""
|
||||||
"没有符合条件的可用账号(status=active 且未被其他 lease 占用)。"))
|
if lid or lab:
|
||||||
|
hint = f"(login_id={lid!r} label={lab!r})"
|
||||||
|
_say(_err_json(
|
||||||
|
"ERROR:NO_ACCOUNT",
|
||||||
|
f"没有符合条件的可用账号{hint}(status=active 且未被其他 lease 占用)。",
|
||||||
|
))
|
||||||
return
|
return
|
||||||
|
|
||||||
picked_id = candidate_ids[0]
|
picked_id = candidate_ids[0]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ PyArmor trial per-file line limits. This module re-exports the public API.
|
|||||||
"""
|
"""
|
||||||
from service._svc_common import PlatformCallerMeta
|
from service._svc_common import PlatformCallerMeta
|
||||||
from service.account_add_commands import cmd_account_add_secret, cmd_account_add_web
|
from service.account_add_commands import cmd_account_add_secret, cmd_account_add_web
|
||||||
|
from service.account_ensure_commands import cmd_account_ensure_web
|
||||||
from service.account_mark_commands import (
|
from service.account_mark_commands import (
|
||||||
cmd_account_mark_error,
|
cmd_account_mark_error,
|
||||||
cmd_account_mark_session,
|
cmd_account_mark_session,
|
||||||
@@ -25,6 +26,7 @@ from service.account_query_commands import (
|
|||||||
cmd_account_get_credential,
|
cmd_account_get_credential,
|
||||||
cmd_account_list,
|
cmd_account_list,
|
||||||
cmd_account_pick_web,
|
cmd_account_pick_web,
|
||||||
|
cmd_account_resolve,
|
||||||
cmd_get,
|
cmd_get,
|
||||||
cmd_list_json,
|
cmd_list_json,
|
||||||
cmd_pick_web,
|
cmd_pick_web,
|
||||||
@@ -43,6 +45,7 @@ __all__ = [
|
|||||||
"PlatformCallerMeta",
|
"PlatformCallerMeta",
|
||||||
"cmd_account_add_secret",
|
"cmd_account_add_secret",
|
||||||
"cmd_account_add_web",
|
"cmd_account_add_web",
|
||||||
|
"cmd_account_ensure_web",
|
||||||
"cmd_account_get",
|
"cmd_account_get",
|
||||||
"cmd_account_get_credential",
|
"cmd_account_get_credential",
|
||||||
"cmd_account_list",
|
"cmd_account_list",
|
||||||
@@ -50,6 +53,7 @@ __all__ = [
|
|||||||
"cmd_account_mark_session",
|
"cmd_account_mark_session",
|
||||||
"cmd_account_mark_used",
|
"cmd_account_mark_used",
|
||||||
"cmd_account_pick_web",
|
"cmd_account_pick_web",
|
||||||
|
"cmd_account_resolve",
|
||||||
"cmd_credential_pick",
|
"cmd_credential_pick",
|
||||||
"cmd_credential_resolve",
|
"cmd_credential_resolve",
|
||||||
"cmd_delete_by_id",
|
"cmd_delete_by_id",
|
||||||
|
|||||||
119
tests/test_account_resolve_pick.py
Normal file
119
tests/test_account_resolve_pick.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Human-friendly account resolve / pick-web --login-id / ensure by login_id."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _capture(fn, *args, **kwargs):
|
||||||
|
buf = io.StringIO()
|
||||||
|
old = sys.stdout
|
||||||
|
sys.stdout = buf
|
||||||
|
try:
|
||||||
|
fn(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
sys.stdout = old
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _add_web(platform, login_id, label):
|
||||||
|
from service.account_service import cmd_account_add_web
|
||||||
|
|
||||||
|
payload = json.loads(
|
||||||
|
_capture(
|
||||||
|
cmd_account_add_web,
|
||||||
|
platform,
|
||||||
|
login_id,
|
||||||
|
"username",
|
||||||
|
label,
|
||||||
|
None,
|
||||||
|
"production",
|
||||||
|
"default",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if isinstance(payload, dict) and isinstance(payload.get("data"), dict):
|
||||||
|
return payload["data"]
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccountResolve:
|
||||||
|
def test_resolve_by_login_id(self, clean_db):
|
||||||
|
from service.account_service import cmd_account_resolve
|
||||||
|
|
||||||
|
created = _add_web("maersk", "chendelian", "陈德联")
|
||||||
|
raw = _capture(
|
||||||
|
cmd_account_resolve,
|
||||||
|
platform_input="maersk",
|
||||||
|
login_id="ChenDelian",
|
||||||
|
)
|
||||||
|
acc = json.loads(raw)
|
||||||
|
assert acc.get("success") is not False
|
||||||
|
assert acc["id"] == created["id"]
|
||||||
|
assert acc["login_id"] == "chendelian"
|
||||||
|
|
||||||
|
def test_resolve_by_query_fallback_label(self, clean_db):
|
||||||
|
from service.account_service import cmd_account_resolve
|
||||||
|
|
||||||
|
created = _add_web("maersk", "u1", "我的马士基")
|
||||||
|
raw = _capture(
|
||||||
|
cmd_account_resolve,
|
||||||
|
platform_input="maersk",
|
||||||
|
query="我的马士基",
|
||||||
|
)
|
||||||
|
acc = json.loads(raw)
|
||||||
|
assert acc["id"] == created["id"]
|
||||||
|
|
||||||
|
def test_resolve_ambiguous(self, clean_db):
|
||||||
|
from service.account_service import cmd_account_resolve
|
||||||
|
|
||||||
|
_add_web("maersk", "same", "A")
|
||||||
|
_add_web("cma_cgm", "same", "B")
|
||||||
|
raw = _capture(cmd_account_resolve, login_id="same")
|
||||||
|
err = json.loads(raw)
|
||||||
|
assert err["success"] is False
|
||||||
|
assert err["error"]["code"] == "ERROR:AMBIGUOUS_ACCOUNT"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPickByLoginId:
|
||||||
|
def test_pick_web_filters_login_id(self, clean_db):
|
||||||
|
from service.account_service import cmd_account_pick_web
|
||||||
|
|
||||||
|
a = _add_web("maersk", "alice", "A")
|
||||||
|
_add_web("maersk", "bob", "B")
|
||||||
|
raw = _capture(
|
||||||
|
cmd_account_pick_web,
|
||||||
|
"maersk",
|
||||||
|
login_id="bob",
|
||||||
|
)
|
||||||
|
picked = json.loads(raw)
|
||||||
|
assert picked["login_id"] == "bob"
|
||||||
|
assert picked["id"] != a["id"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnsureByLoginId:
|
||||||
|
def test_ensure_creates_named_even_if_platform_has_others(self, clean_db):
|
||||||
|
from db.accounts_repo import find_accounts
|
||||||
|
from service.account_service import cmd_account_ensure_web
|
||||||
|
|
||||||
|
_add_web("maersk", "existing", "已有")
|
||||||
|
before = find_accounts(platform_key="maersk", status="active", limit=50)
|
||||||
|
assert len(before) == 1
|
||||||
|
|
||||||
|
raw = _capture(
|
||||||
|
cmd_account_ensure_web,
|
||||||
|
"maersk",
|
||||||
|
login_id="chendelian",
|
||||||
|
label="陈德联",
|
||||||
|
)
|
||||||
|
result = json.loads(raw)
|
||||||
|
assert result.get("account_ensured") is True
|
||||||
|
assert result["login_id"] == "chendelian"
|
||||||
|
|
||||||
|
after = find_accounts(platform_key="maersk", status="active", limit=50)
|
||||||
|
assert len(after) == 2
|
||||||
143
tests/test_ensure_web.py
Normal file
143
tests/test_ensure_web.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Tests for account ensure-web (pick-or-create)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def _capture(fn, *args, **kwargs):
|
||||||
|
buf = io.StringIO()
|
||||||
|
old = sys.stdout
|
||||||
|
sys.stdout = buf
|
||||||
|
try:
|
||||||
|
fn(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
sys.stdout = old
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnsureWeb:
|
||||||
|
def test_ensure_creates_when_empty(self, clean_db):
|
||||||
|
from service.account_service import cmd_account_ensure_web
|
||||||
|
|
||||||
|
raw = _capture(
|
||||||
|
cmd_account_ensure_web,
|
||||||
|
"maersk",
|
||||||
|
label="马士基默认",
|
||||||
|
)
|
||||||
|
result = json.loads(raw)
|
||||||
|
assert result.get("success") is not False
|
||||||
|
assert result["platform_key"] == "maersk"
|
||||||
|
assert result.get("profile_dir")
|
||||||
|
assert result.get("account_ensured") is True
|
||||||
|
assert result.get("id")
|
||||||
|
|
||||||
|
def test_ensure_reuses_existing_no_duplicate(self, clean_db):
|
||||||
|
from service.account_service import cmd_account_add_web, cmd_account_ensure_web
|
||||||
|
from db.accounts_repo import find_accounts
|
||||||
|
|
||||||
|
_capture(
|
||||||
|
cmd_account_add_web,
|
||||||
|
"maersk",
|
||||||
|
"a@test.com",
|
||||||
|
"email",
|
||||||
|
"已有",
|
||||||
|
None,
|
||||||
|
"production",
|
||||||
|
"default",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
before = find_accounts(platform_key="maersk", status="active", limit=50)
|
||||||
|
assert len(before) == 1
|
||||||
|
|
||||||
|
raw = _capture(cmd_account_ensure_web, "maersk")
|
||||||
|
result = json.loads(raw)
|
||||||
|
assert result.get("account_ensured") is not True
|
||||||
|
assert result["id"] == before[0]["id"]
|
||||||
|
|
||||||
|
after = find_accounts(platform_key="maersk", status="active", limit=50)
|
||||||
|
assert len(after) == 1
|
||||||
|
|
||||||
|
def test_ensure_lease_busy_does_not_create(self, clean_db):
|
||||||
|
from db.accounts_repo import acquire_lease, find_account_ids, find_accounts
|
||||||
|
from service.account_service import cmd_account_add_web, cmd_account_ensure_web
|
||||||
|
|
||||||
|
_capture(
|
||||||
|
cmd_account_add_web,
|
||||||
|
"cma_cgm",
|
||||||
|
"b@cma.com",
|
||||||
|
"email",
|
||||||
|
"CMA",
|
||||||
|
None,
|
||||||
|
"production",
|
||||||
|
"default",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
ids = find_account_ids("cma_cgm")
|
||||||
|
assert len(ids) == 1
|
||||||
|
acquire_lease(
|
||||||
|
account_id=ids[0],
|
||||||
|
lease_token="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||||
|
holder="other",
|
||||||
|
purpose="busy",
|
||||||
|
ttl_sec=900,
|
||||||
|
)
|
||||||
|
assert find_account_ids("cma_cgm") == []
|
||||||
|
|
||||||
|
raw = _capture(cmd_account_ensure_web, "cma_cgm", do_lease=True, holder="me")
|
||||||
|
err = json.loads(raw)
|
||||||
|
assert err["success"] is False
|
||||||
|
assert err["error"]["code"] == "ERROR:LEASE_CONFLICT"
|
||||||
|
|
||||||
|
still = find_accounts(platform_key="cma_cgm", status="active", limit=50)
|
||||||
|
assert len(still) == 1
|
||||||
|
|
||||||
|
def test_pick_web_ensure_flag_cli(self, clean_db, fake_env):
|
||||||
|
"""CLI account pick-web --ensure creates when empty."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
scripts = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts"
|
||||||
|
)
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
"PYTHONPATH": scripts,
|
||||||
|
"JIANGCHANG_DATA_ROOT": fake_env["data_root"],
|
||||||
|
"JIANGCHANG_USER_ID": fake_env["user_id"],
|
||||||
|
}
|
||||||
|
r = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"cli.app",
|
||||||
|
"account",
|
||||||
|
"pick-web",
|
||||||
|
"--platform",
|
||||||
|
"maersk",
|
||||||
|
"--ensure",
|
||||||
|
"--label",
|
||||||
|
"cli-default",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
cwd=scripts,
|
||||||
|
)
|
||||||
|
assert r.returncode == 0, r.stderr + r.stdout
|
||||||
|
# last non-empty JSON line
|
||||||
|
lines = [ln for ln in r.stdout.splitlines() if ln.strip()]
|
||||||
|
result = json.loads(lines[-1])
|
||||||
|
assert result.get("platform_key") == "maersk"
|
||||||
|
assert result.get("account_ensured") is True
|
||||||
|
assert result.get("profile_dir")
|
||||||
@@ -27,7 +27,11 @@ def test_usage_includes_all_account_subcommands():
|
|||||||
"export-credentials",
|
"export-credentials",
|
||||||
"import-credentials",
|
"import-credentials",
|
||||||
"add-web",
|
"add-web",
|
||||||
|
"ensure-web",
|
||||||
"get-credential",
|
"get-credential",
|
||||||
"pick-web",
|
"pick-web",
|
||||||
|
"resolve",
|
||||||
):
|
):
|
||||||
assert cmd in usage, f"USAGE 缺少 {cmd}"
|
assert cmd in usage, f"USAGE 缺少 {cmd}"
|
||||||
|
assert "--ensure" in usage
|
||||||
|
assert "--login-id" in usage
|
||||||
|
|||||||
Reference in New Issue
Block a user