refactor: 凭证下沉账号层——平台不再持有API凭证;账号支持登录凭证+API配置,SDK测试/同步改为账号维度,同步产出自动挂账号;含存量数据自动迁移
This commit is contained in:
@@ -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]:
|
||||
"""资产库会话(默认)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user