chore: auto release commit (2026-06-19 11:19:10)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
325
scripts/service/account_query_commands.py
Normal file
325
scripts/service/account_query_commands.py
Normal file
@@ -0,0 +1,325 @@
|
||||
# -*- 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 <id> — same as account get <id>."""
|
||||
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 <platform|all> [--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 <platform> — same as account pick-web without lease."""
|
||||
cmd_account_pick_web(platform_input, do_lease=False)
|
||||
Reference in New Issue
Block a user