Files
account-manager/scripts/service/_svc_common.py
chendelian 3f1cde1f99
All checks were successful
技能自动化发布 / release (push) Successful in 6s
fix: validate platform keys at the gate and stop auto-register
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 15:40:30 +08:00

191 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Shared helpers for account service command modules."""
import json
import os
import shutil
import sys
from dataclasses import dataclass, field
from typing import Optional
from db.accounts_repo import (
insert_account,
resolve_auth_strategy_for_platform,
)
from db.auth_strategy import (
AUTH_STRATEGY_CLIENT_CERTIFICATE,
AUTH_STRATEGY_DEVICE_BOUND,
AUTH_STRATEGY_PASSWORD_PLUS_2FA,
AUTH_STRATEGY_PER_SESSION_MANUAL,
AUTH_STRATEGY_QR_CODE_MANUAL,
AUTH_STRATEGY_SSO_REDIRECT,
)
from db.connection import get_conn, init_db
from util.platforms import (
PlatformResolveResult,
_platform_list_cn_for_help,
resolve_platform_key_dynamic,
)
def _map_secret_read_error(exc: BaseException) -> tuple[str, str]:
"""Map secret read/write exceptions to ERROR:* codes (no secret text in code)."""
msg = str(exc)
if "SECRET_ENV_MISSING" in msg:
return "ERROR:SECRET_ENV_MISSING", msg
if "WINDOWS_CREDENTIAL_UNAVAILABLE" in msg:
return "ERROR:WINDOWS_CREDENTIAL_UNAVAILABLE", msg
return "ERROR:SECRET_READ_FAILED", msg
def _credential_ok_line(payload: dict) -> str:
"""Single-line JSON for credential commands (machine integration)."""
return json.dumps({"success": True, **payload}, ensure_ascii=False)
def _derive_session_persistent(auth_strategy: str) -> int:
"""Derive default session_persistent from auth_strategy when CLI omits it."""
if auth_strategy in (
AUTH_STRATEGY_QR_CODE_MANUAL,
AUTH_STRATEGY_PASSWORD_PLUS_2FA,
AUTH_STRATEGY_DEVICE_BOUND,
AUTH_STRATEGY_SSO_REDIRECT,
):
return 1
if auth_strategy in (AUTH_STRATEGY_PER_SESSION_MANUAL, AUTH_STRATEGY_CLIENT_CERTIFICATE):
return 0
return 1
def _insert_secret_holder_account(
conn,
*,
platform_key: str,
account_label: str,
environment: str,
role: str,
provider_code: str,
url: Optional[str],
now: int,
) -> int:
"""Create a minimal accounts row so credentials can join on environment/role."""
return insert_account(
conn=conn,
platform_key=platform_key,
account_label=account_label,
login_id=None,
login_id_type="api_account",
tenant_id=None,
environment=environment,
role=role,
provider_code=provider_code,
profile_dir=None,
url=url,
extra_json="{}",
now=now,
auth_strategy=resolve_auth_strategy_for_platform(conn, platform_key),
)
def _err_json(code: str, message: str) -> str:
"""Return a JSON error line."""
return json.dumps(
{"success": False, "error": {"code": code, "message": message}},
ensure_ascii=False,
)
def _ok_json(data) -> str:
"""Return a JSON success line."""
return json.dumps(
{"success": True, "data": data},
ensure_ascii=False,
)
def _say(msg: str) -> None:
"""Print to stdout."""
print(msg)
def _say_err(msg: str) -> None:
"""Print to stderr."""
print(msg, file=sys.stderr)
def _fail_human(code: str, hint_lines: list[str]) -> None:
"""Print human-readable error block."""
_say(f"ERROR:{code}")
for line in hint_lines:
_say(line)
def _remove_profile_dir(path: str) -> None:
p = (path or "").strip()
if not p or not os.path.isdir(p):
return
try:
shutil.rmtree(p)
except OSError as e:
_say_err(f"WARNING: 未能删除用户数据目录(可手工删):{p}\n {e}")
@dataclass
class PlatformCallerMeta:
"""调用方可附带的平台展示信息。不再用于自动插入 platforms 行。"""
display_name: Optional[str] = None
url: Optional[str] = None
domain: Optional[str] = None
auth_strategy: Optional[str] = None
aliases: list[str] = field(default_factory=list)
_RESOLVE_CODES_WITHOUT_PLATFORM_LIST = frozenset(
{
"PLATFORM_NOT_REGISTERED",
"INVALID_PLATFORM_KEY",
"PLATFORM_DISABLED",
"AMBIGUOUS_PLATFORM",
"PLATFORM_ALIAS_CONFLICT",
"INVALID_PLATFORM_DISPLAY_NAME",
}
)
def _emit_platform_resolve_error(result: PlatformResolveResult, hint: str = "") -> None:
code = result.error_code or "INVALID_PLATFORM"
if not code.startswith("ERROR:"):
code = f"ERROR:{code}"
_say(_err_json(code, result.message or "无法识别平台。"))
if hint:
_say(hint)
def _resolve_or_auto_register(
platform_input: str,
meta: Optional[PlatformCallerMeta] = None,
*,
hint: str = "",
) -> Optional[str]:
"""解析已注册平台;未注册则拒绝。meta 仅兼容旧调用方,不再自动建平台。"""
del meta
return _resolve_or_fail(platform_input, hint=hint)
def _resolve_or_fail(input_name: str, hint: str = "") -> Optional[str]:
"""Resolve platform key or emit structured JSON error and return None."""
init_db()
conn = get_conn()
try:
result = resolve_platform_key_dynamic(input_name, conn=conn)
finally:
conn.close()
if result.ok:
return result.platform_key
_emit_platform_resolve_error(result, hint=hint)
if result.error_code not in _RESOLVE_CODES_WITHOUT_PLATFORM_LIST:
_say("支持:" + _platform_list_cn_for_help())
return None