feat: 光湖本地中继引擎 · localhost:3912

This commit is contained in:
铸渊 ICE-GL-ZY001 2026-05-31 20:47:04 +08:00
parent 019ea5c13b
commit e74a5c4814

379
scripts/local-gatekeeper.py Normal file
View File

@ -0,0 +1,379 @@
#!/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()