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:
539
scripts/service/account_add_commands.py
Normal file
539
scripts/service/account_add_commands.py
Normal file
@@ -0,0 +1,539 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Account creation CLI commands (add-web, add-secret)."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from db.accounts_repo import (
|
||||
get_account_by_id,
|
||||
insert_account,
|
||||
insert_credential,
|
||||
resolve_auth_strategy_for_platform,
|
||||
)
|
||||
from db.auth_strategy import (
|
||||
ALL_AUTH_STRATEGIES,
|
||||
AUTH_STRATEGY_DEVICE_BOUND,
|
||||
)
|
||||
from db.connection import get_conn, init_db
|
||||
from db.credentials_repo import insert_credential_encrypted
|
||||
from service._svc_common import (
|
||||
PlatformCallerMeta,
|
||||
_derive_session_persistent,
|
||||
_err_json,
|
||||
_insert_secret_holder_account,
|
||||
_ok_json,
|
||||
_resolve_or_auto_register,
|
||||
_resolve_or_fail,
|
||||
_say,
|
||||
_say_err,
|
||||
)
|
||||
from service.crypto import CredentialDecryptError
|
||||
from service.master_key import MasterKeyMissingError, is_master_key_available
|
||||
from service.secret_store import (
|
||||
mask_secret,
|
||||
make_windows_credential_target,
|
||||
write_secret,
|
||||
)
|
||||
from util.logging_config import get_skill_logger
|
||||
from util.platforms import get_platform_spec, seed_platforms
|
||||
from util.runtime_paths import get_default_profile_dir_for_web
|
||||
|
||||
|
||||
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."""
|
||||
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)
|
||||
if not key:
|
||||
return
|
||||
|
||||
init_db()
|
||||
now = int(time.time())
|
||||
conn = get_conn()
|
||||
try:
|
||||
seed_platforms(conn)
|
||||
platform_spec = get_platform_spec(key, conn=conn)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
safe_label = (label or "").strip() or f"{platform_spec.get('display_name', key)} {environment} {role}"
|
||||
extra = {}
|
||||
if extra_json_str:
|
||||
try:
|
||||
extra = json.loads(extra_json_str)
|
||||
except json.JSONDecodeError:
|
||||
_say(_err_json("ERROR:CLI_BAD_ARGS", "extra_json 不是合法的 JSON 字符串。"))
|
||||
return
|
||||
|
||||
if profile_dir_override:
|
||||
resolved_profile_dir = os.path.abspath(profile_dir_override)
|
||||
else:
|
||||
resolved_profile_dir = get_default_profile_dir_for_web(
|
||||
platform_key=key,
|
||||
label=safe_label,
|
||||
login_id=login_id,
|
||||
domain=platform_spec.get("domain", "generic"),
|
||||
)
|
||||
os.makedirs(resolved_profile_dir, exist_ok=True)
|
||||
|
||||
resolved_url = (url or "").strip() or platform_spec.get("default_url", "")
|
||||
resolved_provider = provider_code or platform_spec.get("provider_code") or key
|
||||
resolved_extra = json.dumps(extra, ensure_ascii=False)
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
seed_platforms(conn)
|
||||
if auth_strategy is None:
|
||||
resolved_auth = resolve_auth_strategy_for_platform(conn, key)
|
||||
else:
|
||||
if auth_strategy not in ALL_AUTH_STRATEGIES:
|
||||
_say(_err_json(
|
||||
"ERR_AUTH_STRATEGY_NOT_SUPPORTED",
|
||||
f"不支持的 auth_strategy:{auth_strategy}",
|
||||
))
|
||||
return
|
||||
resolved_auth = auth_strategy
|
||||
|
||||
if session_persistent is None:
|
||||
sp = _derive_session_persistent(resolved_auth)
|
||||
else:
|
||||
if session_persistent not in (0, 1):
|
||||
_say(_err_json(
|
||||
"ERR_INVALID_SESSION_PERSISTENT",
|
||||
"session_persistent 必须是 0 或 1。",
|
||||
))
|
||||
return
|
||||
sp = session_persistent
|
||||
|
||||
if device_fingerprint is not None and resolved_auth != AUTH_STRATEGY_DEVICE_BOUND:
|
||||
_say(_err_json(
|
||||
"ERR_DEVICE_FINGERPRINT_NOT_APPLICABLE",
|
||||
"device_fingerprint 仅在 auth_strategy=device_bound 时可设置。",
|
||||
))
|
||||
return
|
||||
|
||||
new_id = insert_account(
|
||||
conn=conn,
|
||||
platform_key=key,
|
||||
account_label=safe_label,
|
||||
login_id=login_id or None,
|
||||
login_id_type=login_id_type,
|
||||
tenant_id=tenant_id or None,
|
||||
environment=environment,
|
||||
role=role,
|
||||
provider_code=resolved_provider,
|
||||
profile_dir=resolved_profile_dir,
|
||||
url=resolved_url,
|
||||
extra_json=resolved_extra,
|
||||
now=now,
|
||||
auth_strategy=resolved_auth,
|
||||
session_persistent=sp,
|
||||
device_fingerprint=device_fingerprint,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
log.exception("account_add_web_failure")
|
||||
_say(_err_json("ERROR:DB_ERROR", "Database insert failed."))
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
log.info("account_add_web_success id=%s platform=%s profile_dir=%s", new_id, key, resolved_profile_dir)
|
||||
_say(_ok_json({
|
||||
"id": new_id,
|
||||
"platform_key": key,
|
||||
"account_label": safe_label,
|
||||
"profile_dir": resolved_profile_dir,
|
||||
"url": resolved_url,
|
||||
"environment": environment,
|
||||
"role": role,
|
||||
"tenant_id": tenant_id or "",
|
||||
"provider_code": resolved_provider,
|
||||
"status": "active",
|
||||
"session_status": "unknown",
|
||||
"auth_strategy": resolved_auth,
|
||||
"session_persistent": sp,
|
||||
"device_fingerprint": device_fingerprint,
|
||||
}))
|
||||
|
||||
|
||||
def cmd_account_add_secret(
|
||||
platform_input: str,
|
||||
credential_type: str,
|
||||
label: Optional[str],
|
||||
secret_storage: str,
|
||||
secret_stdin: bool,
|
||||
secret_ref: Optional[str],
|
||||
account_id: Optional[int],
|
||||
environment: Optional[str],
|
||||
role: Optional[str],
|
||||
extra_json_str: Optional[str],
|
||||
) -> None:
|
||||
"""account add-secret — create a credential record with masked secret."""
|
||||
log = get_skill_logger()
|
||||
log.info("account_add_secret_attempt platform=%s type=%s storage=%s",
|
||||
platform_input, credential_type, secret_storage)
|
||||
|
||||
key = _resolve_or_fail(platform_input)
|
||||
if not key:
|
||||
return
|
||||
|
||||
storage_n = secret_storage.strip().lower()
|
||||
if storage_n not in ("none", "env", "windows_credential", "local_encrypted"):
|
||||
_say(_err_json("ERROR:SECRET_STORAGE_UNSUPPORTED",
|
||||
f"不支持的存储方式:{secret_storage}。支持:none / env / windows_credential / local_encrypted"))
|
||||
return
|
||||
|
||||
if storage_n == "none" and credential_type not in ("browser_profile", "note"):
|
||||
_say(_err_json("ERROR:SECRET_STORAGE_UNSUPPORTED",
|
||||
"storage=none 仅允许 credential_type=browser_profile 或 note。"))
|
||||
return
|
||||
if storage_n == "env" and not secret_ref:
|
||||
_say(_err_json("ERROR:SECRET_REF_MISSING",
|
||||
"env 存储时必须提供 --secret-ref 环境变量名。"))
|
||||
return
|
||||
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
seed_platforms(conn)
|
||||
platform_spec = get_platform_spec(key, conn=conn)
|
||||
finally:
|
||||
conn.close()
|
||||
now = int(time.time())
|
||||
safe_label = (label or "").strip() or f"{platform_spec.get('display_name', key)} {credential_type}"
|
||||
|
||||
if storage_n == "local_encrypted":
|
||||
if not is_master_key_available():
|
||||
_say(_err_json(
|
||||
"ERR_MASTER_KEY_MISSING",
|
||||
"master key 未初始化。先跑 `account init-master-key`。",
|
||||
))
|
||||
return
|
||||
if not secret_stdin:
|
||||
_say(_err_json(
|
||||
"ERR_LOCAL_ENCRYPTED_REQUIRES_STDIN",
|
||||
"local_encrypted 后端必须 --secret-stdin 输入密文,避免命令历史泄漏。",
|
||||
))
|
||||
return
|
||||
if credential_type not in (
|
||||
"password",
|
||||
"api_key",
|
||||
"api_token",
|
||||
"bearer_token",
|
||||
"refresh_token",
|
||||
"oauth_client",
|
||||
):
|
||||
_say(_err_json(
|
||||
"ERR_LOCAL_ENCRYPTED_TYPE_NOT_ALLOWED",
|
||||
f"local_encrypted 不接受 credential_type={credential_type}(仅 secret 类)。",
|
||||
))
|
||||
return
|
||||
if secret_ref:
|
||||
_say_err("[add-secret] WARNING: secret_ref ignored for local_encrypted (secret comes from stdin).")
|
||||
|
||||
extra: dict = {}
|
||||
if extra_json_str:
|
||||
try:
|
||||
extra = json.loads(extra_json_str)
|
||||
except json.JSONDecodeError:
|
||||
extra = {}
|
||||
resolved_env = environment or "production"
|
||||
resolved_role = role or "api"
|
||||
extra["environment"] = resolved_env
|
||||
extra["role"] = resolved_role
|
||||
resolved_extra = json.dumps(extra, ensure_ascii=False)
|
||||
resolved_url = (platform_spec.get("default_url") or "").strip() or None
|
||||
resolved_provider = platform_spec.get("provider_code") or key
|
||||
|
||||
plaintext = sys.stdin.readline().rstrip("\r\n")
|
||||
if not plaintext:
|
||||
_say(_err_json("ERR_LOCAL_ENCRYPTED_EMPTY_STDIN", "stdin 没读到密码内容。"))
|
||||
return
|
||||
secret_mask_out = mask_secret(plaintext)
|
||||
|
||||
conn = get_conn()
|
||||
cred_id: Optional[int] = None
|
||||
holder_id: Optional[int] = None
|
||||
try:
|
||||
seed_platforms(conn)
|
||||
holder_id = account_id
|
||||
if holder_id is not None:
|
||||
acc = get_account_by_id(holder_id)
|
||||
if not acc:
|
||||
_say(_err_json(
|
||||
"ERROR:ACCOUNT_NOT_FOUND",
|
||||
f"account_id={holder_id} 不存在。",
|
||||
))
|
||||
return
|
||||
if holder_id is None:
|
||||
holder_id = _insert_secret_holder_account(
|
||||
conn,
|
||||
platform_key=key,
|
||||
account_label=safe_label,
|
||||
environment=resolved_env,
|
||||
role=resolved_role,
|
||||
provider_code=resolved_provider,
|
||||
url=resolved_url,
|
||||
now=now,
|
||||
)
|
||||
cred_id = insert_credential_encrypted(
|
||||
conn,
|
||||
account_id=holder_id,
|
||||
platform_key=key,
|
||||
credential_label=safe_label,
|
||||
credential_type=credential_type,
|
||||
plaintext=plaintext,
|
||||
expires_at=None,
|
||||
extra_json=resolved_extra,
|
||||
now=now,
|
||||
)
|
||||
conn.commit()
|
||||
except MasterKeyMissingError as e:
|
||||
conn.rollback()
|
||||
_say(_err_json("ERR_MASTER_KEY_MISSING", str(e)))
|
||||
return
|
||||
except CredentialDecryptError as e:
|
||||
conn.rollback()
|
||||
_say(_err_json("ERR_CREDENTIAL_DECRYPT_FAILED", str(e)))
|
||||
return
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
log.exception("account_add_secret_failure")
|
||||
_say(_err_json("ERROR:DB_ERROR", "Database insert failed."))
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
log.info(
|
||||
"account_add_secret_encrypted_success cred_id=%s account_id=%s",
|
||||
cred_id,
|
||||
holder_id,
|
||||
)
|
||||
_say(_ok_json({
|
||||
"credential_id": cred_id,
|
||||
"account_id": holder_id,
|
||||
"platform_key": key,
|
||||
"credential_type": credential_type,
|
||||
"secret_storage": "local_encrypted",
|
||||
"secret_mask": secret_mask_out,
|
||||
}))
|
||||
return
|
||||
|
||||
if storage_n == "env":
|
||||
secret_ref_value = secret_ref.strip()
|
||||
secret_mask_value = mask_secret(f"env:{secret_ref_value}")
|
||||
extra = {}
|
||||
if extra_json_str:
|
||||
try:
|
||||
extra = json.loads(extra_json_str)
|
||||
except json.JSONDecodeError:
|
||||
extra = {}
|
||||
resolved_env = environment or "production"
|
||||
resolved_role = role or "api"
|
||||
extra["environment"] = resolved_env
|
||||
extra["role"] = resolved_role
|
||||
extra_json_final = json.dumps(extra, ensure_ascii=False)
|
||||
|
||||
resolved_url = (platform_spec.get("default_url") or "").strip() or None
|
||||
resolved_provider = platform_spec.get("provider_code") or key
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
seed_platforms(conn)
|
||||
holder_id = account_id
|
||||
if holder_id is None:
|
||||
holder_id = _insert_secret_holder_account(
|
||||
conn,
|
||||
platform_key=key,
|
||||
account_label=safe_label,
|
||||
environment=resolved_env,
|
||||
role=resolved_role,
|
||||
provider_code=resolved_provider,
|
||||
url=resolved_url,
|
||||
now=now,
|
||||
)
|
||||
cred_id = insert_credential(
|
||||
conn=conn,
|
||||
account_id=holder_id,
|
||||
platform_key=key,
|
||||
credential_label=safe_label,
|
||||
credential_type=credential_type,
|
||||
secret_storage=storage_n,
|
||||
secret_ref=secret_ref_value,
|
||||
secret_mask=secret_mask_value,
|
||||
expires_at=None,
|
||||
extra_json=extra_json_final,
|
||||
now=now,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
log.exception("account_add_secret_failure")
|
||||
_say(_err_json("ERROR:DB_ERROR", "Database insert failed."))
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
log.info("account_add_secret_success id=%s platform=%s storage=env ref=%s",
|
||||
cred_id, key, secret_ref_value)
|
||||
_say(_ok_json({
|
||||
"account_id": holder_id,
|
||||
"credential_id": cred_id,
|
||||
"platform_key": key,
|
||||
"credential_label": safe_label,
|
||||
"credential_type": credential_type,
|
||||
"secret_storage": storage_n,
|
||||
"secret_ref": secret_ref_value,
|
||||
"secret_mask": secret_mask_value,
|
||||
"warning": "env 存储方式:请确保环境变量在运行时可用。",
|
||||
}))
|
||||
return
|
||||
|
||||
if storage_n == "windows_credential":
|
||||
if not secret_stdin:
|
||||
_say(_err_json("ERROR:CLI_BAD_ARGS",
|
||||
"windows_credential 需要 --secret-stdin 从 stdin 读取 secret。"))
|
||||
return
|
||||
secret_value = sys.stdin.readline().strip()
|
||||
if not secret_value:
|
||||
_say(_err_json("ERROR:CLI_BAD_ARGS", "stdin 中未读到 secret 值。"))
|
||||
return
|
||||
|
||||
secret_mask_value = mask_secret(secret_value)
|
||||
cred_label_for_target = safe_label
|
||||
target = make_windows_credential_target(key, credential_type, cred_label_for_target)
|
||||
|
||||
try:
|
||||
write_secret("windows_credential", target, secret_value)
|
||||
except (OSError, ValueError) as e:
|
||||
log.error("secret_write_failed storage=windows_credential error=%s", str(e))
|
||||
_say(_err_json("ERROR:SECRET_WRITE_FAILED", f"写 Windows Credential Manager 失败:{e}"))
|
||||
return
|
||||
|
||||
extra = {}
|
||||
if extra_json_str:
|
||||
try:
|
||||
extra = json.loads(extra_json_str)
|
||||
except json.JSONDecodeError:
|
||||
extra = {}
|
||||
resolved_env = environment or "production"
|
||||
resolved_role = role or "api"
|
||||
extra["environment"] = resolved_env
|
||||
extra["role"] = resolved_role
|
||||
extra_json_final = json.dumps(extra, ensure_ascii=False)
|
||||
|
||||
resolved_url = (platform_spec.get("default_url") or "").strip() or None
|
||||
resolved_provider = platform_spec.get("provider_code") or key
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
seed_platforms(conn)
|
||||
holder_id = account_id
|
||||
if holder_id is None:
|
||||
holder_id = _insert_secret_holder_account(
|
||||
conn,
|
||||
platform_key=key,
|
||||
account_label=safe_label,
|
||||
environment=resolved_env,
|
||||
role=resolved_role,
|
||||
provider_code=resolved_provider,
|
||||
url=resolved_url,
|
||||
now=now,
|
||||
)
|
||||
cred_id = insert_credential(
|
||||
conn=conn,
|
||||
account_id=holder_id,
|
||||
platform_key=key,
|
||||
credential_label=safe_label,
|
||||
credential_type=credential_type,
|
||||
secret_storage=storage_n,
|
||||
secret_ref=target,
|
||||
secret_mask=secret_mask_value,
|
||||
expires_at=None,
|
||||
extra_json=extra_json_final,
|
||||
now=now,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
log.exception("account_add_secret_failure")
|
||||
_say(_err_json("ERROR:DB_ERROR", "Database insert failed."))
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
log.info("account_add_secret_success id=%s platform=%s storage=windows_credential",
|
||||
cred_id, key)
|
||||
secret_value = ""
|
||||
|
||||
_say(_ok_json({
|
||||
"account_id": holder_id,
|
||||
"credential_id": cred_id,
|
||||
"platform_key": key,
|
||||
"credential_label": safe_label,
|
||||
"credential_type": credential_type,
|
||||
"secret_storage": storage_n,
|
||||
"secret_ref": target,
|
||||
"secret_mask": secret_mask_value,
|
||||
}))
|
||||
return
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
seed_platforms(conn)
|
||||
cred_id = insert_credential(
|
||||
conn=conn,
|
||||
account_id=account_id,
|
||||
platform_key=key,
|
||||
credential_label=safe_label,
|
||||
credential_type=credential_type,
|
||||
secret_storage=storage_n,
|
||||
secret_ref=None,
|
||||
secret_mask="",
|
||||
expires_at=None,
|
||||
extra_json=extra_json_str or "{}",
|
||||
now=now,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
log.exception("account_add_secret_failure")
|
||||
_say(_err_json("ERROR:DB_ERROR", "Database insert failed."))
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
log.info("account_add_secret_success id=%s platform=%s storage=none", cred_id, key)
|
||||
_say(_ok_json({
|
||||
"account_id": account_id,
|
||||
"credential_id": cred_id,
|
||||
"platform_key": key,
|
||||
"credential_label": safe_label,
|
||||
"credential_type": credential_type,
|
||||
"secret_storage": storage_n,
|
||||
"secret_ref": "",
|
||||
"secret_mask": "",
|
||||
}))
|
||||
Reference in New Issue
Block a user