日期:2026-08-14
环境:Debian 13 工作站 + 内网服务器(192.168.31.82)
涉及组件:opencode、转发服务(7890)、cliproxy-api / cpa(8317)
一、问题现象
opencode 通过 cpa(http://192.168.31.82:8317)调用 API 时,持续报错:
text
AI_APICallError: Bad Gateway
Bad Gateway [retrying in 2s attempt #2]
二、排查过程
第一层:cpa 本身是否正常
SSH 到服务器直接测试:
bash
curl http://127.0.0.1:8317/v1/models
# 返回 HTTP 200,cpa 自身正常
cpa 配置中 deepseek-v4-flash 映射了 4 个上游,采用 round-robin 轮询。其中一个上游完全不可达,而 cpa 设置了重试机制(request-retry: 2,max-retry-interval: 30),每次轮询到该上游会卡住 30 秒,三次超时共 90 秒,最终返回 502。
阶段方案:禁用不可达上游。
第二层:opencode 配置字段名错误
opencode 配置文件写的是 "provider"(单数),但实际读取的是 "providers"(复数),导致配置未生效。修正为 "provider" 后配置被正确加载。
第三层:Node.js 对 NO_PROXY 的 CIDR 格式支持问题
这是根本原因。
工作站配置了转发环境变量:
bash
export http_proxy=http://192.168.31.82:7890
export NO_PROXY="192.168.0.0/16"
测试结果:
| 工具 | 对 CIDR 的支持 | 访问 192.168.31.82 的实际行为 |
|---|---|---|
| curl 8.14 | ✅ 支持 | 直连(在 NO_PROXY 范围内) |
| Node.js / undici | ❌ 不支持 | 走转发(认为不在范围内) |
opencode 是 Node.js 应用,其 HTTP 请求不受 NO_PROXY 中的 CIDR 规则控制,所有流量都被发往转发服务 7890。但当时转发服务不可用,导致连接失败,表现为 502。
阶段方案 :将 NO_PROXY 改为精确 IP:
bash
export NO_PROXY="localhost,127.0.0.1,192.168.31.82"
第四层:systemd 用户会话固化了环境变量
修改 ~/.bashrc 后仍然无效,因为 systemd user manager 在登录时就已固化环境变量,不会自动读取 shell 配置文件。
bash
# 查看当前固化值
systemctl --user show-environment | grep NO_PROXY
# 更新
systemctl --user set-environment NO_PROXY="localhost,127.0.0.1,192.168.31.82"
第五层:gsettings 代理模式
检查发现 gsettings 的转发模式为 manual,某些图形应用会从这里读取配置。虽然 opencode 是 TUI 不依赖此配置,但保持环境一致是必要的。
三、最终方案
核心思路:让转发服务自己判断哪些请求走转发通道,哪些直连。
在转发服务的配置文件中,将内网 IP 段设置为 DIRECT(直连),并放在规则列表最前面:
yaml
rules:
- 'IP-CIDR,192.168.0.0/16,DIRECT,no-resolve'
- 'IP-CIDR,10.0.0.0/8,DIRECT,no-resolve'
- 'IP-CIDR,172.16.0.0/12,DIRECT,no-resolve'
- 'IP-CIDR,127.0.0.0/8,DIRECT,no-resolve'
- 'GEOIP,CN,DIRECT'
- 'MATCH,ALL'
这样配置后:
-
客户端所有 HTTP/HTTPS 流量都发给转发服务 7890
-
转发服务收到请求后检查目标 IP
-
内网 IP → 直连
-
外网 IP → 走转发通道
好处:
-
客户端 NO_PROXY 只写
localhost,127.0.0.1即可 -
不再受 Node.js 不支持 CIDR 的限制
-
即使转发服务重启,内网流量仍能被正确路由
四、最终配置清单
服务器端:转发服务配置(rules 顶部添加)
yaml
rules:
- 'IP-CIDR,192.168.0.0/16,DIRECT,no-resolve'
- 'IP-CIDR,10.0.0.0/8,DIRECT,no-resolve'
- 'IP-CIDR,172.16.0.0/12,DIRECT,no-resolve'
客户端:环境变量
bash
export http_proxy=http://192.168.31.82:7890
export https_proxy=http://192.168.31.82:7890
export NO_PROXY="localhost,127.0.0.1"
systemd 用户会话(持久化)
bash
systemctl --user set-environment http_proxy=http://192.168.31.82:7890
systemctl --user set-environment https_proxy=http://192.168.31.82:7890
systemctl --user set-environment NO_PROXY="localhost,127.0.0.1"
五、故障快速恢复命令
bash
# 1. 重启转发服务
ssh 192.168.31.82 "systemctl restart forwarder"
# 2. 重启 cpa
ssh 192.168.31.82 "pkill -f cli-proxy-api; /usr/local/bin/cli-proxy-api -config /mnt/shared/CPA/config.yaml &"
# 3. 更新 systemd 环境变量
systemctl --user set-environment NO_PROXY="localhost,127.0.0.1"
# 4. 清空当前 shell 的旧变量
unset http_proxy https_proxy NO_PROXY
六、经验教训
| 序号 | 教训 |
|---|---|
| 1 | curl 通 ≠ Node.js 通。不同 HTTP 客户端对 NO_PROXY 的解析规则不一致,尤其是 CIDR 格式的支持 |
| 2 | systemd 用户会话会固化环境变量 。修改 ~/.bashrc 不够,需用 systemctl --user set-environment 更新 |
| 3 | 502 不一定是上游问题。可能是转发服务本身挂了,也可能是流量被错误地路由到了不可达的转发服务 |
| 4 | 让转发层做路由决策,而不是在客户端配置复杂的 NO_PROXY 规则,可从根本上避免跨语言实现差异带来的问题 |