feat: simplify account create/edit and add row copy
All checks were successful
技能自动化发布 / release (push) Successful in 8s
All checks were successful
技能自动化发布 / release (push) Successful in 8s
Data-management add/edit now use short forms with enums; copy clones a platform account into a new empty profile. Co-authored-by: Cursor <cursoragent@cursor.com>
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|ensure-web|add-secret|get|get-credential|list|resolve|pick-web|mark-session|mark-error|mark-used|open|reveal-profile
|
||||
account add-web|copy|update|ensure-web|add-secret|get|get-credential|list|resolve|pick-web|mark-session|mark-error|mark-used|open|reveal-profile
|
||||
init-master-key|export-credentials|import-credentials
|
||||
credential pick|resolve
|
||||
lease release|list|cleanup
|
||||
@@ -23,7 +23,9 @@ from service.account_crypto_commands import (
|
||||
from service.account_service import (
|
||||
cmd_account_add_secret,
|
||||
cmd_account_add_web,
|
||||
cmd_account_copy,
|
||||
cmd_account_ensure_web,
|
||||
cmd_account_update,
|
||||
cmd_account_get,
|
||||
cmd_account_get_credential,
|
||||
cmd_account_list,
|
||||
@@ -71,11 +73,14 @@ _USAGE = """account-manager v2 — Credential & Browser Profile Manager
|
||||
[--url <url>] [--auth-strategy <s>]
|
||||
|
||||
账号管理:
|
||||
account add-web --platform <p> --login-id <id> --login-id-type <type>
|
||||
account add-web --platform <p> [--login-id <id>] [--login-id-type <type>]
|
||||
[--label <label>] [--tenant-id <t>] [--environment <env>]
|
||||
[--role <role>] [--provider-code <code>] [--url <url>]
|
||||
[--extra-json <json>] [--profile-dir <path>]
|
||||
[--auth-strategy <s>] [--session-persistent 0|1] [--device-fingerprint <fp>]
|
||||
account copy <id>
|
||||
account update --id <id> [--label <label>] [--login-id <id>]
|
||||
[--login-id-type <type>] [--url <url>] [--status active|disabled|needs_review]
|
||||
account add-secret --platform <p> --credential-type <type>
|
||||
--secret-storage env|windows_credential|none|local_encrypted
|
||||
[--secret-ref <env_var>] [--secret-stdin]
|
||||
@@ -276,6 +281,13 @@ def main() -> None:
|
||||
sub = av[2]
|
||||
if sub == "add-web":
|
||||
_cmd_add_web(av)
|
||||
elif sub == "copy":
|
||||
if len(av) < 4:
|
||||
print("ERROR:CLI_BAD_ARGS account copy 需要 <id>。")
|
||||
sys.exit(1)
|
||||
cmd_account_copy(av[3])
|
||||
elif sub == "update":
|
||||
_cmd_account_update(av)
|
||||
elif sub == "ensure-web":
|
||||
_cmd_ensure_web(av)
|
||||
elif sub == "add-secret":
|
||||
@@ -467,7 +479,7 @@ def main() -> None:
|
||||
def _cmd_add_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")
|
||||
login_id_type = _get_opt(argv, "--login-id-type")
|
||||
label = _get_opt(argv, "--label")
|
||||
tenant_id = _get_opt(argv, "--tenant-id")
|
||||
environment = _get_opt(argv, "--environment", "production")
|
||||
@@ -511,6 +523,34 @@ def _cmd_add_web(argv: list[str]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _cmd_account_update(argv: list[str]) -> None:
|
||||
from db.accounts_repo import _UNSET
|
||||
|
||||
aid = _get_opt(argv, "--id")
|
||||
if not aid and len(argv) >= 4 and not str(argv[3]).startswith("-"):
|
||||
aid = argv[3]
|
||||
label = _get_opt(argv, "--label")
|
||||
login_id_type = _get_opt(argv, "--login-id-type")
|
||||
status = _get_opt(argv, "--status")
|
||||
login_id = _UNSET
|
||||
if "--login-id" in argv:
|
||||
login_id = _get_opt(argv, "--login-id") or ""
|
||||
url = _UNSET
|
||||
if "--url" in argv:
|
||||
url = _get_opt(argv, "--url") or ""
|
||||
if not aid:
|
||||
print("ERROR:CLI_BAD_ARGS account update 需要 --id <id>。")
|
||||
return
|
||||
cmd_account_update(
|
||||
aid,
|
||||
label=label,
|
||||
login_id=login_id,
|
||||
login_id_type=login_id_type,
|
||||
url=url,
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
def _cmd_get_credential(argv: list[str]) -> None:
|
||||
aid_raw = argv[3]
|
||||
try:
|
||||
@@ -619,7 +659,7 @@ 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")
|
||||
login_id_type = _get_opt(argv, "--login-id-type")
|
||||
label = _get_opt(argv, "--label")
|
||||
tenant_id = _get_opt(argv, "--tenant-id")
|
||||
environment = _get_opt(argv, "--environment")
|
||||
@@ -651,7 +691,7 @@ def _cmd_ensure_web(argv: list[str]) -> None:
|
||||
cmd_account_ensure_web(
|
||||
platform_input=plat,
|
||||
login_id=login_id,
|
||||
login_id_type=login_id_type or "unknown",
|
||||
login_id_type=login_id_type,
|
||||
label=label,
|
||||
tenant_id=tenant_id,
|
||||
environment=environment,
|
||||
|
||||
@@ -471,6 +471,58 @@ def update_account(
|
||||
)
|
||||
|
||||
|
||||
def list_account_labels(conn) -> list[str]:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT account_label FROM accounts WHERE account_label IS NOT NULL")
|
||||
return [str(row[0]) for row in cur.fetchall() if row[0]]
|
||||
|
||||
|
||||
def update_account_user_fields(
|
||||
conn,
|
||||
account_id: int,
|
||||
*,
|
||||
account_label: Optional[str] = None,
|
||||
login_id: Any = _UNSET,
|
||||
login_id_type: Optional[str] = None,
|
||||
url: Any = _UNSET,
|
||||
status: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Patch fields the data-management edit form is allowed to change."""
|
||||
sets: list[str] = []
|
||||
params: list[Any] = []
|
||||
if account_label is not None:
|
||||
label = str(account_label).strip()
|
||||
if not label:
|
||||
raise ValueError("account_label must not be empty")
|
||||
sets.append("account_label = ?")
|
||||
params.append(label)
|
||||
if login_id is not _UNSET:
|
||||
text = str(login_id).strip() if login_id is not None else ""
|
||||
sets.append("login_id = ?")
|
||||
params.append(text or None)
|
||||
if login_id_type is not None:
|
||||
typed = str(login_id_type).strip() or "unknown"
|
||||
sets.append("login_id_type = ?")
|
||||
params.append(typed)
|
||||
if url is not _UNSET:
|
||||
text = str(url).strip() if url is not None else ""
|
||||
sets.append("url = ?")
|
||||
params.append(text)
|
||||
if status is not None:
|
||||
sets.append("status = ?")
|
||||
params.append(status)
|
||||
if not sets:
|
||||
return
|
||||
sets.append("updated_at = ?")
|
||||
params.append(_now_unix())
|
||||
params.append(int(account_id))
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
f"UPDATE accounts SET {', '.join(sets)} WHERE id = ?",
|
||||
tuple(params),
|
||||
)
|
||||
|
||||
|
||||
def account_exists_with_login_id(conn, platform_key: str, login_id: str, environment: str, role: str) -> bool:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
|
||||
@@ -41,6 +41,83 @@ ACCOUNT_ID_LOOKUP_JSON = json.dumps(
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
def _enum_options(pairs: tuple[tuple[str, str], ...]) -> str:
|
||||
return json.dumps(
|
||||
[{"value": value, "label": label} for value, label in pairs],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
LOGIN_ID_TYPE_OPTIONS_JSON = _enum_options(
|
||||
(
|
||||
("unknown", "未指定"),
|
||||
("phone", "手机号"),
|
||||
("email", "邮箱"),
|
||||
("username", "用户名"),
|
||||
("customer_code", "客户代码"),
|
||||
("api_account", "API 账号"),
|
||||
)
|
||||
)
|
||||
ENVIRONMENT_OPTIONS_JSON = _enum_options(
|
||||
(
|
||||
("production", "生产"),
|
||||
("simulator", "仿真"),
|
||||
("staging", "预发"),
|
||||
("test", "测试"),
|
||||
)
|
||||
)
|
||||
ROLE_OPTIONS_JSON = _enum_options(
|
||||
(
|
||||
("default", "默认"),
|
||||
("booking", "订舱"),
|
||||
("tracking", "跟踪"),
|
||||
("api", "API"),
|
||||
("admin", "管理"),
|
||||
("sales", "销售"),
|
||||
)
|
||||
)
|
||||
ACCOUNT_STATUS_OPTIONS_JSON = _enum_options(
|
||||
(
|
||||
("active", "启用"),
|
||||
("disabled", "停用"),
|
||||
("needs_review", "待复核"),
|
||||
)
|
||||
)
|
||||
SESSION_STATUS_OPTIONS_JSON = _enum_options(
|
||||
(
|
||||
("unknown", "未知"),
|
||||
("likely_valid", "可能有效"),
|
||||
("needs_login", "需要登录"),
|
||||
("expired", "已过期"),
|
||||
)
|
||||
)
|
||||
AUTH_STRATEGY_OPTIONS_JSON = _enum_options(
|
||||
(
|
||||
("password_auto", "账密自动"),
|
||||
("password_with_captcha", "账密+验证码"),
|
||||
("password_plus_2fa", "账密+2FA"),
|
||||
("qr_code_manual", "扫码"),
|
||||
("per_session_manual", "每次手动"),
|
||||
("device_bound", "设备绑定"),
|
||||
("client_certificate", "客户端证书"),
|
||||
("api_token", "API Token"),
|
||||
("sso_redirect", "SSO 跳转"),
|
||||
)
|
||||
)
|
||||
YES_NO_OPTIONS_JSON = _enum_options((("1", "是"), ("0", "否")))
|
||||
ENABLED_OPTIONS_JSON = _enum_options((("1", "启用"), ("0", "停用")))
|
||||
DOMAIN_OPTIONS_JSON = _enum_options(
|
||||
(
|
||||
("logistics", "物流"),
|
||||
("llm", "大模型"),
|
||||
("content", "内容"),
|
||||
("dev", "开发"),
|
||||
("sim", "仿真"),
|
||||
("generic", "通用"),
|
||||
)
|
||||
)
|
||||
|
||||
USER_BUSINESS_TABLES = (
|
||||
PLATFORMS_TABLE,
|
||||
ACCOUNTS_TABLE,
|
||||
@@ -122,13 +199,28 @@ PLATFORMS_TABLE_METADATA = {
|
||||
PLATFORMS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||
_col("platform_key", "平台键", description="平台唯一标识(英文 kebab-case)", editable=0),
|
||||
_col("display_name", "展示名称", description="平台在界面上的显示名称"),
|
||||
_col("domain", "业务域", description="logistics / llm / content / dev / sim 等"),
|
||||
_col(
|
||||
"domain",
|
||||
"业务域",
|
||||
description="物流 / 大模型 / 内容等",
|
||||
options_json=DOMAIN_OPTIONS_JSON,
|
||||
),
|
||||
_col("provider_code", "供应商代码", description="供应商或平台代码,可空"),
|
||||
_col("default_url", "默认链接", description="平台默认入口 URL"),
|
||||
_col("default_url", "默认链接", description="平台默认入口 URL", display_type="url"),
|
||||
_col("aliases_json", "别名列表", description="平台别名 JSON 数组", editable=0),
|
||||
_col("capabilities_json", "能力声明", description='能力 JSON,如 {"web":true,"api_key":false}', editable=0),
|
||||
_col("enabled", "是否启用", description="1=启用,0=停用"),
|
||||
_col("default_auth_strategy", "默认登录策略", description="新建账号时的默认 auth_strategy"),
|
||||
_col(
|
||||
"enabled",
|
||||
"是否启用",
|
||||
description="启用或停用该平台",
|
||||
options_json=ENABLED_OPTIONS_JSON,
|
||||
),
|
||||
_col(
|
||||
"default_auth_strategy",
|
||||
"默认登录策略",
|
||||
description="新建账号时的默认登录方式",
|
||||
options_json=AUTH_STRATEGY_OPTIONS_JSON,
|
||||
),
|
||||
_ts("created_at", "创建时间", "记录创建时间(Unix 秒级时间戳)"),
|
||||
_ts("updated_at", "更新时间", "记录最后更新时间(Unix 秒级时间戳)"),
|
||||
)
|
||||
@@ -150,26 +242,67 @@ ACCOUNTS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||
description="所属平台键(来自平台信息表)",
|
||||
display_type="lookup",
|
||||
options_json=PLATFORM_KEY_LOOKUP_JSON,
|
||||
editable=0,
|
||||
),
|
||||
_col("account_label", "账号名称", description="人可读的账号名称"),
|
||||
_col("login_id", "登录账号", description="手机号、邮箱、用户名等登录标识"),
|
||||
_col("login_id_type", "登录标识类型", description="phone / email / username / unknown 等"),
|
||||
_col("tenant_id", "租户编号", description="租户或客户标识,可空"),
|
||||
_col("environment", "环境", description="production / simulator / staging 等"),
|
||||
_col("role", "角色", description="booking / tracking / api / default 等"),
|
||||
_col("provider_code", "供应商代码", description="冗余供应商码,便于查询"),
|
||||
_col(
|
||||
"login_id_type",
|
||||
"登录标识类型",
|
||||
description="未指定 / 手机号 / 邮箱 / 用户名等",
|
||||
options_json=LOGIN_ID_TYPE_OPTIONS_JSON,
|
||||
),
|
||||
_col("tenant_id", "租户编号", description="租户或客户标识,可空", editable=0),
|
||||
_col(
|
||||
"environment",
|
||||
"环境",
|
||||
description="生产 / 仿真 / 预发 / 测试",
|
||||
options_json=ENVIRONMENT_OPTIONS_JSON,
|
||||
editable=0,
|
||||
),
|
||||
_col(
|
||||
"role",
|
||||
"角色",
|
||||
description="默认 / 订舱 / 跟踪 / API 等",
|
||||
options_json=ROLE_OPTIONS_JSON,
|
||||
editable=0,
|
||||
),
|
||||
_col("provider_code", "供应商代码", description="冗余供应商码,便于查询", editable=0),
|
||||
_col("profile_dir", "浏览器配置目录", description="Playwright 持久化 Profile 路径", editable=0),
|
||||
_col("url", "入口链接", description="账号默认入口 URL"),
|
||||
_col("status", "状态", description="active / disabled / needs_review 等"),
|
||||
_col("session_status", "会话状态", description="unknown / likely_valid / needs_login 等"),
|
||||
_col("url", "入口链接", description="账号默认入口 URL", display_type="url"),
|
||||
_col(
|
||||
"status",
|
||||
"状态",
|
||||
description="启用 / 停用 / 待复核",
|
||||
options_json=ACCOUNT_STATUS_OPTIONS_JSON,
|
||||
),
|
||||
_col(
|
||||
"session_status",
|
||||
"会话状态",
|
||||
description="运行时登录态,由技能回写",
|
||||
options_json=SESSION_STATUS_OPTIONS_JSON,
|
||||
editable=0,
|
||||
),
|
||||
_ts("last_used_at", "最后使用时间", "最近一次被业务技能使用的时间"),
|
||||
_ts("last_login_check_at", "最后登录检查时间", "最近一次登录状态检查时间"),
|
||||
_col("last_error_code", "最近错误码", description="最近一次错误代码", editable=0),
|
||||
_col("last_error_message", "最近错误信息", description="最近一次错误说明", editable=0),
|
||||
_col("extra_json", "扩展信息", description="扩展 JSON 配置", editable=0),
|
||||
_col("auth_strategy", "登录方式", description="九种登录策略之一"),
|
||||
_col("session_persistent", "持久会话", description="是否需要持久化浏览器会话(0/1)"),
|
||||
_col("device_fingerprint", "设备指纹", description="device_bound 策略专用,可空"),
|
||||
_col(
|
||||
"auth_strategy",
|
||||
"登录方式",
|
||||
description="跟随平台默认,一般无需手改",
|
||||
options_json=AUTH_STRATEGY_OPTIONS_JSON,
|
||||
editable=0,
|
||||
),
|
||||
_col(
|
||||
"session_persistent",
|
||||
"持久会话",
|
||||
description="是否需要持久化浏览器会话",
|
||||
options_json=YES_NO_OPTIONS_JSON,
|
||||
editable=0,
|
||||
),
|
||||
_col("device_fingerprint", "设备指纹", description="设备绑定策略专用,可空", editable=0),
|
||||
_ts("created_at", "创建时间", "记录创建时间(Unix 秒级时间戳)"),
|
||||
_ts("updated_at", "更新时间", "记录最后更新时间(Unix 秒级时间戳)"),
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ from service.secret_store import (
|
||||
make_windows_credential_target,
|
||||
write_secret,
|
||||
)
|
||||
from util.account_identity import resolve_login_id_type
|
||||
from util.logging_config import get_skill_logger
|
||||
from util.platforms import get_platform_spec, seed_platforms
|
||||
from util.profile_shortcut import create_profile_browser_shortcut
|
||||
@@ -63,6 +64,7 @@ def create_web_account_record(
|
||||
) -> Optional[dict]:
|
||||
"""Create a web/RPA account. On error prints JSON and returns None; on success returns payload (no stdout)."""
|
||||
log = get_skill_logger()
|
||||
login_id_type = resolve_login_id_type(login_id_type, login_id)
|
||||
log.info("account_add_web_attempt platform=%s login_id_type=%s env=%s role=%s",
|
||||
platform_input, login_id_type, environment, role)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ def cmd_account_ensure_web(
|
||||
platform_input: str,
|
||||
*,
|
||||
login_id: Optional[str] = None,
|
||||
login_id_type: str = "unknown",
|
||||
login_id_type: Optional[str] = None,
|
||||
label: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
environment: Optional[str] = None,
|
||||
|
||||
233
scripts/service/account_mutate_commands.py
Normal file
233
scripts/service/account_mutate_commands.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Account copy / update CLI commands (data-management 复制 / 编辑)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
from db.accounts_repo import (
|
||||
_UNSET,
|
||||
get_account_by_id,
|
||||
list_account_labels,
|
||||
update_account_user_fields,
|
||||
)
|
||||
from db.connection import get_conn, init_db
|
||||
from service._svc_common import _err_json, _say
|
||||
from service.account_add_commands import create_web_account_record
|
||||
from util.account_identity import (
|
||||
KNOWN_ACCOUNT_STATUSES,
|
||||
next_copy_label,
|
||||
resolve_login_id_type,
|
||||
)
|
||||
from util.logging_config import get_skill_logger
|
||||
from util.platforms import get_platform_spec, seed_platforms
|
||||
from util.profile_shortcut import create_profile_browser_shortcut
|
||||
|
||||
|
||||
def _extra_json_str(account: dict) -> Optional[str]:
|
||||
extra = account.get("extra_json")
|
||||
if extra is None or extra == "":
|
||||
return None
|
||||
if isinstance(extra, (dict, list)):
|
||||
return json.dumps(extra, ensure_ascii=False)
|
||||
text = str(extra).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def _ok_result(data: dict, message: str) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"type": "result",
|
||||
"schema_version": 1,
|
||||
"status": "success",
|
||||
"message": message,
|
||||
"success": True,
|
||||
"data": data,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
def cmd_account_copy(account_id) -> None:
|
||||
"""Clone a web account: same platform/strategy, new empty Profile, empty login_id."""
|
||||
log = get_skill_logger()
|
||||
log.info("account_copy_attempt id=%r", account_id)
|
||||
try:
|
||||
aid = int(account_id)
|
||||
except (TypeError, ValueError):
|
||||
_say(_err_json("ERROR:CLI_BAD_ARGS", "account copy 需要整数 id。"))
|
||||
return
|
||||
|
||||
init_db()
|
||||
source = get_account_by_id(aid)
|
||||
if not source:
|
||||
_say(_err_json("ERROR:ACCOUNT_NOT_FOUND", f"账号 id={aid} 不存在。"))
|
||||
return
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
labels = list_account_labels(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
new_label = next_copy_label(str(source.get("account_label") or ""), labels)
|
||||
tenant = str(source.get("tenant_id") or "").strip() or None
|
||||
provider = str(source.get("provider_code") or "").strip() or None
|
||||
url = str(source.get("url") or "").strip() or None
|
||||
extra = _extra_json_str(source)
|
||||
session_persistent = source.get("session_persistent")
|
||||
try:
|
||||
session_persistent_i = int(session_persistent) if session_persistent is not None else None
|
||||
except (TypeError, ValueError):
|
||||
session_persistent_i = None
|
||||
|
||||
ok_payload = create_web_account_record(
|
||||
str(source.get("platform_key") or ""),
|
||||
None,
|
||||
str(source.get("login_id_type") or "unknown"),
|
||||
new_label,
|
||||
tenant,
|
||||
str(source.get("environment") or "production") or "production",
|
||||
str(source.get("role") or "default") or "default",
|
||||
provider,
|
||||
url,
|
||||
extra,
|
||||
None,
|
||||
auth_strategy=source.get("auth_strategy"),
|
||||
session_persistent=session_persistent_i,
|
||||
device_fingerprint=None,
|
||||
platform_key=str(source.get("platform_key") or "") or None,
|
||||
)
|
||||
if not ok_payload:
|
||||
return
|
||||
|
||||
ok_payload["copied_from_id"] = aid
|
||||
log.info(
|
||||
"account_copy_success from=%s to=%s label=%s",
|
||||
aid,
|
||||
ok_payload.get("id"),
|
||||
new_label,
|
||||
)
|
||||
_say(_ok_result(ok_payload, f"已复制为「{new_label}」"))
|
||||
|
||||
|
||||
def cmd_account_update(
|
||||
account_id,
|
||||
*,
|
||||
label: Optional[str] = None,
|
||||
login_id: Any = _UNSET,
|
||||
login_id_type: Optional[str] = None,
|
||||
url: Any = _UNSET,
|
||||
status: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Update the short set of user-facing account fields."""
|
||||
log = get_skill_logger()
|
||||
log.info("account_update_attempt id=%r", account_id)
|
||||
try:
|
||||
aid = int(account_id)
|
||||
except (TypeError, ValueError):
|
||||
_say(_err_json("ERROR:CLI_BAD_ARGS", "account update 需要整数 --id。"))
|
||||
return
|
||||
|
||||
init_db()
|
||||
current = get_account_by_id(aid)
|
||||
if not current:
|
||||
_say(_err_json("ERROR:ACCOUNT_NOT_FOUND", f"账号 id={aid} 不存在。"))
|
||||
return
|
||||
|
||||
next_label = None
|
||||
if label is not None:
|
||||
next_label = str(label).strip()
|
||||
if not next_label:
|
||||
_say(_err_json("ERROR:CLI_BAD_ARGS", "账号名称不能为空。"))
|
||||
return
|
||||
|
||||
next_status = None
|
||||
if status is not None:
|
||||
next_status = str(status).strip()
|
||||
if next_status not in KNOWN_ACCOUNT_STATUSES:
|
||||
_say(_err_json(
|
||||
"ERROR:CLI_BAD_ARGS",
|
||||
f"不支持的状态:{status}。支持:active / disabled / needs_review。",
|
||||
))
|
||||
return
|
||||
|
||||
resolved_login_id = _UNSET
|
||||
if login_id is not _UNSET:
|
||||
resolved_login_id = str(login_id).strip() if login_id is not None else ""
|
||||
|
||||
next_type = None
|
||||
if login_id_type is not None or resolved_login_id is not _UNSET:
|
||||
lid_for_infer = (
|
||||
resolved_login_id
|
||||
if resolved_login_id is not _UNSET
|
||||
else current.get("login_id")
|
||||
)
|
||||
explicit = login_id_type if login_id_type is not None else current.get("login_id_type")
|
||||
next_type = resolve_login_id_type(str(explicit or ""), lid_for_infer)
|
||||
|
||||
resolved_url = _UNSET
|
||||
if url is not _UNSET:
|
||||
text = str(url).strip() if url is not None else ""
|
||||
if text:
|
||||
resolved_url = text
|
||||
else:
|
||||
conn = get_conn()
|
||||
try:
|
||||
seed_platforms(conn)
|
||||
spec = get_platform_spec(str(current.get("platform_key") or ""), conn=conn)
|
||||
finally:
|
||||
conn.close()
|
||||
resolved_url = str(spec.get("default_url") or "")
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
update_account_user_fields(
|
||||
conn,
|
||||
aid,
|
||||
account_label=next_label,
|
||||
login_id=resolved_login_id,
|
||||
login_id_type=next_type,
|
||||
url=resolved_url,
|
||||
status=next_status,
|
||||
)
|
||||
conn.commit()
|
||||
except ValueError as exc:
|
||||
conn.rollback()
|
||||
_say(_err_json("ERROR:CLI_BAD_ARGS", str(exc)))
|
||||
return
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
log.exception("account_update_failure")
|
||||
_say(_err_json("ERROR:DB_ERROR", "Database update failed."))
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
updated = get_account_by_id(aid)
|
||||
if not updated:
|
||||
_say(_err_json("ERROR:ACCOUNT_NOT_FOUND", f"账号 id={aid} 不存在。"))
|
||||
return
|
||||
|
||||
profile_dir = str(updated.get("profile_dir") or "").strip()
|
||||
if profile_dir:
|
||||
create_profile_browser_shortcut(
|
||||
profile_dir,
|
||||
str(updated.get("account_label") or ""),
|
||||
log,
|
||||
url=str(updated.get("url") or "") or None,
|
||||
)
|
||||
|
||||
log.info("account_update_success id=%s", aid)
|
||||
_say(_ok_result(
|
||||
{
|
||||
"id": updated["id"],
|
||||
"platform_key": updated.get("platform_key"),
|
||||
"account_label": updated.get("account_label"),
|
||||
"login_id": updated.get("login_id") or "",
|
||||
"login_id_type": updated.get("login_id_type"),
|
||||
"url": updated.get("url") or "",
|
||||
"status": updated.get("status"),
|
||||
},
|
||||
"已保存",
|
||||
))
|
||||
@@ -16,6 +16,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_mutate_commands import cmd_account_copy, cmd_account_update
|
||||
from service.account_mark_commands import (
|
||||
cmd_account_mark_error,
|
||||
cmd_account_mark_session,
|
||||
@@ -50,7 +51,9 @@ __all__ = [
|
||||
"PlatformCallerMeta",
|
||||
"cmd_account_add_secret",
|
||||
"cmd_account_add_web",
|
||||
"cmd_account_copy",
|
||||
"cmd_account_ensure_web",
|
||||
"cmd_account_update",
|
||||
"cmd_account_get",
|
||||
"cmd_account_get_credential",
|
||||
"cmd_account_list",
|
||||
|
||||
53
scripts/util/account_identity.py
Normal file
53
scripts/util/account_identity.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""账号名称副本编号、登录标识类型推断。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Iterable, Optional
|
||||
|
||||
_COPY_SUFFIX_RE = re.compile(r"^(.*?)(?: 副本(?: (\d+))?)?$")
|
||||
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
_CN_MOBILE_RE = re.compile(r"^1\d{10}$")
|
||||
_PHONE_RE = re.compile(r"^\+?\d{8,15}$")
|
||||
_PHONE_STRIP_RE = re.compile(r"[\s\-()]")
|
||||
|
||||
KNOWN_LOGIN_ID_TYPES = frozenset(
|
||||
{"email", "username", "phone", "customer_code", "api_account", "unknown"}
|
||||
)
|
||||
KNOWN_ACCOUNT_STATUSES = frozenset({"active", "disabled", "needs_review"})
|
||||
|
||||
|
||||
def infer_login_id_type(login_id: Optional[str]) -> str:
|
||||
"""从登录标识猜类型;空则 unknown。"""
|
||||
text = (login_id or "").strip()
|
||||
if not text:
|
||||
return "unknown"
|
||||
if _EMAIL_RE.match(text):
|
||||
return "email"
|
||||
compact = _PHONE_STRIP_RE.sub("", text)
|
||||
if _CN_MOBILE_RE.match(compact) or _PHONE_RE.match(compact):
|
||||
return "phone"
|
||||
return "username"
|
||||
|
||||
|
||||
def resolve_login_id_type(explicit: Optional[str], login_id: Optional[str]) -> str:
|
||||
"""有明确类型(且不是 unknown)时用明确值,否则按 login_id 推断。"""
|
||||
typed = (explicit or "").strip()
|
||||
if typed and typed != "unknown":
|
||||
return typed
|
||||
return infer_login_id_type(login_id)
|
||||
|
||||
|
||||
def next_copy_label(source_label: str, existing_labels: Iterable[str]) -> str:
|
||||
"""在已有名称中生成「原名 副本」/「原名 副本 2」…,避免叠「副本 副本」。"""
|
||||
raw = (source_label or "").strip() or "账号"
|
||||
existing = {item.strip().lower() for item in existing_labels if (item or "").strip()}
|
||||
match = _COPY_SUFFIX_RE.match(raw)
|
||||
stem = (match.group(1).strip() if match else raw) or raw
|
||||
n = 1
|
||||
while n <= 9999:
|
||||
candidate = f"{stem} 副本" if n == 1 else f"{stem} 副本 {n}"
|
||||
if candidate.lower() not in existing:
|
||||
return candidate
|
||||
n += 1
|
||||
return f"{stem} 副本 {n}"
|
||||
Reference in New Issue
Block a user