feat: resolve/pick accounts by login_id or label
All checks were successful
技能自动化发布 / release (push) Successful in 11s
All checks were successful
技能自动化发布 / release (push) Successful in 11s
This commit is contained in:
@@ -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|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
|
||||
credential pick|resolve
|
||||
lease release|list|cleanup
|
||||
@@ -31,6 +31,7 @@ from service.account_service import (
|
||||
cmd_account_mark_session,
|
||||
cmd_account_mark_used,
|
||||
cmd_account_pick_web,
|
||||
cmd_account_resolve,
|
||||
cmd_credential_pick,
|
||||
cmd_credential_resolve,
|
||||
cmd_delete_by_id,
|
||||
@@ -78,17 +79,23 @@ _USAGE = """account-manager v2 — Credential & Browser Profile Manager
|
||||
[--environment <env>] [--role <role>]
|
||||
account get <id> [--with-credentials]
|
||||
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>]
|
||||
[--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>]
|
||||
[--role <role>] [--lease] [--ttl-sec 900]
|
||||
[--role <role>] [--login-id <手机号/用户名>] [--label <备注>]
|
||||
[--lease] [--ttl-sec 900]
|
||||
[--holder <holder>] [--purpose <purpose>]
|
||||
[--ensure] # 同 ensure-web:无账号时自动登记再 pick
|
||||
account ensure-web --platform <p> [--login-id <id>] [--label <label>]
|
||||
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-error <id> --code <code> --message <msg>
|
||||
account mark-used <id>
|
||||
@@ -277,6 +284,8 @@ def main() -> None:
|
||||
cmd_account_get(aid, include_credentials=with_creds)
|
||||
elif sub == "list":
|
||||
_cmd_account_list(av)
|
||||
elif sub == "resolve":
|
||||
_cmd_account_resolve(av)
|
||||
elif sub == "pick-web":
|
||||
_cmd_pick_web_new(av)
|
||||
elif sub == "mark-session":
|
||||
@@ -533,21 +542,37 @@ def _cmd_account_list(argv: list[str]) -> None:
|
||||
env = _get_opt(argv, "--environment")
|
||||
tenant = _get_opt(argv, "--tenant-id")
|
||||
role = _get_opt(argv, "--role")
|
||||
login_id = _get_opt(argv, "--login-id")
|
||||
label = _get_opt(argv, "--label")
|
||||
limit = _get_int_opt(argv, "--limit", 200)
|
||||
cmd_account_list(
|
||||
platform_input=plat,
|
||||
environment=env,
|
||||
tenant_id=tenant,
|
||||
role=role,
|
||||
login_id=login_id,
|
||||
label=label,
|
||||
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:
|
||||
plat = _get_opt(argv, "--platform", "")
|
||||
env = _get_opt(argv, "--environment")
|
||||
tenant = _get_opt(argv, "--tenant-id")
|
||||
role = _get_opt(argv, "--role")
|
||||
login_id = _get_opt(argv, "--login-id")
|
||||
label = _get_opt(argv, "--label")
|
||||
do_lease = _has_flag(argv, "--lease")
|
||||
ttl = _get_int_opt(argv, "--ttl-sec", 900)
|
||||
holder = _get_opt(argv, "--holder")
|
||||
@@ -566,6 +591,8 @@ def _cmd_pick_web_new(argv: list[str]) -> None:
|
||||
environment=env,
|
||||
tenant_id=tenant,
|
||||
role=role,
|
||||
login_id=login_id,
|
||||
label=label,
|
||||
do_lease=do_lease,
|
||||
ttl_sec=ttl,
|
||||
holder=holder,
|
||||
|
||||
@@ -229,6 +229,26 @@ def get_account_by_id(account_id) -> Optional[dict]:
|
||||
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(
|
||||
platform_key: Optional[str] = None,
|
||||
environment: Optional[str] = None,
|
||||
@@ -236,6 +256,8 @@ def find_accounts(
|
||||
role: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
provider_code: Optional[str] = None,
|
||||
login_id: Optional[str] = None,
|
||||
label: Optional[str] = None,
|
||||
limit: int = 200,
|
||||
) -> list[dict]:
|
||||
"""Find accounts by optional filters. Returns list of dicts."""
|
||||
@@ -263,6 +285,9 @@ def find_accounts(
|
||||
if provider_code:
|
||||
conditions.append("provider_code = ?")
|
||||
params.append(provider_code)
|
||||
_append_human_ref_filters(
|
||||
conditions, params, login_id=login_id, label=label
|
||||
)
|
||||
|
||||
where = ""
|
||||
if conditions:
|
||||
@@ -289,6 +314,8 @@ def count_active_accounts(
|
||||
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()
|
||||
@@ -306,6 +333,9 @@ def count_active_accounts(
|
||||
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()
|
||||
@@ -319,6 +349,8 @@ def find_account_ids(
|
||||
environment: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
role: Optional[str] = None,
|
||||
login_id: Optional[str] = None,
|
||||
label: Optional[str] = None,
|
||||
) -> list[int]:
|
||||
"""Return account IDs matching filters, excluding those with active leases."""
|
||||
conn = get_conn()
|
||||
@@ -336,6 +368,9 @@ def find_account_ids(
|
||||
if role:
|
||||
conditions.append("a.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
|
||||
now = _now_unix()
|
||||
|
||||
@@ -59,23 +59,30 @@ def cmd_account_ensure_web(
|
||||
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 = (label or "").strip() or None
|
||||
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,
|
||||
login_id,
|
||||
lid,
|
||||
login_id_type,
|
||||
create_label,
|
||||
tenant_id,
|
||||
@@ -95,16 +102,19 @@ def cmd_account_ensure_web(
|
||||
return
|
||||
created = True
|
||||
log.info(
|
||||
"account_ensure_web_created id=%s platform=%s profile_dir=%s",
|
||||
"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",
|
||||
"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.
|
||||
@@ -121,6 +131,8 @@ def cmd_account_ensure_web(
|
||||
environment=environment,
|
||||
tenant_id=tenant_id,
|
||||
role=role,
|
||||
login_id=lid,
|
||||
label=lab,
|
||||
do_lease=do_lease,
|
||||
ttl_sec=ttl_sec,
|
||||
holder=holder,
|
||||
@@ -145,9 +157,10 @@ def cmd_account_ensure_web(
|
||||
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)
|
||||
|
||||
@@ -193,12 +193,21 @@ def cmd_account_list(
|
||||
environment: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
role: Optional[str] = None,
|
||||
login_id: Optional[str] = None,
|
||||
label: Optional[str] = None,
|
||||
limit: int = 200,
|
||||
) -> None:
|
||||
"""account list — output JSON array of accounts matching filters."""
|
||||
log = get_skill_logger()
|
||||
log.info("cli_start subcommand=account_list platform=%s env=%s tenant=%s role=%s",
|
||||
platform_input, environment, tenant_id, role)
|
||||
log.info(
|
||||
"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()
|
||||
key = None
|
||||
if platform_input and platform_input not in ("all", "全部", ""):
|
||||
@@ -211,11 +220,115 @@ def cmd_account_list(
|
||||
environment=environment,
|
||||
tenant_id=tenant_id,
|
||||
role=role,
|
||||
login_id=login_id,
|
||||
label=label,
|
||||
limit=limit,
|
||||
)
|
||||
_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:
|
||||
"""Legacy: list-json <platform|all> [--limit N]"""
|
||||
log = get_skill_logger()
|
||||
@@ -238,6 +351,8 @@ def cmd_account_pick_web(
|
||||
environment: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
role: Optional[str] = None,
|
||||
login_id: Optional[str] = None,
|
||||
label: Optional[str] = None,
|
||||
do_lease: bool = False,
|
||||
ttl_sec: int = 900,
|
||||
holder: Optional[str] = None,
|
||||
@@ -246,8 +361,16 @@ def cmd_account_pick_web(
|
||||
) -> None:
|
||||
"""Pick a web/RPA account for use. Optionally acquire a lease."""
|
||||
log = get_skill_logger()
|
||||
log.info("account_pick_web_attempt platform=%s env=%s tenant=%s role=%s lease=%s",
|
||||
platform_input, environment, tenant_id, role, do_lease)
|
||||
log.info(
|
||||
"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)
|
||||
if not key:
|
||||
@@ -260,15 +383,45 @@ def cmd_account_pick_web(
|
||||
finally:
|
||||
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(
|
||||
platform_key=key,
|
||||
environment=environment,
|
||||
tenant_id=tenant_id,
|
||||
role=role,
|
||||
login_id=lid,
|
||||
label=lab,
|
||||
)
|
||||
if not candidate_ids:
|
||||
_say(_err_json("ERROR:NO_ACCOUNT",
|
||||
"没有符合条件的可用账号(status=active 且未被其他 lease 占用)。"))
|
||||
hint = ""
|
||||
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
|
||||
|
||||
picked_id = candidate_ids[0]
|
||||
|
||||
@@ -26,6 +26,7 @@ from service.account_query_commands import (
|
||||
cmd_account_get_credential,
|
||||
cmd_account_list,
|
||||
cmd_account_pick_web,
|
||||
cmd_account_resolve,
|
||||
cmd_get,
|
||||
cmd_list_json,
|
||||
cmd_pick_web,
|
||||
@@ -52,6 +53,7 @@ __all__ = [
|
||||
"cmd_account_mark_session",
|
||||
"cmd_account_mark_used",
|
||||
"cmd_account_pick_web",
|
||||
"cmd_account_resolve",
|
||||
"cmd_credential_pick",
|
||||
"cmd_credential_resolve",
|
||||
"cmd_delete_by_id",
|
||||
|
||||
Reference in New Issue
Block a user