# -*- coding: utf-8 -*- """Account query and pick CLI commands.""" import json import uuid from typing import Optional from db.accounts_repo import ( acquire_lease, find_account_ids, find_accounts, find_credentials, get_account_by_id, get_active_lease_by_token, update_account_last_used, ) from db.connection import get_conn, init_db from db.credentials_repo import ( CredentialNotEncryptedError, list_credentials_by_account, read_credential_plaintext, ) from service._svc_common import ( PlatformCallerMeta, _err_json, _resolve_or_auto_register, _resolve_or_fail, _say, _say_err, ) from service.crypto import CredentialDecryptError from service.master_key import MasterKeyMissingError from util.logging_config import get_skill_logger from util.platforms import seed_platforms from util.runtime_paths import _runtime_paths_debug_text def cmd_account_get(account_id, include_credentials: bool = False) -> None: """Get a single account by id. Returns JSON.""" log = get_skill_logger() log.info("cli_start subcommand=account_get id=%r", account_id) init_db() acc = get_account_by_id(account_id) if not acc: log.warning("account_get_not_found id=%r", account_id) _say(f"ERROR:ACCOUNT_NOT_FOUND 账号 id={account_id} 不存在。") _say_err(_runtime_paths_debug_text()) return if include_credentials: creds = find_credentials( platform_key=acc["platform_key"], limit=20, ) linked_creds = [c for c in creds if c["account_id"] == acc["id"]] if not linked_creds: linked_creds = [c for c in creds if c["account_id"] is None] acc["credentials"] = [ { "id": c["id"], "credential_label": c["credential_label"], "credential_type": c["credential_type"], "secret_storage": c["secret_storage"], "secret_ref": c["secret_ref"], "secret_mask": c["secret_mask"], "status": c["status"], } for c in linked_creds ] _say(json.dumps(acc, ensure_ascii=False)) def cmd_account_get_credential( account_id: int, *, reveal: bool = False, lease_token: Optional[str] = None, ) -> None: """account get-credential — metadata or plaintext when lease proves access.""" log = get_skill_logger() log.info("cli_start subcommand=account_get_credential id=%s reveal=%s", account_id, reveal) init_db() acc = get_account_by_id(account_id) if not acc: log.warning("account_get_credential_not_found id=%s", account_id) _say(_err_json("ERROR:ACCOUNT_NOT_FOUND", f"账号 id={account_id} 不存在。")) return conn = get_conn() try: creds_all = list_credentials_by_account(conn, account_id) creds = [c for c in creds_all if (c.get("status") or "").strip().lower() == "active"] if not creds: _say(_err_json( "ERR_CREDENTIAL_MISSING", f"账号 id={account_id} 没有活跃凭据。", )) return cred = creds[0] base_out = { "success": True, "account_id": account_id, "credential_id": cred["id"], "credential_type": cred["credential_type"], "secret_storage": cred["secret_storage"], "secret_mask": cred["secret_mask"], } if not reveal: _say(json.dumps(base_out, ensure_ascii=False)) return tok = (lease_token or "").strip() if not tok: log.warning("account_get_credential_reveal_missing_token id=%s", account_id) _say(_err_json( "ERR_LEASE_INVALID", "reveal 必须带 --lease-token。", )) return lease = get_active_lease_by_token(conn, tok) lease_prefix = tok[:8] if lease is None: log.warning( "account_get_credential_reveal_bad_lease id=%s lease_prefix=%s", account_id, lease_prefix, ) _say(_err_json( "ERR_LEASE_INVALID", "lease token 无效或已过期", )) return if int(lease["account_id"]) != int(account_id): log.warning( "account_get_credential_reveal_lease_mismatch id=%s lease_account=%s lease_prefix=%s", account_id, lease["account_id"], lease_prefix, ) _say(_err_json( "ERR_LEASE_INVALID", "lease token 不属于该 account", )) return try: pt = read_credential_plaintext(conn, int(cred["id"])) except MasterKeyMissingError as e: log.warning("account_get_credential_master_missing id=%s", account_id) _say(_err_json("ERR_MASTER_KEY_MISSING", str(e))) return except CredentialDecryptError as e: log.warning( "account_get_credential_decrypt_failed id=%s cred_id=%s lease_prefix=%s", account_id, cred["id"], lease_prefix, ) _say(_err_json("ERR_CREDENTIAL_DECRYPT_FAILED", str(e))) return except CredentialNotEncryptedError: out = {**base_out, "plaintext": None, "note": "credential storage=none, no plaintext to reveal"} log.info( "account_get_credential_reveal_note_plain id=%s cred_id=%s lease_prefix=%s", account_id, cred["id"], lease_prefix, ) _say(json.dumps(out, ensure_ascii=False)) return log.info( "account_get_credential_reveal_success account_id=%s cred_id=%s lease_prefix=%s", account_id, cred["id"], lease_prefix, ) _say(json.dumps({**base_out, "plaintext": pt}, ensure_ascii=False)) finally: conn.close() def cmd_get(account_id) -> None: """Legacy: get — same as account get .""" cmd_account_get(account_id, include_credentials=False) def cmd_account_list( platform_input: Optional[str] = None, environment: Optional[str] = None, tenant_id: Optional[str] = None, role: 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) init_db() key = None if platform_input and platform_input not in ("all", "全部", ""): key = _resolve_or_fail(platform_input) if not key: return accounts = find_accounts( platform_key=key, environment=environment, tenant_id=tenant_id, role=role, limit=limit, ) _say(json.dumps(accounts, ensure_ascii=False)) def cmd_list_json(platform_input: str, limit: int = 200) -> None: """Legacy: list-json [--limit N]""" log = get_skill_logger() log.info("cli_start subcommand=list_json filter=%r limit=%s", platform_input, limit) init_db() raw = (platform_input or "all").strip() if not raw or raw.lower() == "all" or raw == "全部": key = None else: key = _resolve_or_fail(raw) if not key: return lim = max(1, min(int(limit), 500)) accounts = find_accounts(platform_key=key, limit=lim) _say(json.dumps(accounts, ensure_ascii=False)) def cmd_account_pick_web( platform_input: str, environment: Optional[str] = None, tenant_id: Optional[str] = None, role: 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/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) 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() candidate_ids = find_account_ids( platform_key=key, environment=environment, tenant_id=tenant_id, role=role, ) if not candidate_ids: _say(_err_json("ERROR:NO_ACCOUNT", "没有符合条件的可用账号(status=active 且未被其他 lease 占用)。")) return picked_id = candidate_ids[0] acc = get_account_by_id(picked_id) if not acc: _say(_err_json("ERROR:ACCOUNT_NOT_FOUND", "选中的账号突然不存在了。")) return update_account_last_used(picked_id) result = { "id": acc["id"], "platform_key": acc["platform_key"], "account_label": acc["account_label"], "login_id": acc["login_id"], "profile_dir": acc["profile_dir"], "url": acc["url"], "tenant_id": acc["tenant_id"], "environment": acc["environment"], "role": acc["role"], "provider_code": acc["provider_code"], "session_status": acc["session_status"], "auth_strategy": acc["auth_strategy"], "session_persistent": acc["session_persistent"], "device_fingerprint": acc.get("device_fingerprint"), } lease_info = None if do_lease: lease_token = uuid.uuid4().hex[:32] resolved_holder = holder or "unknown" try: lease_info = acquire_lease( account_id=picked_id, lease_token=lease_token, holder=resolved_holder, purpose=purpose, ttl_sec=ttl_sec, ) result["lease_token"] = lease_info["lease_token"] result["lease_expires_at"] = lease_info["expires_at"] except ValueError as e: log.warning("account_pick_web_lease_conflict id=%s", picked_id) _say(_err_json("ERROR:LEASE_CONFLICT", str(e))) return log.info("account_pick_web_success id=%s token_prefix=%s", picked_id, lease_info["lease_token"][:8] if lease_info else "none") _say(json.dumps(result, ensure_ascii=False)) def cmd_pick_web(platform_input: str) -> None: """Legacy: pick-web — same as account pick-web without lease.""" cmd_account_pick_web(platform_input, do_lease=False)