feat: AI账号api_key加密存储 + 余额刷新接口与前端按钮

This commit is contained in:
gouki
2026-08-02 17:26:59 +00:00
parent 1e574315bd
commit 2e3e9f8b0b
7 changed files with 124 additions and 5 deletions
+13 -1
View File
@@ -68,7 +68,9 @@ def _detail_to_read(detail):
if isinstance(detail, DomainDetail):
return DomainDetailRead.model_validate(detail)
if isinstance(detail, AIAccount):
return AIAccountRead.model_validate(detail)
read = AIAccountRead.model_validate(detail)
read.has_api_key = bool(detail.api_key_encrypted or detail.api_key)
return read
return None
@@ -103,6 +105,10 @@ def _build_detail(asset_type: AssetType, asset_id: int, detail_in):
data["ssh_key_encrypted"] = crypto.encrypt(detail_in.ssh_key)
data["password_encrypted"] = crypto.encrypt(detail_in.password)
return model(asset_id=asset_id, **data)
if asset_type == AssetType.AI_AGENT:
data = detail_in.model_dump(exclude={"api_key"})
data["api_key_encrypted"] = crypto.encrypt(detail_in.api_key)
return model(asset_id=asset_id, **data)
return model(asset_id=asset_id, **detail_in.model_dump())
@@ -116,6 +122,12 @@ def _apply_detail_update(asset_type: AssetType, existing, detail_in) -> None:
existing.ssh_key_encrypted = crypto.encrypt(detail_in.ssh_key)
if detail_in.password is not None:
existing.password_encrypted = crypto.encrypt(detail_in.password)
elif asset_type == AssetType.AI_AGENT:
data = detail_in.model_dump(exclude={"api_key"})
for key, value in data.items():
setattr(existing, key, value)
if detail_in.api_key is not None:
existing.api_key_encrypted = crypto.encrypt(detail_in.api_key)
else:
for key, value in detail_in.model_dump().items():
setattr(existing, key, value)
+72
View File
@@ -16,6 +16,7 @@ from app.adapters import registry
from app.adapters.base import BaseAdapter
from app.core import crypto
from app.models.asset import (
AIAccount,
Asset,
AssetStatus,
AssetType,
@@ -223,3 +224,74 @@ def sync_provider(session: Session, provider_id: int) -> dict:
session.commit()
result["last_synced_at"] = provider.last_synced_at.isoformat()
return result
# AI 服务商 slug -> sdk_type 映射(Provider 未配 sdk_type 时的回退推断)
_AI_SDK_MAP = {
"openai": "openai-api",
"deepseek": "deepseek-api",
"kimi": "moonshot-api",
"moonshot": "moonshot-api",
"minimax": "minimax-api",
}
def refresh_ai_balance(session: Session, asset_id: int) -> dict:
"""刷新 AI 账号余额:解密 api_key → 适配器 get_account → 更新余额"""
asset = session.get(Asset, asset_id)
if not asset or asset.asset_type != AssetType.AI_AGENT:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="非 AI 账号资产"
)
ai = session.exec(select(AIAccount).where(AIAccount.asset_id == asset_id)).first()
if not ai:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="AI 账号详情不存在"
)
sdk_type = None
if asset.provider_id:
provider = session.get(Provider, asset.provider_id)
if provider:
sdk_type = provider.sdk_type
if not sdk_type:
sdk_type = _AI_SDK_MAP.get((ai.provider or "").lower())
if not sdk_type or not registry.is_supported(sdk_type):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"无法确定 AI 适配器({ai.provider}",
)
api_key = crypto.decrypt(ai.api_key_encrypted) or ai.api_key
if not api_key:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="未配置 API Key"
)
adapter = registry.get_adapter(sdk_type, {"api_key": api_key})
try:
acc = adapter.get_account()
except NotImplementedError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"{sdk_type} 暂不支持余额查询",
)
except HTTPException:
raise
except Exception as e: # noqa: BLE001
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, detail=f"查询余额失败:{e}"
)
if acc.balance is not None:
ai.balance = acc.balance
ai.currency = acc.currency
ai.last_synced_at = datetime.utcnow()
session.add(ai)
session.commit()
session.refresh(ai)
return {
"balance": ai.balance,
"currency": ai.currency,
"last_synced_at": ai.last_synced_at.isoformat(),
}