guanghulab/scripts/local-gatekeeper.py
2026-05-31 20:49:06 +08:00

380 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
光湖本地中继引擎 · Local Gatekeeper v1.0
监听 localhost:3912WorkBuddy 通过它操作六台服务器。
本地引擎负责密钥管理、代理绕过、分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()