feat: AI账号api_key加密存储 + 余额刷新接口与前端按钮
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user