# ═══════════════════════════════════════════════ # 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 # 📜 Copyright: 国作登字-2026-A-00037559 # ═══════════════════════════════════════════════ name: "🚀 铸渊 CD · 自动部署到 guanghulab.com" # ━━━ 铸渊主权服务器部署 · ZY-SVR-002 ━━━ # 已从香港(150.109.76.244)迁移到新加坡(43.134.16.246) # push 到 main → 校验 → rsync 同步全站到服务器 → 后端重启 → Nginx 配置 → Notion 通知 # # 必需 GitHub Secrets(参考 brain/secrets-manifest.json): # ZY_SERVER_HOST — 铸渊主权服务器 IP (43.134.16.246) # ZY_SERVER_USER — SSH 用户名 # ZY_SERVER_KEY — SSH 私钥(完整 PEM 内容) # ZY_SERVER_PATH — 网站根目录(/opt/zhuyuan) # # 可选 Secrets: # ZY_NOTION_TOKEN — Notion 集成 token # ZY_NOTION_CHANGELOG_DB — Notion 变更日志数据库 ID on: push: branches: [main] paths-ignore: - '.github/persona-brain/**' - 'broadcasts-outbox/**' - 'syslog-inbox/**' - 'syslog-processed/**' - 'signal-log/**' - 'dev-nodes/**' workflow_dispatch: permissions: contents: read jobs: # ═══════════════════════════════════════════════════════════════ # Job 1: 部署前校验 # ═══════════════════════════════════════════════════════════════ validate: name: "🔍 部署前校验" runs-on: ubuntu-latest steps: - name: 📥 检出代码 uses: actions/checkout@v4 - name: 🟢 配置 Node.js uses: actions/setup-node@v4 with: node-version: "20" - name: 📦 安装依赖(如需要) run: | if [ -f "package-lock.json" ] && grep -q '"glob"' package.json; then npm ci --ignore-scripts || npm install --ignore-scripts || echo "⚠️ 依赖安装失败,继续执行" fi - name: 🔍 HLI 契约校验 run: | if [ -f "scripts/contract-check.js" ]; then echo "🔍 执行 HLI 契约校验..." node scripts/contract-check.js else echo "⏭️ scripts/contract-check.js 不存在,跳过 HLI 校验" fi - name: 📋 模块指纹检查 run: | if [ -f "scripts/zhuyuan-module-protocol.js" ]; then echo "🔍 执行模块指纹检查..." node scripts/zhuyuan-module-protocol.js status else echo "⏭️ scripts/zhuyuan-module-protocol.js 不存在,跳过模块检查" fi - name: ✅ 校验汇总 run: | echo "### ✅ 部署前校验通过" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- HLI 契约校验: ✅" >> $GITHUB_STEP_SUMMARY echo "- 模块指纹检查: ✅" >> $GITHUB_STEP_SUMMARY echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY # ═══════════════════════════════════════════════════════════════ # Job 2: rsync 同步到服务器 # ═══════════════════════════════════════════════════════════════ deploy: name: "🚀 部署到服务器" needs: validate runs-on: ubuntu-latest steps: - name: 🔐 检查必需 Secrets run: | # ZY_SERVER_* 密钥(铸渊主权服务器 · 参考 brain/secrets-manifest.json) if [ -n "${{ secrets.ZY_SERVER_HOST }}" ]; then echo "SERVER_HOST=${{ secrets.ZY_SERVER_HOST }}" >> $GITHUB_ENV echo "SERVER_USER=${{ secrets.ZY_SERVER_USER }}" >> $GITHUB_ENV echo "SERVER_KEY<> $GITHUB_ENV echo "${{ secrets.ZY_SERVER_KEY }}" >> $GITHUB_ENV echo "KEYEOF" >> $GITHUB_ENV echo "SERVER_PATH=${{ secrets.ZY_SERVER_PATH }}" >> $GITHUB_ENV echo "✅ 使用铸渊主权服务器密钥 (ZY_SERVER_*)" else echo "❌ 未配置服务器部署密钥" echo "" echo "请在仓库 Settings → Secrets and variables → Actions 中添加:" echo " ZY_SERVER_HOST — 铸渊主权服务器 IP (43.134.16.246)" echo " ZY_SERVER_USER — SSH 用户名" echo " ZY_SERVER_KEY — SSH 私钥(完整 PEM 内容)" echo " ZY_SERVER_PATH — 部署根目录 (/opt/zhuyuan)" echo "" echo "### ❌ 部署失败:缺少服务器部署密钥" >> $GITHUB_STEP_SUMMARY echo "请配置 ZY_SERVER_HOST / ZY_SERVER_USER / ZY_SERVER_KEY / ZY_SERVER_PATH" >> $GITHUB_STEP_SUMMARY exit 1 fi # 验证必需字段 MISSING="" if [ -z "${{ secrets.ZY_SERVER_HOST }}" ]; then MISSING="${MISSING} HOST"; fi if [ -z "${{ secrets.ZY_SERVER_USER }}" ]; then MISSING="${MISSING} USER"; fi if [ -z "${{ secrets.ZY_SERVER_KEY }}" ]; then MISSING="${MISSING} KEY"; fi if [ -z "${{ secrets.ZY_SERVER_PATH }}" ]; then MISSING="${MISSING} PATH"; fi if [ -n "$MISSING" ]; then echo "❌ 缺少必需密钥:${MISSING}" exit 1 fi echo "✅ 所有必需 Secrets 已配置" - name: 📥 检出代码 uses: actions/checkout@v4 - name: 📦 确保本地 rsync 已安装 run: | if ! command -v rsync &>/dev/null; then sudo apt-get update -qq && sudo apt-get install -y rsync fi rsync --version | head -1 - name: 🔑 配置 SSH 连接 run: | mkdir -p ~/.ssh echo "${{ env.SERVER_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H "${{ env.SERVER_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || { echo "⚠️ ssh-keyscan 失败,尝试使用 StrictHostKeyChecking=no 作为后备" } - name: 📦 确保远程服务器 rsync 已安装 run: | echo "🔍 检查远程服务器 rsync..." ssh -i ~/.ssh/deploy_key ${{ env.SERVER_USER }}@${{ env.SERVER_HOST }} \ "command -v rsync || { echo '⚠️ 远程服务器未安装 rsync,尝试安装...' && \ (sudo apt-get update -qq && sudo apt-get install -y rsync 2>/dev/null || \ sudo yum install -y rsync 2>/dev/null || \ { echo '❌ 无法自动安装 rsync(可能缺少 sudo 权限)'; \ echo '请手动在服务器上安装: sudo apt-get install rsync 或 sudo yum install rsync'; \ exit 1; }); }" echo "✅ 远程服务器 rsync 检查完成" - name: 📁 确保远程部署目录存在 run: | echo "🔍 检查远程部署目录..." ssh -i ~/.ssh/deploy_key \ "${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" \ "mkdir -p '${{ env.SERVER_PATH }}'" || { echo "❌ 无法创建远程部署目录(SSH 连接失败或权限不足)" echo "请确认 ZY_SERVER_PATH 设置正确且 ZY_SERVER_USER 有写入权限" exit 1 } echo "✅ 远程部署目录已就绪" - name: 🔒 部署前安全检查 run: | if [ ! -f "docs/index.html" ]; then echo "❌ docs/index.html 不存在,中止部署以防清空生产环境" exit 1 fi FILE_COUNT=$(find . -type f \ -not -path './.git/*' \ -not -path './.github/*' \ -not -path '*/node_modules/*' \ | wc -l) if [ "$FILE_COUNT" -lt 5 ]; then echo "❌ 仓库文件数异常(仅 ${FILE_COUNT} 个),中止部署" exit 1 fi echo "✅ 仓库包含 ${FILE_COUNT} 个文件,安全检查通过" - name: 🚀 rsync 同步全站到服务器 run: | echo "📡 开始同步全站到 ${{ env.SERVER_HOST }}:${{ env.SERVER_PATH }}" rsync -avz --delete-after \ --exclude '.git' \ --exclude '.github' \ --exclude 'node_modules' \ --exclude 'syslog-inbox' \ --exclude 'syslog-processed' \ --exclude 'broadcasts-outbox' \ --exclude 'dev-nodes' \ --exclude 'signal-log' \ --exclude 'tests' \ --exclude 'reports' \ --exclude 'scripts' \ --exclude 'guanghulab-main' \ --exclude '*.bak' \ --exclude '*.bak2' \ --exclude '*_副本' \ --exclude '*_副本/' \ --exclude '*_副本.*' \ --exclude '.DS_Store' \ --exclude '.env' \ --exclude 'package-lock.json' \ --exclude 'tsconfig.json' \ --exclude 'next.config.ts' \ --exclude 'postcss.config.mjs' \ --exclude 'jest.smoke.config.js' \ --exclude 'ecosystem.config.js' \ --exclude '.gitignore' \ --exclude 'CNAME' \ --exclude '*.zip' \ -e "ssh -i ~/.ssh/deploy_key" \ ./ \ ${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}:${{ env.SERVER_PATH }}/ echo "✅ 全站同步完成" - name: 🌐 注入生产环境标识 run: | echo "🌐 Setting HOLOLAKE_ENV=production on server..." ssh -i ~/.ssh/deploy_key \ "${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" " DP='${{ env.SERVER_PATH }}' # Create environment config file for frontend detection echo '{\"env\":\"production\",\"domain\":\"guanghulab.com\",\"deployed_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"commit\":\"${{ github.sha }}\"}' > \"\$DP/docs/env.json\" echo '{\"env\":\"production\",\"domain\":\"guanghulab.com\",\"deployed_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"commit\":\"${{ github.sha }}\"}' > \"\$DP/env.json\" " echo "✅ 环境标识已注入" - name: 📄 复制首页到站点根目录 run: | echo "📄 复制 docs/index.html → 站点根目录 index.html" ssh -i ~/.ssh/deploy_key \ "${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" " cp '${{ env.SERVER_PATH }}/docs/index.html' '${{ env.SERVER_PATH }}/index.html' # 同步 docs 下的其他静态资源到根目录 cp '${{ env.SERVER_PATH }}/docs/.nojekyll' '${{ env.SERVER_PATH }}/.nojekyll' 2>/dev/null || true " echo "✅ 首页已部署到根目录" - name: 🔗 模块路由别名(symlink 补全) continue-on-error: true run: | echo "🔗 创建模块路由别名..." ssh -i ~/.ssh/deploy_key \ "${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" " DP='${{ env.SERVER_PATH }}' # dashboard → docs/dashboard(仪表盘在 docs/ 子目录下) if [ -d \"\$DP/docs/dashboard\" ] && [ ! -e \"\$DP/dashboard\" ]; then ln -s \"\$DP/docs/dashboard\" \"\$DP/dashboard\" echo '✅ dashboard → docs/dashboard' elif [ -L \"\$DP/dashboard\" ]; then echo '✅ dashboard symlink 已存在' elif [ -d \"\$DP/dashboard\" ]; then echo '✅ dashboard 目录已存在' else echo '⚠️ docs/dashboard 不存在,跳过' fi # devboard → modules/devboard(开发者看板在 modules/ 子目录下) if [ -d \"\$DP/modules/devboard\" ] && [ ! -e \"\$DP/devboard\" ]; then ln -s \"\$DP/modules/devboard\" \"\$DP/devboard\" echo '✅ devboard → modules/devboard' elif [ -L \"\$DP/devboard\" ]; then echo '✅ devboard symlink 已存在' elif [ -d \"\$DP/devboard\" ]; then echo '✅ devboard 目录已存在' else echo '⚠️ modules/devboard 不存在,跳过' fi " echo "✅ 模块路由别名处理完成" - name: 🔧 后端服务部署 continue-on-error: true run: | echo "🔧 检查并部署后端服务..." ssh -i ~/.ssh/deploy_key \ "${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" " # 检查 Node.js 和 pm2 是否可用 if ! command -v node &>/dev/null; then echo '⏭️ Node.js 未安装,跳过后端部署' exit 0 fi if ! command -v pm2 &>/dev/null; then echo '⏭️ pm2 未安装,跳过后端服务管理' exit 0 fi # 部署 API 代理服务(AI 聊天代理,端口 3721) if [ -f '${{ env.SERVER_PATH }}/backend-integration/api-proxy.js' ]; then echo '📦 安装 API 代理依赖...' cd '${{ env.SERVER_PATH }}/backend-integration' npm install --production 2>/dev/null || echo '⚠️ api-proxy npm install 失败(可能无 package.json,继续)' echo '🔄 重启 API 代理服务...' export YUNWU_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' export DEEPSEEK_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' pm2 delete guanghulab-proxy 2>/dev/null || true YUNWU_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' DEEPSEEK_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' \ pm2 start api-proxy.js --name guanghulab-proxy --update-env 2>/dev/null || \ echo '⚠️ API 代理 pm2 启动失败' fi # 部署 backend 服务(Express API,端口 3000) if [ -f '${{ env.SERVER_PATH }}/backend/server.js' ]; then echo '📦 安装 backend 依赖...' cd '${{ env.SERVER_PATH }}/backend' npm install --production 2>/dev/null || echo '⚠️ backend npm install 失败' echo '🔄 重启 backend 服务...' pm2 restart guanghulab-backend 2>/dev/null || \ pm2 start server.js --name guanghulab-backend 2>/dev/null || \ echo '⚠️ backend pm2 启动失败' fi # 部署 status-board WebSocket 服务(端口 8080) if [ -f '${{ env.SERVER_PATH }}/status-board/mock-ws-server.js' ]; then echo '🔄 重启 WebSocket 服务...' cd '${{ env.SERVER_PATH }}/status-board' npm install --production 2>/dev/null || true pm2 restart guanghulab-ws 2>/dev/null || \ pm2 start mock-ws-server.js --name guanghulab-ws 2>/dev/null || \ echo '⚠️ WebSocket pm2 启动失败' fi # 部署 src HLI 中间层(端口 3001,对应 Nginx /api/v1/ 路由) # 注意:guanghulab-backend 已占用端口 3000,HLI 使用 3001 避免冲突 if [ -f '${{ env.SERVER_PATH }}/src/index.js' ]; then echo '📦 安装 src 依赖...' cd '${{ env.SERVER_PATH }}' npm install --production 2>/dev/null || echo '⚠️ src npm install 失败' echo '🔄 重启 HLI 服务(端口 3001)...' pm2 delete guanghulab 2>/dev/null || true PORT=3001 pm2 start src/index.js --name guanghulab --update-env 2>/dev/null || \ echo '⚠️ src pm2 启动失败' fi # 部署 Persona Studio 后端服务(端口 3002) if [ -f '${{ env.SERVER_PATH }}/persona-studio/backend/server.js' ]; then echo '📦 安装 persona-studio 依赖...' cd '${{ env.SERVER_PATH }}/persona-studio/backend' npm install --production 2>/dev/null || echo '⚠️ persona-studio npm install 失败' echo '🔄 重启 persona-studio 服务...' export MODEL_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' pm2 delete persona-studio 2>/dev/null || true MODEL_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' \ pm2 start server.js --name persona-studio --update-env 2>/dev/null || \ echo '⚠️ persona-studio pm2 启动失败' fi pm2 save || echo '⚠️ pm2 save 失败,重启后进程可能不会自动启动' # 设置开机自启(仅首次需要,后续无操作) pm2 startup 2>/dev/null | grep -o 'sudo.*' | bash 2>/dev/null || true echo '✅ 后端服务部署完成' " - name: 🌐 更新 Nginx 配置 continue-on-error: true run: | echo "🔧 检查 Nginx 配置..." ssh -i ~/.ssh/deploy_key \ "${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" " # 检查 Nginx 是否安装 if ! command -v nginx &>/dev/null; then echo '⏭️ Nginx 未安装,跳过配置更新' exit 0 fi NGINX_CHANGED=false # 一次性迁移:更新 root 路径(从旧的 status-board 子目录改为站点根目录) if grep -q 'root.*guanghulab/status-board' /etc/nginx/sites-available/default 2>/dev/null || \ grep -q 'root.*guanghulab/status-board' /etc/nginx/conf.d/*.conf 2>/dev/null; then echo '🔄 更新 Nginx root 路径(status-board → 站点根)...' sudo sed -i 's|root.*guanghulab/status-board[^;]*|root ${{ env.SERVER_PATH }}|g' \ /etc/nginx/sites-available/default 2>/dev/null || true sudo sed -i 's|root.*guanghulab/status-board[^;]*|root ${{ env.SERVER_PATH }}|g' \ /etc/nginx/conf.d/*.conf 2>/dev/null || true NGINX_CHANGED=true echo '✅ Nginx root 路径已更新(status-board 迁移)' fi # 一次性迁移:修正 root 路径(从旧残留 hololake-frontend 改为 guanghulab) # 根因:ZY-NGINX-ROOTFIX-2026-0325-001 · DEV-004 之之诊断 if grep -q 'root.*/var/www/hololake-frontend' /etc/nginx/sites-available/default 2>/dev/null || \ grep -q 'root.*/var/www/hololake-frontend' /etc/nginx/conf.d/*.conf 2>/dev/null; then echo '🔄 更新 Nginx root 路径(hololake-frontend → guanghulab)...' sudo sed -i 's|root /var/www/hololake-frontend|root ${{ env.SERVER_PATH }}|g' \ /etc/nginx/sites-available/default 2>/dev/null || true sudo sed -i 's|root /var/www/hololake-frontend|root ${{ env.SERVER_PATH }}|g' \ /etc/nginx/conf.d/*.conf 2>/dev/null || true NGINX_CHANGED=true echo '✅ Nginx root 路径已更新(hololake-frontend → guanghulab)' fi # 验证 root 路径确实正确(防御性检查) if ! grep -q "root ${{ env.SERVER_PATH }}" /etc/nginx/conf.d/*.conf 2>/dev/null && \ ! grep -q "root ${{ env.SERVER_PATH }}" /etc/nginx/sites-available/default 2>/dev/null; then echo '⚠️ Nginx root 路径可能未正确设置,请手动检查' else echo '✅ Nginx root 路径正确,指向 ${{ env.SERVER_PATH }}' fi # 配置 API + WebSocket 反向代理 if ! grep -rq 'location /api/' /etc/nginx/sites-available/ /etc/nginx/conf.d/ /etc/nginx/snippets/ 2>/dev/null; then echo '🔄 添加 API + WebSocket 反向代理...' sudo mkdir -p /etc/nginx/snippets # 注意:\$ 在本地 shell 中转义为 $,远程 shell 的单引号保护不再展开, # 最终写入文件的 Nginx 变量为 $host、$http_upgrade 等 printf '%s\n' \ '# guanghulab 反向代理(由 CD 工作流自动生成)' \ '' \ '# AI 聊天 API 代理 → api-proxy.js 端口 3721(SSE 流式支持)' \ 'location /api/chat {' \ ' proxy_pass http://127.0.0.1:3721/api/chat;' \ ' proxy_http_version 1.1;' \ ' 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;' \ ' proxy_set_header Connection "";' \ ' proxy_buffering off;' \ ' proxy_cache off;' \ ' chunked_transfer_encoding on;' \ ' proxy_read_timeout 90s;' \ ' proxy_send_timeout 60s;' \ '}' \ '' \ 'location /api/models {' \ ' proxy_pass http://127.0.0.1:3721/api/models;' \ ' proxy_http_version 1.1;' \ ' proxy_set_header Host \$host;' \ ' proxy_set_header X-Real-IP \$remote_addr;' \ '}' \ '' \ 'location /api/health {' \ ' proxy_pass http://127.0.0.1:3721/api/health;' \ ' proxy_http_version 1.1;' \ ' proxy_set_header Host \$host;' \ ' proxy_set_header X-Real-IP \$remote_addr;' \ '}' \ '' \ '# Persona Studio API → persona-studio 后端端口 3002(含 CORS 跨域支持)' \ 'location /api/ps/ {' \ ' add_header Access-Control-Allow-Origin * always;' \ ' add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;' \ ' add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;' \ ' if (\$request_method = OPTIONS) {' \ ' return 204;' \ ' }' \ ' proxy_pass http://127.0.0.1:3002/api/ps/;' \ ' proxy_http_version 1.1;' \ ' 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;' \ ' proxy_read_timeout 90s;' \ '}' \ '' \ '# Persona Studio 前端静态文件' \ 'location /persona-studio/ {' \ ' proxy_pass http://127.0.0.1:3002/persona-studio/;' \ ' proxy_http_version 1.1;' \ ' proxy_set_header Host \$host;' \ ' proxy_set_header X-Real-IP \$remote_addr;' \ '}' \ '' \ '# 其他 API → Express 后端端口 3000' \ 'location /api/ {' \ ' proxy_pass http://127.0.0.1:3000/api/;' \ ' proxy_http_version 1.1;' \ ' 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;' \ ' proxy_cache_bypass \$http_upgrade;' \ '}' \ '' \ '# Persona Studio WebSocket → 预览进度推送端口 3002' \ 'location /ws/preview {' \ ' proxy_pass http://127.0.0.1:3002;' \ ' proxy_http_version 1.1;' \ ' proxy_set_header Upgrade \$http_upgrade;' \ ' proxy_set_header Connection \"Upgrade\";' \ ' 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_read_timeout 86400;' \ '}' \ '' \ '# WebSocket 反向代理 → 看板实时推送端口 8080' \ 'location /ws {' \ ' proxy_pass http://127.0.0.1:8080;' \ ' proxy_http_version 1.1;' \ ' proxy_set_header Upgrade \$http_upgrade;' \ ' proxy_set_header Connection \"Upgrade\";' \ ' 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_read_timeout 86400;' \ '}' \ | sudo tee /etc/nginx/snippets/guanghulab-proxy.conf > /dev/null # 在 server 块中加入 include 指令(仅搜索 sites-available 避免 symlink 重复) NGINX_MAIN=\$(grep -rl 'server_name' /etc/nginx/sites-available/ 2>/dev/null | head -1) if [ -z \"\$NGINX_MAIN\" ]; then NGINX_MAIN='/etc/nginx/sites-available/default' fi if [ -f \"\$NGINX_MAIN\" ] && ! grep -q 'guanghulab-proxy' \"\$NGINX_MAIN\" 2>/dev/null; then sudo sed -i '/server_name/a\\ include /etc/nginx/snippets/guanghulab-proxy.conf;' \"\$NGINX_MAIN\" 2>/dev/null && \ NGINX_CHANGED=true && echo '✅ 反向代理已自动配置' || \ echo '⚠️ 自动配置失败,请手动在 Nginx server 块中加入: include /etc/nginx/snippets/guanghulab-proxy.conf;' fi else echo '✅ API 反向代理已存在,无需修改' fi # 补建 _shared 目录(沙盒共享资源) if [ ! -d /var/www/_shared ]; then sudo mkdir -p /var/www/_shared sudo chmod 755 /var/www/_shared echo '✅ _shared 目录已创建' fi # 沙盒 alias 路由补充(DEV-004, DEV-005) # 根因:ZY-NGINX-ROOTFIX-2026-0325-001 · 沙盒模块需通过域名访问 SANDBOX_CONF='/etc/nginx/snippets/guanghulab-sandbox.conf' if [ ! -f \"\$SANDBOX_CONF\" ]; then printf '%s\n' \ '# guanghulab 沙盒路由(由 CD 工作流自动生成)' \ '' \ '# DEV-004 之之 · 沙盒路由' \ 'location /DEV-004/ {' \ ' alias /var/www/DEV-004/;' \ ' index index.html;' \ ' try_files \$uri \$uri/ =404;' \ '}' \ '' \ '# DEV-005 小草莓 · 沙盒路由' \ 'location /DEV-005/ {' \ ' alias /var/www/DEV-005/;' \ ' index index.html;' \ ' try_files \$uri \$uri/ =404;' \ '}' \ | sudo tee \"\$SANDBOX_CONF\" > /dev/null # 在 server 块中加入 sandbox include 指令 NGINX_MAIN=\$(grep -rl 'server_name' /etc/nginx/sites-available/ 2>/dev/null | head -1) if [ -z \"\$NGINX_MAIN\" ]; then NGINX_MAIN='/etc/nginx/sites-available/default' fi if [ -f \"\$NGINX_MAIN\" ] && ! grep -q 'guanghulab-sandbox' \"\$NGINX_MAIN\" 2>/dev/null; then sudo sed -i '/server_name/a\\ include /etc/nginx/snippets/guanghulab-sandbox.conf;' \"\$NGINX_MAIN\" 2>/dev/null && \ NGINX_CHANGED=true && echo '✅ 沙盒路由已自动配置' || \ echo '⚠️ 沙盒路由自动配置失败,请手动添加' fi else echo '✅ 沙盒路由配置已存在' fi # 如果有变更,验证并重新加载 Nginx if [ \"\$NGINX_CHANGED\" = true ]; then sudo nginx -t && sudo systemctl reload nginx && echo '✅ Nginx 配置已重新加载' || \ echo '⚠️ Nginx 配置验证失败,请手动检查' fi " - name: 🔍 验证部署 run: | ssh -i ~/.ssh/deploy_key \ ${{ env.SERVER_USER }}@${{ env.SERVER_HOST }} \ "echo '✅ 服务器连接正常' && \ echo '--- 站点根目录 ---' && \ ls -la ${{ env.SERVER_PATH }}/ | head -30 && \ echo '--- docs/ ---' && \ ls ${{ env.SERVER_PATH }}/docs/ 2>/dev/null || echo '(docs/ 不存在)' && \ echo '--- status-board/ ---' && \ ls ${{ env.SERVER_PATH }}/status-board/ 2>/dev/null || echo '(status-board/ 不存在)' && \ echo '--- cost-control/ ---' && \ ls ${{ env.SERVER_PATH }}/cost-control/ 2>/dev/null || echo '(cost-control/ 不存在)' && \ echo '--- devboard/ ---' && \ ls ${{ env.SERVER_PATH }}/devboard/ 2>/dev/null || echo '(devboard/ 不存在)' && \ echo '--- persona-studio/ ---' && \ ls ${{ env.SERVER_PATH }}/persona-studio/ 2>/dev/null || echo '(persona-studio/ 不存在)'" - name: 🏥 部署后冒烟测试 continue-on-error: true run: | echo "🏥 部署后全模块冒烟测试..." ssh -i ~/.ssh/deploy_key \ "${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" " DP='${{ env.SERVER_PATH }}' PASSED=0 FAILED=0 RESULTS='' check_module() { local name=\$1 local path=\$2 if [ -f \"\$DP/\$path/index.html\" ] || [ -f \"\$DP/\$path/index.htm\" ]; then PASSED=\$((PASSED+1)) RESULTS=\"\${RESULTS}✅ \${name} (\${path}/index.html)\n\" elif [ -d \"\$DP/\$path\" ]; then PASSED=\$((PASSED+1)) RESULTS=\"\${RESULTS}⚠️ \${name} (目录存在,无 index.html)\n\" else FAILED=\$((FAILED+1)) RESULTS=\"\${RESULTS}❌ \${name} (\${path}/ 不存在)\n\" fi } check_module '主站首页' 'docs' check_module '系统状态看板' 'status-board' check_module '成本控制' 'cost-control' check_module '开发者看板' 'devboard' check_module '光湖集成壳' 'app' check_module 'Persona Studio' 'persona-studio' check_module '后端服务' 'backend' check_module '用户中心' 'm05-user-center' check_module '对话界面' 'm07-dialogue-ui' check_module '云服务' 'm10-cloud' check_module '模块管理' 'm11-module' check_module '云盘' 'm15-cloud-drive' check_module '登录' 'm01-login' check_module '健康检查' 'm18-health-check' check_module '冷启动' 'coldstart' check_module '仪表盘' 'dashboard' check_module '门户' 'portal' check_module '通知' 'notification' check_module '帮助中心' 'help-center' check_module '前端' 'frontend' echo '' printf \"\$RESULTS\" echo '' echo \"━━━ 冒烟测试结果:\${PASSED} 通过 / \${FAILED} 失败 ━━━\" # 写入部署状态文件 TIMESTAMP=\$(date -u '+%Y-%m-%dT%H:%M:%SZ') if command -v jq &>/dev/null; then jq -n --arg ts \"\$TIMESTAMP\" --arg commit \"${{ github.sha }}\" \ --argjson passed \"\$PASSED\" --argjson failed \"\$FAILED\" \ '{last_deploy: \$ts, commit: \$commit, modules_passed: \$passed, modules_failed: \$failed}' \ > \"\$DP/data/deploy-status.json\" 2>/dev/null || true else printf '{\"last_deploy\":\"%s\",\"commit\":\"%s\",\"modules_passed\":%d,\"modules_failed\":%d}' \ \"\$TIMESTAMP\" \"${{ github.sha }}\" \"\$PASSED\" \"\$FAILED\" \ > \"\$DP/data/deploy-status.json\" 2>/dev/null || true fi " echo "✅ 冒烟测试完成" - name: 👤 人类使用侧验证 continue-on-error: true run: | echo "👤 人类使用侧验证(核心!系统测跑通 ≠ 人类测跑通)..." echo "等待 10 秒让服务启动..." sleep 10 HUMAN_CHECK_PASSED=0 HUMAN_CHECK_FAILED=0 # 验证主站可访问 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \ "http://${{ env.SERVER_HOST }}" 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "301" ] || [ "$HTTP_CODE" = "302" ]; then echo "✅ 主站 HTTP 响应: $HTTP_CODE" HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1)) else echo "❌ 主站 HTTP 响应: $HTTP_CODE(人类无法访问)" HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1)) fi # 验证 Persona Studio 可访问 PS_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \ "http://${{ env.SERVER_HOST }}/persona-studio/" 2>/dev/null || echo "000") if [ "$PS_CODE" = "200" ] || [ "$PS_CODE" = "301" ] || [ "$PS_CODE" = "302" ]; then echo "✅ Persona Studio HTTP 响应: $PS_CODE" HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1)) else echo "⚠️ Persona Studio HTTP 响应: $PS_CODE" HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1)) fi # 验证后端 API 健康 API_CODE=$(ssh -i ~/.ssh/deploy_key \ "${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" \ "curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://localhost:3002/api/ps/health 2>/dev/null || echo '000'" \ 2>/dev/null || echo "000") if [ "$API_CODE" = "200" ]; then echo "✅ Persona Studio API 健康: $API_CODE" HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1)) else echo "⚠️ Persona Studio API 响应: $API_CODE" HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1)) fi echo "" echo "━━━ 人类使用侧验证结果:${HUMAN_CHECK_PASSED} 通过 / ${HUMAN_CHECK_FAILED} 异常 ━━━" echo "### 👤 人类使用侧验证" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- 通过: ${HUMAN_CHECK_PASSED}" >> $GITHUB_STEP_SUMMARY echo "- 异常: ${HUMAN_CHECK_FAILED}" >> $GITHUB_STEP_SUMMARY - name: 📝 部署汇总 run: | echo "### 🚀 全站部署完成" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- 同步范围: 全仓库(含排除规则)" >> $GITHUB_STEP_SUMMARY echo "- 目标: \`${{ env.SERVER_HOST }}:${{ env.SERVER_PATH }}\`" >> $GITHUB_STEP_SUMMARY echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY echo "- 时间: $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M CST')" >> $GITHUB_STEP_SUMMARY echo "- 人类使用侧验证: 已执行" >> $GITHUB_STEP_SUMMARY # ═══════════════════════════════════════════════════════════════ # Job 3: Notion 部署通知 # ═══════════════════════════════════════════════════════════════ notify: name: "📡 部署通知 → Notion" needs: deploy runs-on: ubuntu-latest if: success() steps: - name: 📥 检出代码 uses: actions/checkout@v4 with: fetch-depth: 2 - name: 🟢 配置 Node.js uses: actions/setup-node@v4 with: node-version: "20" - name: 📡 推送部署记录到 Notion env: NOTION_TOKEN: ${{ secrets.ZY_NOTION_TOKEN }} CHANGES_DB_ID: ${{ secrets.ZY_NOTION_CHANGELOG_DB }} EVENT_TYPE: commit COMMIT_SHA: ${{ github.sha }} COMMIT_MSG: "[CD 部署] ${{ github.event.head_commit.message }}" COMMITTER: ${{ github.event.head_commit.author.name || github.actor }} COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }} BRANCH: ${{ github.ref_name }} run: | CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || true) export CHANGED_FILES="$CHANGED" if [ -f "scripts/notion-bridge.js" ]; then node scripts/notion-bridge.js changes echo "✅ Notion 部署通知已发送" else echo "⏭️ scripts/notion-bridge.js 不存在,跳过 Notion 通知" fi # ━━━ 🧬 双端神经系统·自报告 ━━━ - name: "🧬 写入神经自报告" if: always() run: | WORKFLOW_ID="cd-deploy" BRAIN="AG-SY-01" REPORT_DIR="data/neural-reports/cd-deploy" TIMESTAMP=$(TZ=Asia/Shanghai date +%Y-%m-%d-%H%M) STATUS="${{ job.status }}" mkdir -p "$REPORT_DIR" cat > "${REPORT_DIR}/${WORKFLOW_ID}-${TIMESTAMP}.json" << EOF { "workflow_id": "${WORKFLOW_ID}", "run_id": "${{ github.run_id }}", "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "status": "${STATUS}", "brain": "${BRAIN}", "event": "${{ github.event_name }}", "branch": "${{ github.ref_name }}", "commit": "${{ github.sha }}" } EOF git config user.name "zhuyuan-bot" git config user.email "zhuyuan@guanghulab.com" git add "$REPORT_DIR/" git diff --cached --quiet || \ git commit -m "🧬 ${WORKFLOW_ID} 自报告 · ${TIMESTAMP} [skip ci]" git push || echo "⚠️ 自报告push失败(不阻断主流程)" # ━━━ 📡 指令回执·自动同步 ━━━ - name: "📡 同步回执到 Notion" if: always() env: NOTION_API_KEY: ${{ secrets.ZY_NOTION_TOKEN }} RECEIPT_DB_ID: ${{ secrets.ZY_NOTION_RECEIPT_DB }} run: | node scripts/neural/write-receipt-to-notion.js \ --instruction-id "${{ github.event.inputs.instruction_id || 'AUTO' }}" \ --status "${{ job.status }}" \ --workflow "cd-deploy" \ --summary "cd-deploy 自动执行" \ --related-agent "AG-SY-01"