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 };
},
};