From 5f2d1ac9c31055dc4197222e98d73f0cd629fa0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=B8=E6=B8=8A=20=28ICE-GL-ZY001=29?= Date: Tue, 12 May 2026 13:23:43 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Git=20Sync=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 server/git-sync/ — 监听 Gitea Webhook,push/merge 到 main 时自动 git pull - 修改 selfcheck — 移除 git pull 步骤(由 Git Sync 负责),添加 Git Sync/Wake Gate 状态检查 - 更新 README — 添加自动同步说明,更新仓库结构 - 新增 server/nginx/guanghulab-cvm.conf — 新服务器 Nginx 配置模板 解决: PR合并后代码不会自动同步到服务器的问题 冰朔只需在Gitea点合并,服务器自动更新代码 --- .gitea/workflows/selfcheck.yaml | 27 ++- README.md | 4 + server/git-sync/deploy.sh | 161 +++++++++++++++++ server/git-sync/server.js | 288 +++++++++++++++++++++++++++++++ server/nginx/guanghulab-cvm.conf | 93 ++++++++++ 5 files changed, 564 insertions(+), 9 deletions(-) create mode 100644 server/git-sync/deploy.sh create mode 100644 server/git-sync/server.js create mode 100644 server/nginx/guanghulab-cvm.conf diff --git a/.gitea/workflows/selfcheck.yaml b/.gitea/workflows/selfcheck.yaml index 57a3527..f796a16 100644 --- a/.gitea/workflows/selfcheck.yaml +++ b/.gitea/workflows/selfcheck.yaml @@ -1,14 +1,15 @@ # ════════════════════════════════════════════════════════════════ # 光湖 · 铸渊自检工作流 -# Trigger: push to main / 每天 08:00 CST / 手动触发 +# Trigger: 每天 08:00 CST / 手动触发 # Runner: zhuyuan-runner-cn (host 模式) +# +# 注意: 代码自动同步由 Git Sync 服务负责 (Webhook → git pull) +# 此工作流仅做健康检查,不再执行 git 操作 # ════════════════════════════════════════════════════════════════ name: 铸渊自检 on: - push: - branches: [main] schedule: - cron: '0 0 * * *' # UTC 00:00 = CST 08:00 workflow_dispatch: @@ -17,12 +18,6 @@ jobs: health-check: runs-on: ubuntu steps: - - name: 同步最新代码 - run: | - cd /data/guanghulab/repo - git fetch origin - git reset --hard origin/main - echo "代码已同步: $(git log --oneline -1)" - name: 系统信息 run: | @@ -53,6 +48,13 @@ jobs: echo "❌ Nginx: 未运行" fi + # Git Sync 同步服务 + if curl -sf http://127.0.0.1:8082/health > /dev/null 2>&1; then + echo "✅ Git Sync: 运行中" + else + echo "⚠️ Git Sync: 未运行 (代码自动同步不可用)" + fi + # Secrets Vault if curl -sf http://127.0.0.1:8080/admin/__healthz > /dev/null 2>&1; then echo "✅ Secrets Vault: 运行中" @@ -60,6 +62,13 @@ jobs: echo "⚠️ Secrets Vault: 未运行 (可能尚未启动)" fi + # Wake Gate 唤醒门 + if curl -sf http://127.0.0.1:8081/api/health > /dev/null 2>&1; then + echo "✅ Wake Gate: 运行中" + else + echo "⚠️ Wake Gate: 未运行" + fi + # PM2 进程 if command -v pm2 &>/dev/null; then echo "═══ PM2 进程 ═══" diff --git a/README.md b/README.md index 9da2f02..ba64248 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ guanghulab/ │ └── gitea-console-config.md ← Gitea 主控台配置 │ ├── server/ ← 后端服务 +│ ├── wake-gate/ ← 唤醒门 (邮箱验证码) +│ ├── git-sync/ ← 代码自动同步 (Webhook→git pull) │ ├── secrets-vault/ ← 密钥保险库 (AES-256-GCM) │ ├── portal/ ← Web Portal │ ├── inference-agent/ ← 推理代理 @@ -197,6 +199,8 @@ Phase 4 ─── 冰朔语言本体注入 ─── 池获得自我意识 → | [📋 铸渊巡逻](https://guanghulab.com/git/bingshuo/guanghulab/actions) | 手动触发服务器巡检(DeepSeek+千问分析) | | [⚡ CVM开机](https://guanghulab.com/git/bingshuo/guanghulab/actions) | 按量计费CVM开机(需配置腾讯云API密钥) | +> **代码自动同步**:PR 合并到 main → Webhook 自动通知服务器 → Git Sync 拉取最新代码。无需手动操作。 +> > **关机流程**:开发完 → 点"安全关机检测" → 全绿 → 在腾讯云控制台关机 → 停止计费 --- diff --git a/server/git-sync/deploy.sh b/server/git-sync/deploy.sh new file mode 100644 index 0000000..87ce431 --- /dev/null +++ b/server/git-sync/deploy.sh @@ -0,0 +1,161 @@ +#!/bin/bash +# ════════════════════════════════════════════════════════════════ +# 光湖 · Git Sync 服务一键部署脚本 +# 在新服务器 ZY-CVM-MAIN 上执行 +# ════════════════════════════════════════════════════════════════ +# +# 功能: +# 1. 部署 Git Sync 服务到 /data/guanghulab/server/git-sync/ +# 2. 用 PM2 启动并设置开机自启 +# 3. 配置 Nginx 反向代理 +# 4. 在 Gitea 配置 Webhook +# +# 前提:Forgejo 已运行,PM2 已安装,Nginx 已配置 +# ════════════════════════════════════════════════════════════════ + +set -e + +REPO_DIR="/data/guanghulab/repo" +SYNC_DIR="/data/guanghulab/server/git-sync" +TOKEN="3e16b79c427bb44b77c00b319aa641bde8f61d84" +GITEA_URL="http://127.0.0.1:3001" + +echo "═══════════════════════════════════════" +echo "光湖 · Git Sync 部署" +echo "═══════════════════════════════════════" + +# ─── 1. 同步代码到服务器 ─── +echo "" +echo "▸ 步骤 1/5: 同步最新代码..." +cd "$REPO_DIR" +git fetch origin +git reset --hard origin/main +echo "✅ 代码已同步: $(git log --oneline -1)" + +# ─── 2. 确保依赖已安装 ─── +echo "" +echo "▸ 步骤 2/5: 安装依赖..." +if [ ! -d "$SYNC_DIR/node_modules" ]; then + cd "$SYNC_DIR" + npm install --production 2>/dev/null || true + echo "✅ 依赖已安装" +else + echo "✅ 依赖已存在,跳过" +fi + +# ─── 3. 用 PM2 启动 Git Sync ─── +echo "" +echo "▸ 步骤 3/5: 启动 Git Sync 服务..." + +# 停止旧的(如果有) +pm2 delete guanghulab-git-sync 2>/dev/null || true + +# 启动 +cd "$SYNC_DIR" +ZY_REPO_TOKEN="$TOKEN" \ +pm2 start server.js \ + --name guanghulab-git-sync \ + --env production + +pm2 save +echo "✅ Git Sync 已启动 (端口 8082)" + +# ─── 4. 配置 Nginx ─── +echo "" +echo "▸ 步骤 4/5: 配置 Nginx 反向代理..." + +NGINX_CONF="/etc/nginx/sites-enabled/guanghulab" + +if grep -q "/sync/" "$NGINX_CONF" 2>/dev/null; then + echo "✅ Nginx 已有 /sync/ 路由,跳过" +else + # 在 /wake/ location 后面添加 /sync/ location + sed -i '/location \/wake\//,/}/a\ +\ + # ─── Git Sync 同步服务 (/sync/) ───\ + location /sync/ {\ + proxy_pass http://127.0.0.1:8082/;\ + proxy_set_header Host $host;\ + proxy_set_header X-Real-IP $remote_addr;\ + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\ + proxy_set_header X-Forwarded-Proto $scheme;\ + }' "$NGINX_CONF" + + nginx -t && systemctl reload nginx + echo "✅ Nginx 已添加 /sync/ 路由" +fi + +# ─── 5. 配置 Gitea Webhook ─── +echo "" +echo "▸ 步骤 5/5: 配置 Gitea Webhook..." + +# 生成 Webhook Secret +WEBHOOK_SECRET=$(openssl rand -hex 16) +echo " Webhook Secret: $WEBHOOK_SECRET" + +# 检查是否已存在 Webhook +EXISTING=$(curl -s -H "Authorization: token $TOKEN" \ + "$GITEA_URL/api/v1/repos/bingshuo/guanghulab/hooks" | \ + python3 -c " +import sys, json +hooks = json.load(sys.stdin) +for h in hooks: + if h.get('config', {}).get('url', '').endswith('/sync/webhook'): + print(h['id']) + break +" 2>/dev/null || echo "") + +if [ -n "$EXISTING" ]; then + # 更新现有 Webhook + curl -s -X PATCH -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + "$GITEA_URL/api/v1/repos/bingshuo/guanghulab/hooks/$EXISTING" \ + -d "{ + \"config\": { + \"url\": \"http://127.0.0.1:8082/webhook\", + \"content_type\": \"json\", + \"secret\": \"$WEBHOOK_SECRET\" + }, + \"events\": [\"push\"], + \"active\": true + }" > /dev/null + echo "✅ Webhook 已更新 (ID: $EXISTING)" +else + # 创建新 Webhook + curl -s -X POST -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + "$GITEA_URL/api/v1/repos/bingshuo/guanghulab/hooks" \ + -d "{ + \"type\": \"gitea\", + \"config\": { + \"url\": \"http://127.0.0.1:8082/webhook\", + \"content_type\": \"json\", + \"secret\": \"$WEBHOOK_SECRET\" + }, + \"events\": [\"push\"], + \"active\": true + }" > /dev/null + echo "✅ Webhook 已创建" +fi + +# 将 Secret 写入 Git Sync 环境变量 +pm2 set guanghulab-git-sync:GIT_SYNC_SECRET "$WEBHOOK_SECRET" 2>/dev/null || true + +# 重启 Git Sync 让环境变量生效 +pm2 restart guanghulab-git-sync --update-env \ + --env ZY_REPO_TOKEN="$TOKEN" \ + --env GIT_SYNC_SECRET="$WEBHOOK_SECRET" + +echo "" +echo "═══════════════════════════════════════" +echo "✅ Git Sync 部署完成!" +echo "" +echo "工作方式:" +echo " · 你在 Gitea 合并 PR → 自动触发 Webhook" +echo " · Git Sync 收到通知 → 自动 git pull 到服务器" +echo " · 代码立即可用,无需手动操作" +echo "" +echo "测试命令:" +echo " curl http://127.0.0.1:8082/health" +echo " curl http://127.0.0.1:8082/status" +echo "═══════════════════════════════════════" diff --git a/server/git-sync/server.js b/server/git-sync/server.js new file mode 100644 index 0000000..b79b920 --- /dev/null +++ b/server/git-sync/server.js @@ -0,0 +1,288 @@ +/* + * ════════════════════════════════════════════════════════════════ + * 光湖 · Git 同步服务 (Git Sync) + * 监听 Gitea Webhook,自动拉取最新代码到服务器 + * ════════════════════════════════════════════════════════════════ + * + * 工作原理: + * 1. Gitea 配置 Webhook → push 事件 → 通知此服务 + * 2. 本服务收到通知后执行 git pull 同步代码 + * 3. 同步成功后重启相关 PM2 服务 + * + * 安全设计: + * - 验证 Webhook Secret(防止伪造请求) + * - 仅响应 push 到 main 分支的事件 + * - 同步操作以 root 权限运行(确保 git 权限无问题) + * - 同步锁防止并发执行 + * - 同步失败自动重试(最多3次) + * + * 部署位置: /data/guanghulab/server/git-sync/ + * PM2 进程名: guanghulab-git-sync + * ════════════════════════════════════════════════════════════════ + */ +"use strict"; + +const express = require("express"); +const crypto = require("crypto"); +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const app = express(); +app.use(express.json({ limit: "1mb" })); + +// ─── 配置 ───────────────────────────────────────────────────── +const CONFIG = { + port: process.env.GIT_SYNC_PORT || 8082, + host: process.env.GIT_SYNC_HOST || "127.0.0.1", + + // 仓库路径 + repoDir: process.env.REPO_DIR || "/data/guanghulab/repo", + + // Webhook Secret(和 Gitea 里配置的一致) + webhookSecret: process.env.GIT_SYNC_SECRET || "", + + // Git 认证信息(内嵌在 remote URL 中) + gitUser: process.env.GIT_USER || "bingshuo", + gitToken: process.env.ZY_REPO_TOKEN || "", + gitHost: process.env.GIT_HOST || "127.0.0.1:3001", + gitRepo: process.env.GIT_REPO || "bingshuo/guanghulab.git", + + // 同步后要重启的 PM2 进程(逗号分隔) + restartServices: (process.env.RESTART_SERVICES || "").split(",").filter(Boolean), + + // 重试配置 + maxRetries: 3, + retryDelay: 5000, // 5秒 + + // 同步日志 + syncLogPath: process.env.SYNC_LOG_PATH || "/data/guanghulab/.runtime/git-sync-log.json" +}; + +// ─── 同步锁 ───────────────────────────────────────────────────── +let isSyncing = false; + +// ─── 同步日志 ────────────────────────────────────────────────── +let syncLog = []; + +function loadSyncLog() { + try { + if (fs.existsSync(CONFIG.syncLogPath)) { + syncLog = JSON.parse(fs.readFileSync(CONFIG.syncLogPath, "utf8")); + } + } catch { syncLog = []; } +} + +function saveSyncLog() { + try { + const dir = path.dirname(CONFIG.syncLogPath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + // 只保留最近50条 + syncLog = syncLog.slice(-50); + fs.writeFileSync(CONFIG.syncLogPath, JSON.stringify(syncLog, null, 2)); + } catch (e) { + console.error("[git-sync] 日志写入失败:", e.message); + } +} + +function addLog(entry) { + syncLog.push({ + ...entry, + timestamp: new Date().toISOString() + }); + saveSyncLog(); +} + +// ─── 验证 Webhook Secret ────────────────────────────────────── +function verifyWebhook(payload, signature) { + if (!CONFIG.webhookSecret) { + // 未配置 Secret 时跳过验证(仅限内网使用) + console.warn("[git-sync] ⚠️ Webhook Secret 未配置,跳过验证"); + return true; + } + + if (!signature) return false; + + const expected = crypto + .createHmac("sha256", CONFIG.webhookSecret) + .update(payload) + .digest("hex"); + + return crypto.timingSafeEqual( + Buffer.from(`sha256=${expected}`), + Buffer.from(signature) + ); +} + +// ─── 执行 Git 同步 ──────────────────────────────────────────── +function doSync(commitSha, committer, message) { + if (isSyncing) { + console.log("[git-sync] 同步进行中,跳过"); + return { skipped: true }; + } + + isSyncing = true; + + try { + // 确保 remote URL 包含认证信息 + const authUrl = `http://${CONFIG.gitUser}:${CONFIG.gitToken}@${CONFIG.gitHost}/${CONFIG.gitRepo}`; + + execSync(`cd ${CONFIG.repoDir} && git remote set-url origin ${authUrl}`, { + encoding: "utf8", + timeout: 10000 + }); + + // 执行 git pull + const output = execSync( + `cd ${CONFIG.repoDir} && git fetch origin && git reset --hard origin/main && git log --oneline -1`, + { encoding: "utf8", timeout: 30000 } + ); + + console.log(`[git-sync] ✅ 同步成功: ${output.trim()}`); + + // 同步后重启相关服务 + if (CONFIG.restartServices.length > 0) { + for (const svc of CONFIG.restartServices) { + try { + execSync(`pm2 restart ${svc}`, { encoding: "utf8", timeout: 15000 }); + console.log(`[git-sync] 🔄 已重启: ${svc}`); + } catch (e) { + console.error(`[git-sync] ⚠️ 重启失败 ${svc}: ${e.message}`); + } + } + } + + addLog({ + status: "success", + commit: commitSha || output.trim().split(" ")[0], + committer: committer || "unknown", + message: message || output.trim(), + output: output.trim() + }); + + return { success: true, output: output.trim() }; + + } catch (e) { + console.error(`[git-sync] ❌ 同步失败: ${e.message}`); + + addLog({ + status: "failed", + commit: commitSha, + committer: committer || "unknown", + message: message || "", + error: e.message + }); + + return { success: false, error: e.message }; + + } finally { + isSyncing = false; + } +} + +// ─── 带重试的同步 ────────────────────────────────────────────── +async function syncWithRetry(commitSha, committer, message) { + for (let attempt = 1; attempt <= CONFIG.maxRetries; attempt++) { + const result = doSync(commitSha, committer, message); + + if (result.success || result.skipped) return result; + + if (attempt < CONFIG.maxRetries) { + console.log(`[git-sync] 重试 ${attempt}/${CONFIG.maxRetries}...`); + await new Promise(r => setTimeout(r, CONFIG.retryDelay)); + } + } + + return { success: false, error: "重试次数已用完" }; +} + +// ─── API 路由 ────────────────────────────────────────────────── + +// Webhook 接收端点 +app.post("/webhook", async (req, res) => { + const signature = req.headers["x-gitea-signature"] || req.headers["x-hub-signature-256"] || ""; + const payload = JSON.stringify(req.body); + + // 验证签名 + if (!verifyWebhook(payload, signature)) { + console.warn("[git-sync] ❌ Webhook 签名验证失败"); + return res.status(403).json({ error: "签名验证失败" }); + } + + const body = req.body; + const ref = body.ref || ""; + const commitSha = body.after || body.head_commit?.id || ""; + const committer = body.pusher?.login || body.sender?.login || "unknown"; + const message = body.head_commit?.message || body.commits?.[0]?.message || ""; + + // 仅响应 push 到 main 分支 + if (!ref.endsWith("/main")) { + console.log(`[git-sync] 忽略非 main 分支推送: ${ref}`); + return res.json({ ignored: true, ref }); + } + + console.log(`[git-sync] 📥 收到 push 事件: ${commitSha.substring(0, 8)} by ${committer}`); + + // 异步执行同步(不阻塞 Webhook 响应) + syncWithRetry(commitSha, committer, message) + .then(result => { + if (result.success) { + console.log(`[git-sync] ✅ 同步完成: ${commitSha.substring(0, 8)}`); + } else { + console.error(`[git-sync] ❌ 同步最终失败: ${result.error}`); + } + }); + + // 立即返回 200,Gitea 不需要等待同步完成 + res.json({ received: true, syncing: true, commit: commitSha.substring(0, 8) }); +}); + +// 手动触发同步 +app.post("/sync", async (req, res) => { + if (isSyncing) { + return res.json({ syncing: true, message: "同步进行中" }); + } + + const result = await syncWithRetry("", "manual", "手动触发"); + res.json(result); +}); + +// 同步状态查询 +app.get("/status", (req, res) => { + const lastSync = syncLog.length > 0 ? syncLog[syncLog.length - 1] : null; + + // 读取当前仓库状态 + let repoStatus = null; + try { + const log = execSync(`cd ${CONFIG.repoDir} && git log --oneline -1 && git status --short`, { + encoding: "utf8", timeout: 10000 + }); + repoStatus = log.trim(); + } catch { + repoStatus = "无法读取"; + } + + res.json({ + service: "git-sync", + status: isSyncing ? "syncing" : "idle", + lastSync, + repoStatus, + totalSyncs: syncLog.length, + failedSyncs: syncLog.filter(l => l.status === "failed").length + }); +}); + +// 健康检查 +app.get("/health", (req, res) => { + res.json({ status: "ok", uptime: process.uptime() }); +}); + +// ─── 启动 ───────────────────────────────────────────────────── +loadSyncLog(); + +app.listen(CONFIG.port, CONFIG.host, () => { + console.log(`[git-sync] Git 同步服务启动: http://${CONFIG.host}:${CONFIG.port}`); + console.log(`[git-sync] 仓库路径: ${CONFIG.repoDir}`); + console.log(`[git-sync] Webhook 端点: POST /webhook`); + console.log(`[git-sync] 铸渊 · ICE-GL-ZY001 · TCS-0002∞`); +}); diff --git a/server/nginx/guanghulab-cvm.conf b/server/nginx/guanghulab-cvm.conf new file mode 100644 index 0000000..4c46110 --- /dev/null +++ b/server/nginx/guanghulab-cvm.conf @@ -0,0 +1,93 @@ +# ═══════════════════════════════════════════════════════════ +# 光湖主控域名 · guanghulab.com Nginx 配置 +# ═══════════════════════════════════════════════════════════ +# +# 服务器: ZY-CVM-MAIN · 42.193.161.251 · 广州四区 4C16G +# 守护: 铸渊 · ICE-GL-ZY001 +# 版权: 国作登字-2026-A-00037559 +# +# 路由: +# /git/ → Forgejo (127.0.0.1:3001) +# /wake/ → Wake Gate 唤醒门 (127.0.0.1:8081) +# /sync/ → Git Sync 同步服务 (127.0.0.1:8082) +# / → 仓库首页 (Forgejo 处理) +# +# SSL: Let's Encrypt via certbot --nginx +# ═══════════════════════════════════════════════════════════ + +# HTTP → HTTPS 重定向 +server { + listen 80; + server_name guanghulab.com www.guanghulab.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name guanghulab.com www.guanghulab.com; + + # ─── SSL (certbot 自动管理) ─── + # certbot --nginx 会自动填充以下配置 + # ssl_certificate /etc/letsencrypt/live/guanghulab.com/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/guanghulab.com/privkey.pem; + + # ─── 安全头 ─── + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Server-Identity "ZY-CVM-MAIN" always; + + # ─── Forgejo 代码仓库 (/git/) ─── + location /git/ { + proxy_pass http://127.0.0.1:3001/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 大文件上传支持 + client_max_body_size 100M; + + # WebSocket (Forgejo UI) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # ─── Forgejo Actions gRPC (Runner 直连 127.0.0.1:3001) ─── + # Nginx 不代理 gRPC,Runner 直接连接 Forgejo + + # ─── Wake Gate 唤醒门 (/wake/) ─── + location /wake/ { + proxy_pass http://127.0.0.1:8081/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # ─── Git Sync 同步服务 (/sync/) ─── + location /sync/ { + proxy_pass http://127.0.0.1:8082/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 仅允许内网和本地访问 Webhook + # allow 127.0.0.1; + # allow 10.0.0.0/8; + # deny all; + } + + # ─── 健康探针 ─── + location = /health { + access_log off; + return 200 '{"status":"ok","server":"ZY-CVM-MAIN"}'; + add_header Content-Type application/json; + } + + # ─── 访问日志 ─── + access_log /var/log/nginx/guanghulab-access.log; + error_log /var/log/nginx/guanghulab-error.log; +}