中控 Bridge 增加远程快速更新:/api/hub/version 与 /api/hub/update。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -388,6 +388,27 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None:
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
@app.route("/api/hub/version", methods=["GET"])
|
||||||
|
def hub_version():
|
||||||
|
"""中控探测客户端当前 git 版本/在线."""
|
||||||
|
if not hub_bridge_token_ok(request.headers):
|
||||||
|
return jsonify({"ok": False, "msg": "未授权"}), 401
|
||||||
|
from lib.instance.instance_update_lib import read_git_version
|
||||||
|
|
||||||
|
ver = read_git_version()
|
||||||
|
ver["online"] = True
|
||||||
|
return jsonify(ver)
|
||||||
|
|
||||||
|
@app.route("/api/hub/update", methods=["POST"])
|
||||||
|
def hub_update():
|
||||||
|
"""中控远程快速更新:git pull + pm2 restart(异步)."""
|
||||||
|
if not hub_bridge_token_ok(request.headers):
|
||||||
|
return jsonify({"ok": False, "msg": "未授权"}), 401
|
||||||
|
from lib.instance.instance_update_lib import schedule_pull_and_restart
|
||||||
|
|
||||||
|
out = schedule_pull_and_restart(delay_seconds=1.5)
|
||||||
|
return jsonify(out), (200 if out.get("ok") else 400)
|
||||||
|
|
||||||
@app.route("/api/hub/backup/list", methods=["GET"])
|
@app.route("/api/hub/backup/list", methods=["GET"])
|
||||||
def hub_backup_list():
|
def hub_backup_list():
|
||||||
if not hub_bridge_token_ok(request.headers):
|
if not hub_bridge_token_ok(request.headers):
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""客户端远程快速更新:git pull + pm2 restart(供中控 Bridge 触发)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from lib.paths import REPO_ROOT
|
||||||
|
|
||||||
|
_update_lock = threading.Lock()
|
||||||
|
_update_pending_until = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_root() -> Path:
|
||||||
|
env_root = (os.getenv("REPO") or "").strip()
|
||||||
|
if env_root:
|
||||||
|
return Path(env_root)
|
||||||
|
return Path(REPO_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_git(args: list[str], *, cwd: Path) -> tuple[bool, str]:
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
["git", *args],
|
||||||
|
cwd=str(cwd),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "未找到 git"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "git 超时"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e) or "git 失败"
|
||||||
|
out = (proc.stdout or "").strip() or (proc.stderr or "").strip()
|
||||||
|
return proc.returncode == 0, out
|
||||||
|
|
||||||
|
|
||||||
|
def read_git_version() -> dict[str, Any]:
|
||||||
|
"""读取当前部署目录 git 版本信息."""
|
||||||
|
root = _repo_root()
|
||||||
|
out: dict[str, Any] = {
|
||||||
|
"ok": True,
|
||||||
|
"repo": str(root),
|
||||||
|
"sha": "",
|
||||||
|
"short_sha": "",
|
||||||
|
"branch": "",
|
||||||
|
"dirty": False,
|
||||||
|
"msg": "",
|
||||||
|
}
|
||||||
|
if not (root / ".git").exists() and not (root / ".git").is_file():
|
||||||
|
# worktree 时 .git 可能是文件
|
||||||
|
out["ok"] = False
|
||||||
|
out["msg"] = "非 git 仓库"
|
||||||
|
return out
|
||||||
|
ok, sha = _run_git(["rev-parse", "HEAD"], cwd=root)
|
||||||
|
if not ok or not sha:
|
||||||
|
out["ok"] = False
|
||||||
|
out["msg"] = sha or "无法读取 HEAD"
|
||||||
|
return out
|
||||||
|
out["sha"] = sha
|
||||||
|
out["short_sha"] = sha[:7]
|
||||||
|
ok_b, branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=root)
|
||||||
|
if ok_b and branch:
|
||||||
|
out["branch"] = branch
|
||||||
|
ok_d, dirty = _run_git(["status", "--porcelain"], cwd=root)
|
||||||
|
if ok_d:
|
||||||
|
out["dirty"] = bool(dirty.strip())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_pull_and_restart(*, delay_seconds: float = 1.5) -> dict[str, Any]:
|
||||||
|
"""延迟执行 deploy/pull_and_restart.sh,便于 Bridge 响应先返回."""
|
||||||
|
if not sys.platform.startswith("linux"):
|
||||||
|
return {"ok": False, "msg": "仅 Linux 服务器支持远程更新", "deferred": False}
|
||||||
|
root = _repo_root()
|
||||||
|
script = root / "deploy" / "pull_and_restart.sh"
|
||||||
|
if not script.is_file():
|
||||||
|
return {"ok": False, "msg": f"缺少更新脚本: {script}", "deferred": False}
|
||||||
|
|
||||||
|
global _update_pending_until
|
||||||
|
now = time.time()
|
||||||
|
with _update_lock:
|
||||||
|
if now < _update_pending_until:
|
||||||
|
return {"ok": False, "msg": "已有更新任务进行中,请稍后再试", "deferred": False}
|
||||||
|
_update_pending_until = now + 120.0
|
||||||
|
|
||||||
|
version = read_git_version()
|
||||||
|
try:
|
||||||
|
delay = max(0.5, float(delay_seconds))
|
||||||
|
cmd = (
|
||||||
|
f"sleep {delay} && "
|
||||||
|
f"cd {shlex.quote(str(root))} && "
|
||||||
|
f"exec bash {shlex.quote(str(script))}"
|
||||||
|
)
|
||||||
|
subprocess.Popen(
|
||||||
|
["bash", "-c", cmd],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
cwd=str(root),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"msg": "更新已受理,即将 git pull 并重启",
|
||||||
|
"deferred": True,
|
||||||
|
"before_sha": version.get("sha") or "",
|
||||||
|
"before_short_sha": version.get("short_sha") or "",
|
||||||
|
"branch": version.get("branch") or "",
|
||||||
|
}
|
||||||
|
except FileNotFoundError:
|
||||||
|
with _update_lock:
|
||||||
|
_update_pending_until = 0.0
|
||||||
|
return {"ok": False, "msg": "未找到 bash", "deferred": False}
|
||||||
|
except Exception as e:
|
||||||
|
with _update_lock:
|
||||||
|
_update_pending_until = 0.0
|
||||||
|
return {"ok": False, "msg": str(e) or "触发更新失败", "deferred": False}
|
||||||
Reference in New Issue
Block a user