From 242548ea847890c067b46c3db0a004735ad2afb8 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 6 Jul 2026 14:16:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E8=93=9D=E7=BB=BF=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=E5=8F=8D=E8=BD=AC=E2=80=94=E2=80=94nginx=20=E6=8E=A5?= =?UTF-8?q?=E7=AE=A1=E5=AF=B9=E5=A4=96=208765,=E5=AE=9E=E4=BE=8B=E6=8C=AA?= =?UTF-8?q?=208766/8767(bump=200.41.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对外契约(URL:8765)保持不变:客户端 / platform 嵌入 / EMBED 对接方零改动; blue=8766、green=8767 只绑 127.0.0.1。bootstrap 顺序同步调整(停旧服务释放 8765 后 nginx 才能 reload 接管);已知边缘补文档:切换窗口内对旧实例 run 点「停止」送达不到(cancel 与 event 同为进程内 broker,根因一致)。 Co-Authored-By: Claude Fable 5 --- PROGRESS.md | 3 ++- RUN.md | 17 ++++++++++------- core/__init__.py | 2 +- deploy/instances/blue.env | 2 +- deploy/instances/green.env | 2 +- deploy/nginx/zcbot.conf.example | 13 ++++++++----- deploy/update_bluegreen.sh | 9 +++++---- deploy/zcbot@.service | 5 +++-- 8 files changed, 31 insertions(+), 22 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index f06c69f..6772f84 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-07-06(蓝绿无感部署 B 档落地,bump 0.41.0) +最后更新:2026-07-06(蓝绿端口方案改 nginx 接管 8765,bump 0.41.1) --- @@ -23,6 +23,7 @@ ### 2026-07 +- **07-06 / 0.41.1 蓝绿端口方案反转**:nginx 接管原对外端口 8765(客户端 / platform 嵌入零改动),实例挪 8766(blue)/ 8767(green) 只绑本机——对外契约(URL:8765)不动优先于"实例沿用旧端口"的省事;顺带把「切换窗口内对旧实例 run 点停止送达不到」补进已知边缘文档。 - **07-06 / 0.41.0 蓝绿无感部署(RUN.md B 档落地)**:生产部署零 503 窗口 + 在跑对话 SSE 直播不断。`zcbot@blue/green` 双 systemd 实例(模板 unit + `deploy/instances/*.env`)+ nginx upstream 单行文件切流,编排在 `deploy/update_bluegreen.sh`(复用 `update.sh --prepare-only`;切流后轮询 DB 等旧色 run 自然清空再 stop——直接 stop 会被 uvicorn `timeout_graceful_shutdown=5` 掐 SSE)。双实例并存互踩三修:`tasks.run_owner`(0020)reaper 只收自己色、sandbox 容器名/label 带实例色各清各的、微信 ClawBot 长轮询 PG advisory lock 选主(detach 连接持锁,断连即让位);定时任务 claim 本就 SKIP LOCKED 免改。关键判断:broker 刻意不出进程(不上 Redis),接受"切换瞬间刷新的用户看不到旧实例直播"这一边缘。单实例部署 `ZCBOT_INSTANCE` 未设全部退回原行为,`update.sh` 保留不废。 - **07-06 / 0.40.0 ppt 伪导出根治(三层)**:0.35.1 复发(模型照禁令里的"配方"装 cairosvg 手搓整页贴图导出)复盘——①平台层机检门落地(`core/pptx_guard.py` + loop 钩子:shell/run_python 新产出 .pptx 命中"≥80% 页整版位图 + 全 deck 零原生文本"即注入 `[产物机检 ERROR]` 当场逼返工;判定刻意保守,`tests/test_pptx_guard.py` 8 例);②禁令去菜谱:ppt/brief SKILL.md 否定式指令改"正面窄门"(唯一入口 + 报错就修/上报),删掉可照做的违规配方;③官方脚本 5 处 `pip install` hint 改环境说明(pptx_media/svg_preview/crop_images/fix_image_aspect/pptx_cli)——打印给 agent 的安装指令就是执行指令。详 DESIGN §8.9。 - **07-06 / 0.39.2 research 对接 paper_server API Key**:paper_server GET 接口收紧要 Key 后,`skills/research/paper.py` 全部请求带 `?api_key=`(env `PAPER_SERVER_API_KEY`),认证失败(paper_server 异常处理器压成 HTTP 400 + err_code)转成带指引的 RuntimeError;这把低价值只读 key 是唯一刻意放进 sandbox 的凭证——`run_python` env 过滤器加 allowlist 放行、docker 模式 `_sandbox_env()` 透传进容器(顺带修掉 `PAPER_SERVER_URL` 覆盖不进 docker 的缺口)。 diff --git a/RUN.md b/RUN.md index b18bb5b..8707a24 100644 --- a/RUN.md +++ b/RUN.md @@ -310,7 +310,7 @@ ExecStart=/opt/zcbot/.venv/bin/python main.py web --host 0.0.0.0 --port 8765 --r ### 蓝绿部署(B 档,已实现,生产推荐) -两个 systemd 实例 `zcbot@blue`(127.0.0.1:8765)/ `zcbot@green`(127.0.0.1:8766),nginx upstream(`/etc/nginx/zcbot_upstream.conf`,单行 `server` 指令)在中间切流量。日常部署一条命令: +两个 systemd 实例 `zcbot@blue`(127.0.0.1:8766)/ `zcbot@green`(127.0.0.1:8767),**nginx 对外监听原单实例端口 8765** —— 客户端 / platform 嵌入 URL 一个字不用改;upstream(`/etc/nginx/zcbot_upstream.conf`,单行 `server` 指令)在两实例间切流量。日常部署一条命令: ```bash sudo bash /opt/zcbot/deploy/update_bluegreen.sh @@ -321,7 +321,7 @@ sudo bash /opt/zcbot/deploy/update_bluegreen.sh 脚本流程:`update.sh --prepare-only`(拉码 / pip / migration / build,全复用单实例脚本)→ 起空闲色实例 → healthz 验活(核对 `instance` 字段)→ 改写 upstream + nginx reload(**新请求全走新实例,零 503**)→ **轮询 DB 等旧色在跑 run 自然清空**(上限 `DRAIN_WAIT_SECONDS`,期间旧 SSE 继续直播不断流 —— 直接 stop 的话 uvicorn `timeout_graceful_shutdown=5` 会 5s 掐长连接)→ stop 旧实例(残留 run 走 SIGTERM drain / 协作式 cancel)→ 旧色 SIGKILL 残留的 running 行标 error → enable 新色 / disable 旧色(重启机器只拉起 live 色)。 -效果 vs 单实例 `update.sh`:**没有 503 窗口,在跑对话的 SSE 直播也不断**。已知边缘(接受):用户恰在切换后刷新页面、而他的 run 还在旧实例上跑 → 新实例 broker 没有该 run 的事件,直播看不到(run 照常在旧实例跑完落库,刷新看历史即可)。broker 仍是进程内(`web/broker.py`)不跨实例 —— 刻意取舍,当前规模不值得上 Redis pub/sub。 +效果 vs 单实例 `update.sh`:**没有 503 窗口,在跑对话的 SSE 直播也不断**。已知边缘(接受,仅存在于切换后旧色 drain 的几分钟窗口内、且 run 恰好还在旧实例上):① 用户刷新页面 → SSE 连到新实例,broker 没有该 run 的事件,直播看不到(run 照常在旧实例跑完落库,完了刷新看历史);② 点「停止」→ cancel 信号进的是新实例的进程内 broker,旧实例收不到,等 run 自然收尾即可。两者根因同一个:event/cancel broker 是进程内的(`web/broker.py`)不跨实例 —— 刻意取舍,当前规模不值得上 Redis pub/sub(取舍详见 DESIGN §7.0)。 蓝绿并存期的互踩问题已在代码层解决(单实例部署 `ZCBOT_INSTANCE` 未设,以下全部退回原行为): - **启动 reaper**:`tasks.run_owner`(migration 0020)记 run 归属实例,实例启动只收自己色(+ NULL 遗留)的孤儿 run,不误杀另一实例在跑的 @@ -340,24 +340,27 @@ sudo bash deploy/update.sh sudo cp deploy/zcbot@.service /etc/systemd/system/ sudo systemctl daemon-reload -# 3) 装 nginx + 反代配置 + 初始 upstream 指 blue(conf 里按需改 server_name / TLS / 对外端口) +# 3) 装 nginx + 反代配置 + 初始 upstream 指 blue(conf 里按需改 server_name / TLS) +# 此时先别 reload —— nginx 要监听 8765,旧单实例服务还占着 sudo apt-get install -y nginx sudo cp deploy/nginx/zcbot.conf.example /etc/nginx/conf.d/zcbot.conf -echo 'server 127.0.0.1:8765;' | sudo tee /etc/nginx/zcbot_upstream.conf +echo 'server 127.0.0.1:8766;' | sudo tee /etc/nginx/zcbot_upstream.conf -# 4) 停旧单实例服务(优雅 drain)→ 清无实例 label 的旧 sandbox 容器 → 起 blue → nginx 生效 +# 4) 停旧单实例服务(优雅 drain,释放 8765)→ 清无实例 label 的旧 sandbox 容器 +# → 起 blue(8766)→ nginx 接管 8765。停旧到 nginx 起来之间是本次唯一的中断窗口 sudo systemctl disable --now zcbot sudo docker rm -f $(sudo docker ps -aq -f label=zcbot.product=sandbox) 2>/dev/null || true sudo systemctl enable --now zcbot@blue -curl -fsS http://127.0.0.1:8765/healthz # 应含 "instance":"blue" +curl -fsS http://127.0.0.1:8766/healthz # 应含 "instance":"blue" sudo nginx -t && sudo systemctl reload nginx +curl -fsS http://127.0.0.1:8765/healthz # 经 nginx,应同样返回 ok # 5) 之后日常部署一律: sudo bash deploy/update_bluegreen.sh ``` 注意: -- **对外端口变了**:客户端从直连 8765 改走 nginx(80/443)。platform 嵌入 / EMBED.md 对接方同步换 URL;实例只绑 127.0.0.1 不再直接对外。 +- **对外端口不变**:nginx 接管 8765,客户端 / platform 嵌入 / EMBED.md 对接方零改动;实例(8766/8767)只绑 127.0.0.1 不对外。原来若用 uvicorn 原生 TLS(`--ssl-certfile`)跑 8765,证书挪到 nginx 层配。 - **回滚 ≠ 切回旧色**:代码目录两实例共享,旧实例重启后跑的也是新代码。回滚 = `git revert` 后重跑 `update_bluegreen.sh`。`--switch` 只用于半途恢复(如新实例切完流量后挂了,切回还活着的那个色)。 - `deploy/update.sh`(单实例版)保留不废:蓝绿脚本内部复用它 `--prepare-only`;单实例部署机(host backend / 内网机)继续用它,行为不变。 - unit 的 `TimeoutStopSec=90` 与 `config/agent.yaml` shutdown 段(drain 30s + cancel 15s)的约束同单实例:改 yaml 数值时模板 unit 跟着调。 diff --git a/core/__init__.py b/core/__init__.py index 8b94742..ee68b1d 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.41.0" +__version__ = "0.41.1" diff --git a/deploy/instances/blue.env b/deploy/instances/blue.env index 1d125a0..6f5482f 100644 --- a/deploy/instances/blue.env +++ b/deploy/instances/blue.env @@ -1,4 +1,4 @@ # zcbot@blue 实例配置(systemd 模板 unit EnvironmentFile,RUN.md B 档) # 端口/实例名是蓝绿脚本与 nginx upstream 的契约,别改;业务配置进 /opt/zcbot/.env -ZCBOT_PORT=8765 +ZCBOT_PORT=8766 ZCBOT_INSTANCE=blue diff --git a/deploy/instances/green.env b/deploy/instances/green.env index 1e0c3eb..e6bd0da 100644 --- a/deploy/instances/green.env +++ b/deploy/instances/green.env @@ -1,4 +1,4 @@ # zcbot@green 实例配置(systemd 模板 unit EnvironmentFile,RUN.md B 档) # 端口/实例名是蓝绿脚本与 nginx upstream 的契约,别改;业务配置进 /opt/zcbot/.env -ZCBOT_PORT=8766 +ZCBOT_PORT=8767 ZCBOT_INSTANCE=green diff --git a/deploy/nginx/zcbot.conf.example b/deploy/nginx/zcbot.conf.example index d13c233..cd179d4 100644 --- a/deploy/nginx/zcbot.conf.example +++ b/deploy/nginx/zcbot.conf.example @@ -1,22 +1,25 @@ # zcbot nginx 反代示例(蓝绿双实例,RUN.md §无感更新 B 档)。 # +# 端口约定:nginx 接管原单实例的对外端口 8765(客户端 / platform 嵌入 URL 不用改), +# 实例挪到本机 8766(blue)/ 8767(green)。装 nginx 前先停旧单实例服务释放 8765。 +# # 安装(一次性): -# sudo cp deploy/nginx/zcbot.conf.example /etc/nginx/conf.d/zcbot.conf # 按需改 server_name/端口/TLS -# echo 'server 127.0.0.1:8765;' | sudo tee /etc/nginx/zcbot_upstream.conf # 初始指 blue +# sudo cp deploy/nginx/zcbot.conf.example /etc/nginx/conf.d/zcbot.conf # 按需改 server_name/TLS +# echo 'server 127.0.0.1:8766;' | sudo tee /etc/nginx/zcbot_upstream.conf # 初始指 blue # sudo nginx -t && sudo systemctl reload nginx # # 之后 zcbot_upstream.conf 由 deploy/update_bluegreen.sh 改写 + reload nginx 切流量, # 本文件不再动。nginx reload 对已建立连接零影响:旧 SSE 留在旧实例自然收尾,新请求走新实例。 upstream zcbot_backend { - # 只含一行 server 127.0.0.1:8765|8766;由 update_bluegreen.sh 维护 + # 只含一行 server 127.0.0.1:8766|8767;由 update_bluegreen.sh 维护 include /etc/nginx/zcbot_upstream.conf; keepalive 32; } server { - listen 80; - # TODO 按部署域名改;上 TLS 时换 listen 443 ssl + 证书(certbot 等) + # 对外沿用原单实例端口,客户端无感;上 TLS 时换 listen 443 ssl + 证书(certbot 等) + listen 8765; server_name _; # 上传接口(/v1/files/upload)会传大文件;与后端无硬限制,给足余量 diff --git a/deploy/update_bluegreen.sh b/deploy/update_bluegreen.sh index abb14a8..1abdac2 100755 --- a/deploy/update_bluegreen.sh +++ b/deploy/update_bluegreen.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # zcbot 蓝绿无感部署(RUN.md §无感更新 B 档)。 # -# 拓扑:zcbot@blue(127.0.0.1:8765)/ zcbot@green(127.0.0.1:8766)双 systemd 实例, -# nginx upstream(/etc/nginx/zcbot_upstream.conf,单行 server 指令)在中间切流量。 +# 拓扑:zcbot@blue(127.0.0.1:8766)/ zcbot@green(127.0.0.1:8767)双 systemd 实例, +# nginx 对外监听原单实例端口 8765(客户端 / platform 嵌入无感),upstream +# (/etc/nginx/zcbot_upstream.conf,单行 server 指令)在两实例间切流量。 # # 干什么(顺序写死): # 1) update.sh --prepare-only 拉码 / pip / db migration / docker build(复用单实例脚本) @@ -50,7 +51,7 @@ UPSTREAM_FILE="${UPSTREAM_FILE:-/etc/nginx/zcbot_upstream.conf}" # 切流量后等旧实例在跑 run 自然收尾的上限(超了就 stop,残留 run 走协作式 cancel) DRAIN_WAIT_SECONDS="${DRAIN_WAIT_SECONDS:-600}" -declare -A PORT=([blue]=8765 [green]=8766) +declare -A PORT=([blue]=8766 [green]=8767) log() { echo "[deploy] $*"; } fail() { echo "[deploy] ERROR: $*" >&2; exit 1; } @@ -65,7 +66,7 @@ command -v nginx >/dev/null || fail "nginx 未安装(蓝绿依赖 nginx 反代, [ -f "$UPSTREAM_FILE" ] || fail "缺 $UPSTREAM_FILE(nginx upstream 未初始化,见 RUN.md §蓝绿部署 bootstrap)" systemctl cat zcbot@blue >/dev/null 2>&1 || fail "模板 unit 未安装(sudo cp deploy/zcbot@.service /etc/systemd/system/ && daemon-reload)" if systemctl is-active --quiet zcbot 2>/dev/null; then - fail "旧单实例服务 zcbot.service 还在跑(占 8765 与 zcbot@blue 冲突)—— 先走 RUN.md §蓝绿部署 bootstrap 停掉它" + fail "旧单实例服务 zcbot.service 还在跑(占 8765,与 nginx 对外监听冲突)—— 先走 RUN.md §蓝绿部署 bootstrap 停掉它" fi DB_URL="$(grep -E '^[[:space:]]*ZCBOT_DB_URL=' "$APP_DIR/.env" | tail -n1 | cut -d= -f2-)" [ -n "$DB_URL" ] || fail ".env 里没读到 ZCBOT_DB_URL" diff --git a/deploy/zcbot@.service b/deploy/zcbot@.service index 4bf2234..ecfee1f 100644 --- a/deploy/zcbot@.service +++ b/deploy/zcbot@.service @@ -1,8 +1,9 @@ # zcbot 蓝绿双实例模板 unit(RUN.md §无感更新 B 档)。 # 安装:sudo cp deploy/zcbot@.service /etc/systemd/system/ && sudo systemctl daemon-reload -# 实例:zcbot@blue(port 8765)/ zcbot@green(port 8766),端口与实例名由 +# 实例:zcbot@blue(port 8766)/ zcbot@green(port 8767),端口与实例名由 # deploy/instances/<实例>.env 注入(ZCBOT_PORT / ZCBOT_INSTANCE)。 -# nginx upstream 在两实例间切流量,部署走 deploy/update_bluegreen.sh。 +# 对外端口仍是 8765 —— 由 nginx 接管(客户端 / platform 嵌入无感),upstream 在 +# 两实例间切流量,部署走 deploy/update_bluegreen.sh。 [Unit] Description=zcbot web (FastAPI/uvicorn) - instance %i After=network-online.target postgresql.service