diff --git a/app/main.py b/app/main.py index ad1e7f0..e3e63d9 100644 --- a/app/main.py +++ b/app/main.py @@ -19,7 +19,7 @@ from sqlmodel import Session from app.core.config import settings from app.core.seed import seed_providers from app.database import assets_engine, init_db -from app.routers import agent, assets, monitor, providers, stats +from app.routers import agent, assets, monitor, providers, stats, sync BASE_DIR = Path(__file__).resolve().parent.parent STATIC_DIR = BASE_DIR / "static" @@ -53,6 +53,7 @@ app.include_router(providers.router) app.include_router(stats.router) app.include_router(agent.router) app.include_router(monitor.router) +app.include_router(sync.router) app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") diff --git a/app/routers/sync.py b/app/routers/sync.py new file mode 100644 index 0000000..13aaa27 --- /dev/null +++ b/app/routers/sync.py @@ -0,0 +1,19 @@ +"""同步相关路由(批量同步操作)""" + +from fastapi import APIRouter, Depends +from sqlmodel import Session + +from app.core.security import require_api_key +from app.database import get_session +from app.services import sync_service + +router = APIRouter(prefix="/api/sync", tags=["sync"]) + + +@router.post( + "/ai-balances", + summary="同步所有 AI 账号余额", + dependencies=[Depends(require_api_key)], +) +def sync_ai_balances(session: Session = Depends(get_session)) -> dict: + return sync_service.sync_all_ai_balances(session) diff --git a/app/services/sync_service.py b/app/services/sync_service.py index f32c49f..2c2593b 100644 --- a/app/services/sync_service.py +++ b/app/services/sync_service.py @@ -295,3 +295,30 @@ def refresh_ai_balance(session: Session, asset_id: int) -> dict: "currency": ai.currency, "last_synced_at": ai.last_synced_at.isoformat(), } + + +def sync_all_ai_balances(session: Session) -> dict: + """遍历所有 AI 账号资产,逐个刷新余额(单个失败不中断整体)""" + ai_assets = session.exec( + select(Asset).where(Asset.asset_type == AssetType.AI_AGENT) + ).all() + success = 0 + failed = 0 + errors = [] + for asset in ai_assets: + try: + refresh_ai_balance(session, asset.id) + success += 1 + except HTTPException as e: + failed += 1 + errors.append(f"{asset.name}: {e.detail}") + except Exception as e: # noqa: BLE001 + failed += 1 + errors.append(f"{asset.name}: {e}") + return { + "total": len(ai_assets), + "success": success, + "failed": failed, + "errors": errors, + "synced_at": datetime.utcnow().isoformat(), + } diff --git a/deploy/vps-sync-ai.service b/deploy/vps-sync-ai.service new file mode 100644 index 0000000..3210980 --- /dev/null +++ b/deploy/vps-sync-ai.service @@ -0,0 +1,13 @@ +# AI 账号余额同步服务(oneshot,由 timer 触发) +# 部署位置:/etc/systemd/system/vps-sync-ai.service + +[Unit] +Description=Sync all AI account balances +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +WorkingDirectory=/opt/vps-manager +EnvironmentFile=-/opt/vps-manager/.env +ExecStart=/opt/vps-manager/.venv/bin/python /opt/vps-manager/scripts/sync_ai_balances.py diff --git a/deploy/vps-sync-ai.timer b/deploy/vps-sync-ai.timer new file mode 100644 index 0000000..ad60083 --- /dev/null +++ b/deploy/vps-sync-ai.timer @@ -0,0 +1,13 @@ +# AI 账号余额每日同步定时器 +# 部署位置:/etc/systemd/system/vps-sync-ai.timer + +[Unit] +Description=Daily sync of AI account balances + +[Timer] +OnCalendar=daily +Persistent=true +Unit=vps-sync-ai.service + +[Install] +WantedBy=timers.target diff --git a/scripts/sync_ai_balances.py b/scripts/sync_ai_balances.py new file mode 100644 index 0000000..7fdb8b0 --- /dev/null +++ b/scripts/sync_ai_balances.py @@ -0,0 +1,32 @@ +"""定时同步所有 AI 账号余额(供 systemd timer 调用) + +用法:python scripts/sync_ai_balances.py +依赖 .env 中的 MASTER_KEY(解密各账号 api_key)。 +""" + +import sys +from pathlib import Path + +# 确保能导入 app 包(脚本位于 scripts/ 子目录) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from sqlmodel import Session + +from app.database import assets_engine, init_db +from app.services import sync_service + + +def main() -> int: + init_db() + with Session(assets_engine) as session: + result = sync_service.sync_all_ai_balances(session) + print( + f"[AI余额同步] 总计 {result['total']} | 成功 {result['success']} | 失败 {result['failed']}" + ) + for err in result["errors"]: + print(f" - {err}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/static/js/app.js b/static/js/app.js index 9f31666..70e4820 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -226,6 +226,7 @@ const AssetsView = { + 共 {{ store.assets.length }} 条 @@ -297,7 +298,15 @@ const AssetsView = { await loadAssets(); } catch (e) { alert('刷新失败:' + e.message); } } - return { store, Fmt, reload: loadAssets, edit: openAssetEdit, del: deleteAsset, viewServer, refreshBalance }; + async function syncAllAI() { + if (!confirm('同步所有 AI 账号的余额?')) return; + try { + const r = await Api.post('/sync/ai-balances', {}); + alert('同步完成:成功 ' + r.success + ',失败 ' + r.failed + (r.errors && r.errors.length ? '\n' + r.errors.join('\n') : '')); + await loadAssets(); + } catch (e) { alert('同步失败:' + e.message); } + } + return { store, Fmt, reload: loadAssets, edit: openAssetEdit, del: deleteAsset, viewServer, refreshBalance, syncAllAI }; }, };