refactor: 凭证下沉账号层——平台不再持有API凭证;账号支持登录凭证+API配置,SDK测试/同步改为账号维度,同步产出自动挂账号;含存量数据自动迁移

This commit is contained in:
gouki
2026-08-09 16:50:45 +00:00
parent cf18cadb6b
commit dc2134cf5a
9 changed files with 355 additions and 103 deletions
+90
View File
@@ -98,6 +98,18 @@ def _migrate_assets_db() -> None:
conn.execute(sa.text("ALTER TABLE ai_accounts ADD COLUMN api_key_encrypted VARCHAR"))
# 迁移:将明文 api_key 加密后存入 api_key_encrypted,并清空原字段
_migrate_plaintext_api_keys(conn)
if insp.has_table("accounts"):
cols = {c["name"] for c in insp.get_columns("accounts")}
if "login_user" not in cols:
conn.execute(sa.text("ALTER TABLE accounts ADD COLUMN login_user VARCHAR"))
if "login_password_encrypted" not in cols:
conn.execute(sa.text("ALTER TABLE accounts ADD COLUMN login_password_encrypted VARCHAR"))
if "api_config_encrypted" not in cols:
conn.execute(sa.text("ALTER TABLE accounts ADD COLUMN api_config_encrypted VARCHAR"))
if "last_synced_at" not in cols:
conn.execute(sa.text("ALTER TABLE accounts ADD COLUMN last_synced_at DATETIME"))
# 凭证下沉:平台 api_config 迁到默认账号;AI 资产自动挂靠账号
_backfill_account_credentials(conn)
def _migrate_indexes() -> None:
@@ -151,6 +163,84 @@ def _backfill_provider_services(conn) -> None:
)
def _backfill_account_credentials(conn) -> None:
"""凭证下沉迁移(幂等,仅在首次加列后产生效果):
1. 平台凭证下沉:api_config_encrypted 非空的 Provider → 确保存在
{slug}-默认」账号并拷入凭证(不删平台原值,保留可回滚)。
2. AI 资产挂靠:account 为空的 ai_agent 资产 → 按 ai_accounts.provider
找/建账号并关联;账号无 API 配置时把该资产的 api_key 写入账号配置。
"""
import json
from datetime import datetime, timezone
import sqlalchemy as sa
from app.core.crypto import decrypt, encrypt
def _ensure_account(name: str, platform: str) -> int:
row = conn.execute(sa.text("SELECT id FROM accounts WHERE name = :name"), {"name": name}).first()
if row:
return row[0]
conn.execute(
sa.text("INSERT INTO accounts (name, platform, created_at) VALUES (:name, :platform, :ts)"),
{"name": name, "platform": platform, "ts": datetime.now(timezone.utc).replace(tzinfo=None)},
)
return conn.execute(sa.text("SELECT id FROM accounts WHERE name = :name"), {"name": name}).first()[0]
# 1. 平台凭证下沉到默认账号
for pid, slug, cfg in conn.execute(
sa.text(
"SELECT id, slug, api_config_encrypted FROM providers "
"WHERE api_config_encrypted IS NOT NULL AND api_config_encrypted != ''"
)
).fetchall():
acc_name = f"{slug}-默认"
_ensure_account(acc_name, slug)
conn.execute(
sa.text(
"UPDATE accounts SET api_config_encrypted = :cfg "
"WHERE name = :name AND (api_config_encrypted IS NULL OR api_config_encrypted = '')"
),
{"cfg": cfg, "name": acc_name},
)
# 2. 无账号的 AI 资产按 provider 挂靠
rows = conn.execute(
sa.text(
"SELECT a.id, COALESCE(ai.provider, '') FROM assets a "
"JOIN ai_accounts ai ON ai.asset_id = a.id "
"WHERE a.asset_type = 'ai_agent' AND (a.account IS NULL OR a.account = '')"
)
).fetchall()
for asset_id, provider in rows:
if not provider:
continue
_ensure_account(provider, provider)
conn.execute(
sa.text("UPDATE assets SET account = :acc WHERE id = :id"),
{"acc": provider, "id": asset_id},
)
# 账号尚无 API 配置时,把该资产的 api_key 写入账号配置(解密后重组 JSON 再加密)
key_row = conn.execute(
sa.text("SELECT api_key_encrypted FROM ai_accounts WHERE asset_id = :id"),
{"id": asset_id},
).first()
if key_row and key_row[0]:
api_key = decrypt(key_row[0])
if api_key:
cfg_json = json.dumps({"api_key": api_key})
cfg_enc = encrypt(cfg_json)
if cfg_enc:
conn.execute(
sa.text(
"UPDATE accounts SET api_config_encrypted = :cfg "
"WHERE name = :name AND (api_config_encrypted IS NULL OR api_config_encrypted = '')"
),
{"cfg": cfg_enc, "name": provider},
)
def get_session() -> Generator[Session, None, None]:
"""资产库会话(默认)