feat: account ensure-web (pick-or-create for first-run UX)
All checks were successful
技能自动化发布 / release (push) Successful in 5s

This commit is contained in:
2026-07-18 14:37:16 +08:00
parent 12b1fec0b9
commit e441b0c58e
11 changed files with 470 additions and 17 deletions

View File

@@ -42,7 +42,7 @@ from util.profile_shortcut import create_profile_browser_shortcut
from util.runtime_paths import get_default_profile_dir_for_web
def cmd_account_add_web(
def create_web_account_record(
platform_input: str,
login_id: Optional[str],
login_id_type: str,
@@ -59,15 +59,16 @@ def cmd_account_add_web(
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."""
platform_key: Optional[str] = None,
) -> 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.info("account_add_web_attempt platform=%s login_id_type=%s env=%s role=%s",
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:
return
return None
init_db()
now = int(time.time())
@@ -85,7 +86,7 @@ def cmd_account_add_web(
extra = json.loads(extra_json_str)
except json.JSONDecodeError:
_say(_err_json("ERROR:CLI_BAD_ARGS", "extra_json 不是合法的 JSON 字符串。"))
return
return None
if 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",
f"不支持的 auth_strategy{auth_strategy}",
))
return
return None
resolved_auth = auth_strategy
if session_persistent is None:
@@ -124,7 +125,7 @@ def cmd_account_add_web(
"ERR_INVALID_SESSION_PERSISTENT",
"session_persistent 必须是 0 或 1。",
))
return
return None
sp = session_persistent
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",
"device_fingerprint 仅在 auth_strategy=device_bound 时可设置。",
))
return
return None
new_id = insert_account(
conn=conn,
@@ -157,7 +158,7 @@ def cmd_account_add_web(
conn.rollback()
log.exception("account_add_web_failure")
_say(_err_json("ERROR:DB_ERROR", "Database insert failed."))
return
return None
finally:
conn.close()
@@ -182,7 +183,47 @@ def cmd_account_add_web(
}
if 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(

View File

@@ -0,0 +1,161 @@
# -*- 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()
existing_count = count_active_accounts(
platform_key=key,
environment=environment,
tenant_id=tenant_id,
role=role,
)
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
if not create_label:
create_label = f"{key} 默认"
created_payload = create_web_account_record(
platform_input,
login_id,
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 profile_dir=%s",
created_payload.get("id"),
key,
created_payload.get("profile_dir"),
)
else:
log.info(
"account_ensure_web_reuse platform=%s existing_active=%s",
key,
existing_count,
)
# 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,
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:
_say(_err_json(
"ERROR:LEASE_CONFLICT",
"该平台下已有账号,但均被其他任务占用。请稍后重试或释放租约;不会自动新建账号。",
))
return
_say(raw)
return
if isinstance(payload, dict) and created:
payload["account_ensured"] = True
_say(json.dumps(payload, ensure_ascii=False))
return
_say(raw)

View File

@@ -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.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 (
cmd_account_mark_error,
cmd_account_mark_session,
@@ -43,6 +44,7 @@ __all__ = [
"PlatformCallerMeta",
"cmd_account_add_secret",
"cmd_account_add_web",
"cmd_account_ensure_web",
"cmd_account_get",
"cmd_account_get_credential",
"cmd_account_list",