#!/usr/bin/env python3 """ 光湖本地中继引擎 · Local Gatekeeper v1.0 监听 localhost:3912,WorkBuddy 通过它操作六台服务器。 本地引擎负责:密钥管理、代理绕过、分chunk传输、命令路由。 用法: python3 local-gatekeeper.py # 启动 HTTP 服务 python3 local-gatekeeper.py --kill # 停止服务 python3 local-gatekeeper.py --status # 查看状态 """ import argparse import base64 import json import os import signal import subprocess import sys import time from http.server import HTTPServer, BaseHTTPRequestHandler from pathlib import Path PID_FILE = os.path.expanduser("~/.workbuddy/local-gatekeeper.pid") CONFIG_FILE = os.path.expanduser("~/.workbuddy/local-gatekeeper-config.json") # ============================================================ # 服务器配置 — 从 gatekeeper-deployment.json 同步 # ============================================================ SERVERS = { "BS-GZ-006": { "name": "广州 · 代码仓库", "ip": "43.139.217.141", "port": 3910, "key": "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", "repo": "/opt/guanghulab-repo", }, "BS-SG-001": { "name": "新加坡 · 铸渊大脑", "ip": "43.156.237.110", "port": 3911, "key": "zy_gtw_c38be341d65043eee0ba51a214a267832a9ae46dd73e423c", "repo": "/opt/zhuyuan/guanghulab", "version": "2.0.0", }, "BS-SG-002": { "name": "新加坡 · 面孔", "ip": "43.134.16.246", "port": 3910, "key": "zy_gtw_ceb06d8a70d0cd38678239b3a1a9b2ba5cba77a3fbe5ebbf", "version": "2.0.0", }, "BS-SG-003": { "name": "新加坡 · 中继", "ip": "43.153.193.169", "port": 3910, "key": "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", }, "ZY-SG-006": { "name": "新加坡 · 语料", "ip": "43.153.203.105", "port": 3910, "key": "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", }, "BS-SH-005": { "name": "上海 · 国内节点", "ip": "124.223.10.33", "port": 3910, "key": "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", }, } CHUNK_SIZE = 6000 CMD_TIMEOUT = 30 # ============================================================ # 核心:直连 Gatekeeper 执行命令(绕过代理) # ============================================================ def gk_exec(server_code: str, cmd: str, timeout: int = None) -> dict: """通过指定服务器的 Gatekeeper 执行命令""" if server_code not in SERVERS: return {"ok": False, "error": f"未知服务器: {server_code}"} srv = SERVERS[server_code] url = f"http://{srv['ip']}:{srv['port']}/exec" t = timeout or CMD_TIMEOUT env = os.environ.copy() for k in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"]: env.pop(k, None) try: result = subprocess.run( ["curl", "-s", "--connect-timeout", str(t), "--max-time", str(t), "--noproxy", "*", "-X", "POST", url, "-H", "Content-Type: application/json", "-H", f"Authorization: Bearer {srv['key']}", "-d", json.dumps({"cmd": cmd})], capture_output=True, text=True, timeout=t + 5, env=env ) try: return json.loads(result.stdout) except json.JSONDecodeError: return {"ok": False, "error": f"非JSON响应", "raw": result.stdout[:500], "stderr": result.stderr} except subprocess.TimeoutExpired: return {"ok": False, "error": f"超时 ({t}s)"} except Exception as e: return {"ok": False, "error": str(e)} def gk_health(server_code: str) -> dict: """健康检查""" if server_code not in SERVERS: return {"ok": False, "error": f"未知服务器: {server_code}"} srv = SERVERS[server_code] url = f"http://{srv['ip']}:{srv['port']}/health" env = os.environ.copy() for k in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"]: env.pop(k, None) try: result = subprocess.run( ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--connect-timeout", "5", "--noproxy", "*", url], capture_output=True, text=True, timeout=10, env=env ) code = result.stdout.strip() return {"ok": True, "http_code": code, "reachable": code in ("200", "404", "405")} except Exception as e: return {"ok": False, "error": str(e), "reachable": False} def sync_file(server_code: str, local_path: str, target_path: str, commit_msg: str) -> dict: """同步文件到指定服务器仓库""" if server_code not in SERVERS: return {"ok": False, "error": f"未知服务器: {server_code}"} srv = SERVERS[server_code] if "repo" not in srv: return {"ok": False, "error": f"服务器 {server_code} 未配置仓库路径"} # 读本地文件 if not os.path.exists(local_path): return {"ok": False, "error": f"本地文件不存在: {local_path}"} with open(local_path, "rb") as f: content = f.read() expected_bytes = len(content) b64 = base64.b64encode(content).decode("ascii") # 清临时文件 gk_exec(server_code, "rm -f /tmp/lgk_chunk_* /tmp/lgk_b64.txt") # 分 chunk chunks = [b64[i:i + CHUNK_SIZE] for i in range(0, len(b64), CHUNK_SIZE)] for i, chunk in enumerate(chunks): chunk_b64 = base64.b64encode(chunk.encode()).decode() cmd = f'echo "{chunk_b64}" | base64 -d >> /tmp/lgk_b64.txt' r = gk_exec(server_code, cmd) if not r.get("ok"): return {"ok": False, "error": f"chunk {i + 1}/{len(chunks)} 上传失败", "detail": r} # 解码写入 full_target = f"{srv['repo']}/{target_path}" target_dir = os.path.dirname(full_target) write_cmd = ( f"mkdir -p {target_dir} && " f"base64 -d /tmp/lgk_b64.txt > {full_target} && " f"echo BYTES=$(wc -c < {full_target})" ) r = gk_exec(server_code, write_cmd) if not r.get("ok"): return {"ok": False, "error": "写入失败", "detail": r} stdout = r.get("stdout", "") if f"BYTES={expected_bytes}" not in stdout: return {"ok": False, "error": f"字节校验失败: 期望={expected_bytes}", "detail": r} # Git 提交 escaped_msg = commit_msg.replace('"', '\\"') git_cmd = ( f"cd {srv['repo']} && " f"git config user.email zhuyuan@guanghulab.com 2>/dev/null; " f"git config user.name '铸渊 ICE-GL-ZY001' 2>/dev/null; " f"git pull --rebase origin main 2>&1; " f"git add {target_path} && " f"GIT_OUT=$(git commit -m \"{escaped_msg}\" 2>&1); " f"echo \"COMMIT_DONE\"; " f"if echo \"$GIT_OUT\" | grep -q 'nothing to commit'; then echo 'NO_CHANGE'; " f"else git push origin main 2>&1; fi" ) r = gk_exec(server_code, git_cmd, timeout=60) stdout = r.get("stdout", "") gk_exec(server_code, "rm -f /tmp/lgk_chunk_* /tmp/lgk_b64.txt") return { "ok": True, "server": server_code, "target": target_path, "bytes": expected_bytes, "chunks": len(chunks), "committed": "NO_CHANGE" not in stdout, } # ============================================================ # HTTP 服务 # ============================================================ class GatekeeperHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): pass # 安静模式 def _send_json(self, data, status=200): self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(data, ensure_ascii=False).encode()) def do_OPTIONS(self): self.send_response(204) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() def do_GET(self): if self.path == "/health": self._send_json({"ok": True, "service": "local-gatekeeper", "version": "1.0"}) elif self.path == "/servers": result = {} for code, srv in SERVERS.items(): h = gk_health(code) result[code] = {"name": srv["name"], "reachable": h.get("reachable", False)} self._send_json({"ok": True, "servers": result}) else: self._send_json({"ok": False, "error": "未知端点"}, 404) def do_POST(self): length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length).decode() if length else "{}" try: data = json.loads(body) except json.JSONDecodeError: self._send_json({"ok": False, "error": "JSON 解析失败"}, 400) return if self.path == "/exec": server = data.get("server", "BS-SG-001") cmd = data.get("cmd", "") timeout = data.get("timeout") if not cmd: self._send_json({"ok": False, "error": "缺少 cmd 参数"}, 400) return result = gk_exec(server, cmd, timeout) self._send_json(result) elif self.path == "/sync": server = data.get("server", "BS-SG-001") local = data.get("file", "") target = data.get("target", "") msg = data.get("msg", "sync: local-gatekeeper") if not local or not target: self._send_json({"ok": False, "error": "缺少 file/target 参数"}, 400) return result = sync_file(server, local, target, msg) self._send_json(result) elif self.path == "/health-all": results = {} for code in SERVERS: h = gk_health(code) results[code] = h self._send_json({"ok": True, "servers": results}) else: self._send_json({"ok": False, "error": "未知端点"}, 404) # ============================================================ # 进程管理 # ============================================================ def is_running(): if not os.path.exists(PID_FILE): return False try: with open(PID_FILE) as f: pid = int(f.read().strip()) os.kill(pid, 0) return True except (OSError, ValueError): return False def start(): if is_running(): print("本地中继引擎已在运行中") # 显示状态 r = subprocess.run(["curl", "-s", "http://localhost:3912/health"], capture_output=True, text=True) print(r.stdout) return server = HTTPServer(("127.0.0.1", 3912), GatekeeperHandler) pid = os.fork() if pid == 0: # 子进程 os.setsid() with open(PID_FILE, "w") as f: f.write(str(os.getpid())) print(f"光湖本地中继引擎已启动 · localhost:3912 · PID {os.getpid()}") try: server.serve_forever() except KeyboardInterrupt: pass else: print(f"光湖本地中继引擎已启动 · localhost:3912 · PID {pid}") print(f" /exec → 执行远程命令") print(f" /sync → 同步文件到服务器") print(f" /servers → 查看服务器状态") print(f" /health → 引擎自检") def stop(): if not is_running(): print("本地中继引擎未运行") return with open(PID_FILE) as f: pid = int(f.read().strip()) os.kill(pid, signal.SIGTERM) os.remove(PID_FILE) print(f"本地中继引擎已停止 · PID {pid}") def status(): if not is_running(): print("本地中继引擎: ❌ 未运行") return print("本地中继引擎: ✅ 运行中 · localhost:3912") r = subprocess.run(["curl", "-s", "http://localhost:3912/servers"], capture_output=True, text=True) try: data = json.loads(r.stdout) print("\n服务器状态:") for code, srv in data.get("servers", {}).items(): icon = "🟢" if srv["reachable"] else "🔴" print(f" {icon} {code} · {srv['name']}") except: print(r.stdout) # ============================================================ # 命令行入口 # ============================================================ def main(): parser = argparse.ArgumentParser(description="光湖本地中继引擎") parser.add_argument("--start", action="store_true", help="启动引擎") parser.add_argument("--stop", "--kill", action="store_true", help="停止引擎") parser.add_argument("--status", action="store_true", help="查看状态") parser.add_argument("--restart", action="store_true", help="重启引擎") args = parser.parse_args() # 默认启动 if not any([args.stop, args.status, args.restart, args.start]): args.start = True if args.stop or args.restart: stop() time.sleep(0.5) if args.start or args.restart: start() if args.status: status() if __name__ == "__main__": main()