feat: account ensure-web (pick-or-create for first-run UX)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
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|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|pick-web|mark-session|mark-error|mark-used|open
|
||||
init-master-key|export-credentials|import-credentials
|
||||
credential pick|resolve
|
||||
lease release|list|cleanup
|
||||
@@ -23,6 +23,7 @@ from service.account_crypto_commands import (
|
||||
from service.account_service import (
|
||||
cmd_account_add_secret,
|
||||
cmd_account_add_web,
|
||||
cmd_account_ensure_web,
|
||||
cmd_account_get,
|
||||
cmd_account_get_credential,
|
||||
cmd_account_list,
|
||||
@@ -82,6 +83,12 @@ _USAGE = """account-manager v2 — Credential & Browser Profile Manager
|
||||
account pick-web --platform <p> [--environment <env>] [--tenant-id <t>]
|
||||
[--role <role>] [--lease] [--ttl-sec 900]
|
||||
[--holder <holder>] [--purpose <purpose>]
|
||||
[--ensure] # 同 ensure-web:无账号时自动登记再 pick
|
||||
account ensure-web --platform <p> [--login-id <id>] [--label <label>]
|
||||
[--environment <env>] [--tenant-id <t>] [--role <role>]
|
||||
[--lease] [--ttl-sec 900] [--holder <h>] [--purpose <p>]
|
||||
# 有账号则 pick;零账号则按参数 add-web 后再 pick
|
||||
# 有账号但均被租约占用时不新建,返回 LEASE_CONFLICT
|
||||
account mark-session <id> --session-status <status>
|
||||
account mark-error <id> --code <code> --message <msg>
|
||||
account mark-used <id>
|
||||
@@ -246,12 +253,14 @@ def main() -> None:
|
||||
# ---- account subcommand ----
|
||||
elif cmd == "account":
|
||||
if len(av) < 3:
|
||||
print("ERROR:CLI_BAD_ARGS account 需要子命令(add-web / get / …)。")
|
||||
print("ERROR:CLI_BAD_ARGS account 需要子命令(add-web / ensure-web / get / …)。")
|
||||
print(_USAGE)
|
||||
sys.exit(1)
|
||||
sub = av[2]
|
||||
if sub == "add-web":
|
||||
_cmd_add_web(av)
|
||||
elif sub == "ensure-web":
|
||||
_cmd_ensure_web(av)
|
||||
elif sub == "add-secret":
|
||||
_cmd_add_secret(av)
|
||||
elif sub == "get-credential":
|
||||
@@ -543,11 +552,15 @@ def _cmd_pick_web_new(argv: list[str]) -> None:
|
||||
ttl = _get_int_opt(argv, "--ttl-sec", 900)
|
||||
holder = _get_opt(argv, "--holder")
|
||||
purpose = _get_opt(argv, "--purpose")
|
||||
do_ensure = _has_flag(argv, "--ensure")
|
||||
|
||||
if not plat:
|
||||
print("ERROR:CLI_BAD_ARGS account pick-web 需要 --platform <platform>。")
|
||||
return
|
||||
platform_meta = _parse_platform_caller_meta(argv)
|
||||
if do_ensure:
|
||||
_cmd_ensure_web(argv)
|
||||
return
|
||||
cmd_account_pick_web(
|
||||
platform_input=plat,
|
||||
environment=env,
|
||||
@@ -561,6 +574,61 @@ def _cmd_pick_web_new(argv: list[str]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _cmd_ensure_web(argv: list[str]) -> None:
|
||||
plat = _get_opt(argv, "--platform", "")
|
||||
login_id = _get_opt(argv, "--login-id")
|
||||
login_id_type = _get_opt(argv, "--login-id-type", "unknown")
|
||||
label = _get_opt(argv, "--label")
|
||||
tenant_id = _get_opt(argv, "--tenant-id")
|
||||
environment = _get_opt(argv, "--environment")
|
||||
role = _get_opt(argv, "--role")
|
||||
provider_code = _get_opt(argv, "--provider-code")
|
||||
url = _get_opt(argv, "--url")
|
||||
extra_json_str = _get_opt(argv, "--extra-json")
|
||||
profile_dir_override = _get_opt(argv, "--profile-dir")
|
||||
auth_strategy = _get_opt(argv, "--auth-strategy")
|
||||
session_persistent_raw = _get_opt(argv, "--session-persistent")
|
||||
device_fingerprint = _get_opt(argv, "--device-fingerprint")
|
||||
do_lease = _has_flag(argv, "--lease")
|
||||
ttl = _get_int_opt(argv, "--ttl-sec", 900)
|
||||
holder = _get_opt(argv, "--holder")
|
||||
purpose = _get_opt(argv, "--purpose")
|
||||
|
||||
session_persistent: Optional[int] = None
|
||||
if session_persistent_raw is not None:
|
||||
try:
|
||||
session_persistent = int(session_persistent_raw)
|
||||
except ValueError:
|
||||
print("ERROR:CLI_BAD_ARGS --session-persistent 必须是整数(0 或 1)。")
|
||||
return
|
||||
|
||||
if not plat:
|
||||
print("ERROR:CLI_BAD_ARGS account ensure-web 需要 --platform <platform>。")
|
||||
return
|
||||
platform_meta = _parse_platform_caller_meta(argv)
|
||||
cmd_account_ensure_web(
|
||||
platform_input=plat,
|
||||
login_id=login_id,
|
||||
login_id_type=login_id_type or "unknown",
|
||||
label=label,
|
||||
tenant_id=tenant_id,
|
||||
environment=environment,
|
||||
role=role,
|
||||
provider_code=provider_code,
|
||||
url=url,
|
||||
extra_json_str=extra_json_str,
|
||||
profile_dir_override=profile_dir_override,
|
||||
auth_strategy=auth_strategy,
|
||||
session_persistent=session_persistent,
|
||||
device_fingerprint=device_fingerprint,
|
||||
do_lease=do_lease,
|
||||
ttl_sec=ttl,
|
||||
holder=holder,
|
||||
purpose=purpose,
|
||||
platform_meta=platform_meta,
|
||||
)
|
||||
|
||||
|
||||
def _cmd_cred_pick(argv: list[str]) -> None:
|
||||
plat = _get_opt(argv, "--platform", "")
|
||||
ctype = _get_opt(argv, "--type")
|
||||
|
||||
@@ -284,6 +284,36 @@ def find_accounts(
|
||||
conn.close()
|
||||
|
||||
|
||||
def count_active_accounts(
|
||||
platform_key: str,
|
||||
environment: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
role: Optional[str] = None,
|
||||
) -> int:
|
||||
"""Count active accounts matching filters (ignores lease occupancy)."""
|
||||
conn = get_conn()
|
||||
try:
|
||||
conn.row_factory = None
|
||||
cur = conn.cursor()
|
||||
conditions = ["status = 'active'", "platform_key = ?"]
|
||||
params: list = [platform_key]
|
||||
if environment:
|
||||
conditions.append("environment = ?")
|
||||
params.append(environment)
|
||||
if tenant_id:
|
||||
conditions.append("tenant_id = ?")
|
||||
params.append(tenant_id)
|
||||
if role:
|
||||
conditions.append("role = ?")
|
||||
params.append(role)
|
||||
where = " AND ".join(conditions)
|
||||
cur.execute(f"SELECT COUNT(*) FROM accounts WHERE {where}", params)
|
||||
row = cur.fetchone()
|
||||
return int(row[0] or 0) if row else 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def find_account_ids(
|
||||
platform_key: str,
|
||||
environment: Optional[str] = None,
|
||||
|
||||
@@ -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(
|
||||
|
||||
161
scripts/service/account_ensure_commands.py
Normal file
161
scripts/service/account_ensure_commands.py
Normal 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)
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user