From aaa72c79619b55c9485808dfad5aa9f9e084e0bd Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 8 Jul 2026 23:06:31 +0800 Subject: [PATCH] Fix hub AI save: background PM2 restart after JSON response. Co-authored-by: Cursor --- lib/env/shared_env_lib.py | 13 +++++++++---- manual_trading_hub/hub.py | 18 ++++++------------ manual_trading_hub/hub_env_lib.py | 2 +- manual_trading_hub/static/app.js | 18 +++++++++++++++--- manual_trading_hub/static/index.html | 2 +- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/env/shared_env_lib.py b/lib/env/shared_env_lib.py index ceef445..af2634a 100644 --- a/lib/env/shared_env_lib.py +++ b/lib/env/shared_env_lib.py @@ -195,22 +195,27 @@ def apply_ai_env_to_all(updates: dict[str, str]) -> dict[str, Any]: } -def restart_hub_and_instances_pm2() -> dict[str, Any]: +def restart_instances_then_hub_pm2() -> dict[str, Any]: + """先重启三实例,最后重启中控(避免当前请求被中断)。""" if not sys.platform.startswith("linux"): return {"ok": False, "msg": "仅 Linux 服务器支持 PM2 重启", "results": []} from lib.instance.instance_pm2_lib import restart_instance_pm2 - apps = ["manual-trading-hub"] results: list[dict[str, Any]] = [] - hub_result = _restart_pm2_app("manual-trading-hub") - results.append({"app": "manual-trading-hub", **hub_result}) for ex in ("okx", "binance", "gate"): r = restart_instance_pm2(ex) results.append({"exchange": ex, **r}) + hub_result = _restart_pm2_app("manual-trading-hub") + results.append({"app": "manual-trading-hub", **hub_result}) ok = all(r.get("ok") for r in results) return {"ok": ok, "results": results} +def restart_hub_and_instances_pm2() -> dict[str, Any]: + """兼容旧调用:与 restart_instances_then_hub_pm2 相同顺序。""" + return restart_instances_then_hub_pm2() + + def _restart_pm2_app(app_name: str) -> dict[str, Any]: try: proc = subprocess.run( diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 9dc6ee6..a7c2da7 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -110,7 +110,7 @@ from env_load import load_hub_dotenv load_hub_dotenv() import httpx -from fastapi import Body, FastAPI, File, Form, HTTPException, Request, UploadFile +from fastapi import BackgroundTasks, Body, FastAPI, File, Form, HTTPException, Request, UploadFile from fastapi.responses import FileResponse, HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field @@ -1706,27 +1706,21 @@ def api_get_ai_env(request: Request): @app.post("/api/settings/ai-env") -def api_save_ai_env(request: Request, body: HubAiEnvBody): +def api_save_ai_env(request: Request, body: HubAiEnvBody, background_tasks: BackgroundTasks): _require_hub_logged_in(request) from hub_env_lib import get_hub_ai_env_payload, restart_all_pm2, save_hub_ai_env result = save_hub_ai_env(body.values or {}) if not result.get("ok"): raise HTTPException(status_code=400, detail="; ".join(result.get("errors") or ["保存失败"])) - restart_result = None - if body.restart and result.get("restart_required"): - restart_result = restart_all_pm2() - if not restart_result.get("ok"): - raise HTTPException( - status_code=500, - detail="保存成功但 PM2 重启失败,请手动 restart", - ) payload = get_hub_ai_env_payload() + restart_required = bool(body.restart and result.get("restart_required")) + if restart_required: + background_tasks.add_task(restart_all_pm2) return { "ok": True, "changed": result.get("changed") or {}, - "restart_required": bool(result.get("restart_required")), - "restart": restart_result, + "restart_required": restart_required, "sync_status": payload.get("sync_status"), } diff --git a/manual_trading_hub/hub_env_lib.py b/manual_trading_hub/hub_env_lib.py index ffc9ece..94ee7f8 100644 --- a/manual_trading_hub/hub_env_lib.py +++ b/manual_trading_hub/hub_env_lib.py @@ -41,7 +41,7 @@ def save_hub_ai_env(updates: dict[str, str]) -> dict[str, Any]: def restart_all_pm2() -> dict[str, Any]: - return restart_hub_and_instances_pm2() + return restart_instances_then_hub_pm2() def restart_hub_pm2() -> dict[str, Any]: diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index b30b4d5..65224ad 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -4409,6 +4409,16 @@ return values; } + async function parseApiJson(r) { + const ct = (r.headers.get("content-type") || "").toLowerCase(); + if (!ct.includes("application/json")) { + const text = await r.text(); + const snippet = (text || "").replace(/\s+/g, " ").trim().slice(0, 120); + throw new Error(snippet ? `服务返回非 JSON:${snippet}` : `HTTP ${r.status}`); + } + return r.json(); + } + async function saveHubAiEnv() { const status = document.getElementById("hub-ai-env-save-status"); const setStatus = (msg, err) => { @@ -4423,15 +4433,17 @@ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: collectHubAiEnvValues(), restart: true }), }); - const j = await r.json(); + const j = await parseApiJson(r); if (!r.ok) throw new Error(j.detail || j.msg || "保存失败"); if (!j.changed || !Object.keys(j.changed).length) { setStatus("未修改(与当前配置相同)"); showToast("未修改"); return; } - setStatus("已同步三所,服务重启中…"); - await waitHubHealth(); + if (j.restart_required) { + setStatus("已同步三所,服务重启中…"); + await waitHubHealth(); + } setStatus("AI 配置已保存并同步至三所实例"); showToast("AI 配置已保存并同步"); await loadHubAiEnvSettings(); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 62a812c..8b42f08 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -1192,6 +1192,6 @@ - +