feat: 光湖本地中继引擎 · localhost:3912
This commit is contained in:
parent
019ea5c13b
commit
e74a5c4814
379
scripts/local-gatekeeper.py
Normal file
379
scripts/local-gatekeeper.py
Normal file
@ -0,0 +1,379 @@
|
||||
#!/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()
|
||||
Loading…
x
Reference in New Issue
Block a user