自建的 OpenAI 兼容网关跑了几个月,一直相安无事,直到某天发现"签到按钮还在、任务列表也拉得出来,但一个任务都完成不了"。顺藤摸瓜把版本升级了,又顺手把准备了很久的多重免费备用链接了进去。这篇把过程和判断依据写下来,方便以后照着复现。
文中涉及的所有地址、密钥、账号均为占位符。
一、先判断:这次到底该不该升
看到"有新版本"就升级,是最容易出事的做法。升级前先做三件事:
- 比镜像 digest,而不是比 tag。
latest这种 tag 会漂,docker inspect --format '{{index .RepoDigests 0}}'拿到的 digest 变了才是真的换了内容。 - 看上游 commits/releases 里有没有和你用途相关的东西。你用它跑对话,那"模型目录调整"“限流重试"“鉴权变更"才算关键;纯文档、拼写修正的提交可以直接忽略。
- 翻 issue 区找别人的实测。如果症状和你一致(例如"任务列表能拉到但没有任务码”),说明不是你环境的问题,升级才值得做。
二、升级实录:三个坑
坑 1:配置被打进镜像,重建即回滚
现象:升级完,面板和 API 全部 401,原因很简单——api_key 变回了镜像里的默认占位值。
根因:/app/config.json 是构建进镜像的,容器并没有挂载它。docker compose up -d 重建容器 = 用新镜像里的默认配置覆盖你的自定义项。
应急做法:
# 升级前
docker exec <容器> cat /app/config.json > /root/gw-config.bak.json
docker compose pull -q && docker compose up -d
# 升级后还原
docker cp /root/gw-config.bak.json <容器>:/app/config.json
docker restart <容器>
根治做法是把配置挂出来,以后重建容器不再丢:
services:
gw:
image: <镜像>:<固定tag>
volumes:
- ./config.json:/app/config.json
- gw_data:/app/data
顺带建议:升级前把镜像打成固定 tag(如 gw:2026-10-21),compose.yaml 里也别写 latest——不然哪天上游悄悄推一版,你重建一次容器就"被升级"了。
坑 2:版本号成了 dev,面板永远提示"有更新”
源码构建的版本号是靠构建参数注入的:
ARG APP_VERSION=dev
RUN go build -ldflags="-s -w -X main.appVersion=${APP_VERSION}" ...
不传这个参数,编出来的二进制里版本号就是 dev。面板拿它去跟最新 release 比,永远判定"有更新",很烦人。
docker build --build-arg APP_VERSION=$(date +%Y.%m.%d.%H%M) -t gw:local .
验证方式:构建后看 /panel/api/update,current_version 应该 ≥ latest_version 才对。
坑 3:功能"看着在,其实废了"
这是最隐蔽的一个。现象:
- 签到页有按钮,点了也返回结果
- 单账号任务列表能拉到 5 条
- 但每条都是
task_code: ""、current/target全是 0 - 全量扫描接口返回
pending_count: 0
结论:上游改了返回结构,旧版解析不出任务码 → accept/claim 全都无从下手。界面还在,功能已经死了。
可复用的判据:列表接口返回的每一行,有没有唯一 ID 和进度字段。没有就按"只读"看待,别指望能自动执行。这比看界面元素可靠得多。
处理方式只有一个:换到仍在维护的分支。原项目已在 README 里公告停止维护,换个每天还在提交的下游 fork,任务功能立刻恢复(当天实测 25 项任务全部执行并领奖)。
三、健康检查与回滚:每次升级必做
升级脚本的骨架大概是这样,重点是带密钥探活和留回滚路径:
OLD=$(docker inspect -f '{{.Image}}' gw)
docker compose pull -q
docker compose up -d
sleep 12
code=$(curl -s -m8 -o /dev/null -w '%{http_code}' \
http://127.0.0.1:PORT/v1/models -H "Authorization: Bearer $KEY")
if [ "$code" != "200" ]; then
echo "health fail -> rollback"
docker tag "$OLD" gw:latest
docker compose up -d
exit 2
fi
三个细节别省:
- 带 key 探活。不带 key 返回 401 只能证明进程活着,证明不了鉴权配置是对的。
sleep别省。容器 Status 变 healthy 不等于端口就绪、配置加载完。- 留旧镜像 ID。回滚只需要 retag 再
up -d,比重装快得多。
四、多重免费备用链怎么排
结构上是一个瀑布,主链失败才往下走:
客户端
└─> 主网关(自建,账号池轮转)────> 模型 A
│ 失败 / 429 / 鉴权异常
├─> 备用 1:国内免费额度(低延迟)
├─> 备用 2:另一家国内免费额度
├─> 备用 3:海外免费额度(需代理)
└─> 备用 4:聚合平台的 free 型号(垫底)
配置大概长这样(各家都提供 OpenAI 兼容端点,所以可以直接串):
model:
default: "custom-gateway:model-a"
fallback_providers:
- provider: custom:GLM
model: glm-4.5-flash
base_url: https://open.bigmodel.cn/api/paas/v4
- provider: custom:HUNYUAN
model: hy3
base_url: https://<second-provider>/v1
- provider: custom:GROQ
model: qwen/qwen3.8-27b
base_url: https://api.groq.com/openai/v1
- provider: custom:OPENROUTER
model: <free-model-id>
base_url: https://openrouter.ai/api/v1
排序原则(这部分是踩出来的,不是查出来的):
- 先排同区域的。延迟低、不需要代理、不会因为代理掉线跟着挂。
- 再排额度稳定的。有明确速率限制但长期可用的,优先于"额度看着大但经常抽风"的。
- 要代理的放最后。链路最长,故障点最多。
- 同一家服务商只能占一档。同源故障会一起挂,等于没有备用。
几个已知的坑:
- 只实现
/chat/completions、没有/responses的网关,接不上只认 Responses API 的客户端。选型时先确认协议,别装完才发现。 - 大上下文切模型会全量重读。每个 provider 的 prompt 缓存是独立的,长会话中途切换等于把上下文按全价重算一遍,费用和延迟都会跳。
- 免费层普遍限速。并发一高就 429,客户端侧要有退避重试,不然表现为"对话到一半断开"。
五、免费额度怎么"滚"起来
免费额度有两种机制,别混着理解:
| 类型 | 判定 | 能否自动化 |
|---|---|---|
| 每日活跃奖励 | 当天通过官方客户端发起一次对话 | 自动,次日批量发放,无需领取 |
| 成长任务 | 列表 → accept → 执行动作 → claim | 需要能拿到任务码 + 进度字段才行 |
经验:
- 官方规则里通常写明了"客户端发起的对话才算活跃,网页端不算",脚本、模拟器、批量注册产生的行为也不算。所以想靠网页会话去刷活跃,多数是白费功夫。
- 奖励到账有延迟。当天跑完当场看不到余额变化很正常,看第二天的积分包明细才准。
- 企业版账号大多没有"个人成长体系",任务接口会直接报错。这类账号留在模型池里正常调用就好,别放进任务队列——否则每轮扫描刷一堆 error 日志。
六、把运维交给定时任务
升级完后配了三个自动化,都跑在 no_agent 模式(纯脚本,不烧 token):
1. 每晚检查关键更新
git fetch -q origin main
git log --format='%h|%ci|%s' HEAD..origin/main
再按关键词过滤:任务/积分/登录/token/模型/限流/风控/api_key/配置。命中才发通知,全是文档修正就静默——不然每天一条"有更新"很快就脱敏了。
2. 每天自检:容器健康、带 key 探活、账号数、积分快照,异常才说话。
3. 日志只看三个数:TTFB、token/s、缓存命中率。缓存命中掉到 90% 以下,通常意味着 prompt 前缀被某次改动破坏了,得回头看配置。
七、隐私与安全清单
这套东西天然带着一堆敏感信息,外发前过一遍:
- 清单:内网 IP、公网 IP、域名、API key、账号 uid、昵称、邮箱
- 示例配置一律用占位符,别贴真实 base_url
- 密钥不进 git:
~/.config/<service>/env,权限 600,仓库里.gitignore兜住 - 面板别直接开公网:反代 + 认证 + TLS,必须开放时也只绑
127.0.0.1再反代出去 - 健康检查用
127.0.0.1而不是localhost:后者可能解析到 IPv6,会报出假的"服务挂了"
八、复盘
值得自动化的:探活、备份、更新检查、额度快照。
不值得的:追每一个小提交;为了几十点积分再起一套服务。
一条原则可以收尾:能机械判断的交给脚本,需要判断力的才留给人。这套网关升级加备用链,真正花时间的不是敲命令,而是判断"这个功能是不是真的坏了"“这次升级值不值”——这些恰恰是脚本代替不了的。
