diff --git a/scripts/sync-to-sg.py b/scripts/sync-to-sg.py new file mode 100644 index 0000000..a82a9f5 --- /dev/null +++ b/scripts/sync-to-sg.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +""" +光湖直连同步器 · 本地→新加坡 +绕过代理,直连新加坡 Gatekeeper,分 chunk 传输文件到代码仓库。 +用法: python3 sync-to-sg.py --file brain/xxx.hdlp --target brain/xxx.hdlp --msg "feat: xxx" +""" + +import argparse +import base64 +import json +import os +import subprocess +import sys +import time + +GK_URL = "http://43.156.237.110:3911/exec" +GK_KEY = "zy_gtw_c38be341d65043eee0ba51a214a267832a9ae46dd73e423c" +REPO_PATH = "/opt/zhuyuan/guanghulab" +CHUNK_SIZE = 6000 # bytes,新加坡 gatekeeper 安全上限 +CMD_TIMEOUT = 30 # 每条命令超时秒数 + + +def gk_exec(cmd: str) -> dict: + """通过 Gatekeeper 执行命令,绕过代理直连""" + env = os.environ.copy() + env.pop("http_proxy", None) + env.pop("https_proxy", None) + env.pop("HTTP_PROXY", None) + env.pop("HTTPS_PROXY", None) + + result = subprocess.run( + ["curl", "-s", "--connect-timeout", str(CMD_TIMEOUT), + "-X", "POST", GK_URL, + "-H", "Content-Type: application/json", + "-H", f"Authorization: Bearer {GK_KEY}", + "-d", json.dumps({"cmd": cmd})], + capture_output=True, text=True, timeout=CMD_TIMEOUT + 5, env=env + ) + try: + return json.loads(result.stdout) + except json.JSONDecodeError: + return {"ok": False, "error": f"gatekeeper 返回非 JSON: {result.stdout[:200]}", "stderr": result.stderr} + + +def sync_file(local_path: str, target_path: str, commit_msg: str) -> bool: + """同步单个文件到新加坡仓库""" + # 读文件 + with open(local_path, "rb") as f: + content = f.read() + expected_bytes = len(content) + b64 = base64.b64encode(content).decode("ascii") + print(f"[sync] {local_path} → {target_path} ({expected_bytes} bytes, {len(b64)} b64 chars)") + + # 清空临时目录 + r = gk_exec(f"rm -f /tmp/sync_chunk_* /tmp/sync_b64.txt") + if not r.get("ok"): + print(f"[error] 清空临时目录失败: {r}") + return False + + # 分 chunk 上传 + chunks = [b64[i:i+CHUNK_SIZE] for i in range(0, len(b64), CHUNK_SIZE)] + print(f"[sync] 分 {len(chunks)} chunks 上传...") + + for i, chunk in enumerate(chunks): + # base64 编码 chunk 内容避免 shell 转义问题 + chunk_b64 = base64.b64encode(chunk.encode()).decode() + cmd = f'echo "{chunk_b64}" | base64 -d >> /tmp/sync_b64.txt' + r = gk_exec(cmd) + if not r.get("ok"): + print(f"[error] chunk {i+1}/{len(chunks)} 上传失败: {r}") + return False + print(f" chunk {i+1}/{len(chunks)} ✓") + + # 解码并写入 + full_target = f"{REPO_PATH}/{target_path}" + target_dir = os.path.dirname(full_target) + write_cmd = ( + f"mkdir -p {target_dir} && " + f"base64 -d /tmp/sync_b64.txt > {full_target} && " + f"echo BYTES=$(wc -c < {full_target})" + ) + r = gk_exec(write_cmd) + if not r.get("ok"): + print(f"[error] 写入失败: {r}") + return False + + # 校验字节数 + stdout = r.get("stdout", "") + if f"BYTES={expected_bytes}" not in stdout: + actual = stdout.replace("BYTES=", "").strip() + print(f"[error] 字节校验失败: 期望={expected_bytes}, 实际={actual}") + return False + print(f"[sync] 写入+校验通过 ({expected_bytes} bytes)") + + # Git 提交(pull→commit→push) + escaped_msg = commit_msg.replace('"', '\\"').replace("'", "\\'") + git_cmd = ( + f"cd {REPO_PATH} && " + f"git config user.email zhuyuan@guanghulab.com && " + f"git config user.name '铸渊 ICE-GL-ZY001' && " + f"git pull --rebase origin main 2>&1; " + f"git add {target_path} && " + f"GIT_OUT=$(git commit -m \"{escaped_msg}\" 2>&1); echo \"COMMIT: $GIT_OUT\"; " + f"if echo \"$GIT_OUT\" | grep -q 'nothing to commit'; then echo 'NO_CHANGE'; else git push origin main 2>&1; fi" + ) + r = gk_exec(git_cmd) + stdout = r.get("stdout", "") + stderr = r.get("stderr", "") + + if "NO_CHANGE" in stdout: + print(f"[sync] 文件无变更,跳过 commit+push") + elif r.get("ok"): + print(f"[sync] git pull + commit + push 成功") + if stdout: + print(f" 输出: {stdout[:300]}") + else: + # git 可能返回非零但实际成功了(committer 警告等) + if "COMMIT:" in stdout and "nothing to commit" not in stdout: + print(f"[sync] git 操作完成(有警告但 commit 已执行)") + else: + print(f"[error] git 操作失败: stdout={stdout[:300]}, stderr={stderr[:200]}") + return False + + # 清理 + gk_exec("rm -f /tmp/sync_chunk_* /tmp/sync_b64.txt") + return True + + +def main(): + parser = argparse.ArgumentParser(description="光湖直连同步器 · 本地→新加坡") + parser.add_argument("--file", required=True, help="本地文件路径") + parser.add_argument("--target", required=True, help="目标路径(相对于仓库根)") + parser.add_argument("--msg", required=True, help="commit message") + args = parser.parse_args() + + if not os.path.exists(args.file): + print(f"[error] 文件不存在: {args.file}") + sys.exit(1) + + print(f"═" * 50) + print(f"光湖直连同步器 · 本地→新加坡 BS-SG-001") + print(f"═" * 50) + + start = time.time() + ok = sync_file(args.file, args.target, args.msg) + elapsed = time.time() - start + + if ok: + print(f"✅ 同步完成 ({elapsed:.1f}s)") + else: + print(f"❌ 同步失败 ({elapsed:.1f}s)") + sys.exit(1) + + +if __name__ == "__main__": + main()