Skip to content

Commit 7885d37

Browse files
docs: add ops runbook (deployment / nginx / anti-detection)
1 parent 67ffc01 commit 7885d37

3 files changed

Lines changed: 364 additions & 0 deletions

File tree

ops/ANTI_DETECTION.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# 反检测升级路线
2+
3+
## 1. 核心哲学
4+
5+
**隐入人群,而不是扮演明星。**
6+
7+
- 让检测成本 > 收益
8+
- 多层一致性 > 单层完美
9+
- 评估升级时必问三件事:当前异常频率 / 升级后可维持多久 / 成本回收周期
10+
11+
## 2. 当前策略(已上线 / 正在做)
12+
13+
|| 实现 | 版本 |
14+
|---|---|---|
15+
| 应用层 | 113 UA 画像 + FNV hash | v1.7.40-ua-proxy |
16+
| 传输层 | 双轨 TLS(`CODEX_TRANSPORT_MODE=standard`/`utls_chrome`| 合并 upstream 6204889 |
17+
| 会话层 | session_id 按 API Key 哈希隔离 | - |
18+
| 网络层 | 代理 IP 池 | - |
19+
20+
**双轨模式含义**
21+
- `standard`(默认):Go 原生 TLS,**混入 openai-go SDK 人群**
22+
- `utls_chrome`:uTLS 仿 Chrome,作为备用
23+
24+
## 3. 备选方案:自定义 rustls 画像(高成本高价值)
25+
26+
**触发条件**(须全部满足才值得做):
27+
28+
- 观察到 OpenAI 部署细粒度 JA4 过滤
29+
- `standard``utls_chrome` 两个模式都开始封号
30+
- 业务量大到自定义画像维护成本 < 封号损失
31+
32+
### 3.1 技术实现路径
33+
34+
#### Step 1 · 抓真实 `codex_cli_rs` 的 ClientHello
35+
- 环境:mac 或 linux 装真 codex CLI
36+
- 工具:`tcpdump -i any -w codex.pcap host api.openai.com`
37+
- 或 Wireshark 带 TLS keylog
38+
39+
#### Step 2 · 解析 ClientHello 字段
40+
- Cipher suites 列表和顺序
41+
- Extensions 列表和顺序(含 GREASE 位置)
42+
- Supported groups(X25519 + 传统曲线)
43+
- Signature algorithms
44+
- ALPN(h2, http/1.1)
45+
46+
#### Step 3 · 用 uTLS 手写画像
47+
- `utls.ClientHelloSpec{...}` 自定义 spec
48+
- 参考 `utls/u_parrots.go` 里 Chrome/Firefox 写法
49+
- 注册为 `HelloCustom_Rustls_v0_23`(带 rustls 版本号)
50+
- 落到 `proxy/utls_rustls.go`,作为 `CODEX_TRANSPORT_MODE=rustls` 实现
51+
52+
### 3.2 工程成本
53+
54+
- **初始开发**:2-3 天
55+
- **持续维护**:rustls 新版本发布后验证(~月频)
56+
- **风险**:rustls ClientHello 细节变动需及时跟进,否则反而暴露
57+
58+
### 3.3 关键参考
59+
60+
- [`refraction-networking/utls`](https://github.com/refraction-networking/utls)`u_parrots.go`
61+
- `codex_cli_rs` 源码:依赖 `reqwest`,reqwest 依赖 `rustls`
62+
- rustls 版本:`cargo tree` 看 codex 的 `Cargo.lock`
63+
64+
## 4. 多层对比速查
65+
66+
|| 修改前(v1.7.41)| 当前(双层保险)| rustls 升级(备选)|
67+
|---|---|---|---|
68+
| TLS 指纹 | uTLS Chrome | Go 原生 | rustls 仿真 |
69+
| UA | 113 画像 | 113 画像 | 113 画像 |
70+
| session | 透传 | API Key 哈希 | API Key 哈希 |
71+
| debug 日志 ||||

ops/DEPLOYMENT.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# codex2api 线上部署手册(Node2 · cx.wyzai.top)
2+
3+
> 真值以 `docker ps` 为准,不信文档。文档滞后时以线上状态为准并反向更新本文。
4+
5+
## 1. 线上状态(截至 2026-05-10)
6+
7+
|||
8+
|---|---|
9+
| 镜像 | `codex2api:v1.7.45-sse-keepalive``latest`|
10+
| 容器 | `codex2api` |
11+
| 端口 | `8122`(nginx upstream 指向 127.0.0.1:8122)|
12+
| 部署目录 | `/data/codex2api/` |
13+
| Admin | https://cx.wyzai.top/admin/ secret = `65187777` |
14+
| 数据库 | PG `codex2api-postgres`, Redis `codex2api-redis`, 网络 `codex2api_codex2api-net` |
15+
| 图像卷 | `/data/codex2api-images:/app/images`(独立持久卷,蓝绿不会重建)|
16+
| nginx conf | `/www/server/panel/vhost/nginx/cx.wyzai.top.conf` |
17+
| nginx body 上限 | `client_max_body_size 500m` |
18+
| 本地 git HEAD |`git log -1` |
19+
20+
## 2. SSH 接入
21+
22+
```bash
23+
# Node2(cx.wyzai.top 落点)
24+
sshpass -p 'f3t7uCBeTCizT12' ssh -p 22222 -o StrictHostKeyChecking=no root@152.53.240.159
25+
26+
# 跳板(shell.wyzai.top 的 CF DNS 轮询:两条 A 记录)
27+
sshpass -p 'vCbeY8FXcSGw' ssh -p 18604 root@156.238.226.23
28+
sshpass -p '2R18UapfDNoT' ssh -p 24598 root@156.238.226.55
29+
```
30+
31+
## 3. 蓝绿 SOP(端口轮换)
32+
33+
端口:`8120 → 8121 → 8122 → 8123 → 8120 …`
34+
35+
**🚨 不要把多步串成一行 bash——stdout buffer 会让你误以为卡死。每步独立跑。**
36+
37+
```bash
38+
SSH='sshpass -p f3t7uCBeTCizT12 ssh -p 22222 -o StrictHostKeyChecking=no root@152.53.240.159'
39+
SCP='sshpass -p f3t7uCBeTCizT12 scp -P 22222 -o StrictHostKeyChecking=no'
40+
OLD=8122; NEW=8123; TAG=v1.7.46-xxx
41+
```
42+
43+
### Step 1 · 本地 tar 打包(5-10s)
44+
45+
```bash
46+
rm -f /tmp/codex2api-src.tar.gz
47+
time tar --exclude='.git' --exclude='node_modules' --exclude='frontend/dist' \
48+
--exclude='data' --exclude='codex2api.db*' --exclude='logs' --exclude='*.log' \
49+
--exclude='.DS_Store' \
50+
-czf /tmp/codex2api-src.tar.gz -C /Users/yinghua/Documents/fly/codex2api .
51+
```
52+
53+
### Step 2 · scp 上传(30s 内)
54+
55+
```bash
56+
time $SCP /tmp/codex2api-src.tar.gz root@152.53.240.159:/tmp/
57+
```
58+
59+
### Step 3 · 解压 + 继承旧 .env
60+
61+
```bash
62+
$SSH "rm -rf /data/codex2api-new && mkdir -p /data/codex2api-new && \
63+
tar -xzf /tmp/codex2api-src.tar.gz -C /data/codex2api-new 2>&1 | grep -v 'Ignoring unknown extended header' | head -3 && \
64+
cp /data/codex2api/.env /data/codex2api-new/.env && echo OK"
65+
```
66+
67+
### Step 4 · docker build(独立跑!增量 20s,全量 2 min)
68+
69+
```bash
70+
$SSH "cd /data/codex2api-new && time docker build --build-arg BUILD_VERSION=$TAG -t codex2api:$TAG . 2>&1 | tail -10"
71+
```
72+
73+
### Step 5 · 启 codex2api-new
74+
75+
```bash
76+
$SSH "docker rm -f codex2api-new 2>/dev/null || true; \
77+
docker run -d --name codex2api-new \
78+
--network codex2api_codex2api-net \
79+
-p 127.0.0.1:$NEW:$NEW \
80+
--env-file /data/codex2api-new/.env \
81+
-e CODEX_PORT=$NEW \
82+
-v /data/codex2api-new/logs:/app/logs \
83+
-v /data/codex2api-images:/app/images \
84+
--restart unless-stopped \
85+
codex2api:$TAG && \
86+
sleep 8 && curl -s http://127.0.0.1:$NEW/health && \
87+
docker exec codex2api-new strings /usr/local/bin/codex2api | grep -oE 'v1\\.7\\.[0-9]+[a-z0-9-]*' | head -2"
88+
```
89+
90+
### Step 6 · nginx 切流量
91+
92+
```bash
93+
$SSH "cp /www/server/panel/vhost/nginx/cx.wyzai.top.conf{,.bak-\$(date +%Y%m%d-%H%M%S)} && \
94+
sed -i 's|proxy_pass http://127.0.0.1:$OLD;|proxy_pass http://127.0.0.1:$NEW;|g' \
95+
/www/server/panel/vhost/nginx/cx.wyzai.top.conf && \
96+
nginx -t && nginx -s reload"
97+
curl -s https://cx.wyzai.top/health
98+
```
99+
100+
### Step 7-8 · 老容器 graceful 退出 + 固化
101+
102+
`docker stop -t 120` 必须:默认 10s 不够 `main.go` 的 90s shutdownTimeout。
103+
104+
```bash
105+
$SSH "sleep 30 && time docker stop -t 120 codex2api && \
106+
docker logs codex2api --tail 30 2>&1 | grep -E '收到关闭信号|HTTP 存量请求|已关闭' ; \
107+
docker rm codex2api && docker rename codex2api-new codex2api && \
108+
docker tag codex2api:$TAG codex2api:latest && \
109+
mv /data/codex2api /data/codex2api-old-\$(date +%Y%m%d-%H%M%S) && \
110+
mv /data/codex2api-new /data/codex2api && \
111+
sed -i 's/^CODEX_PORT=.*/CODEX_PORT=$NEW/' /data/codex2api/.env && \
112+
docker ps --filter name=codex2api --format '{{.Names}} {{.Image}} {{.Status}}'"
113+
```
114+
115+
## 4. 关键提醒
116+
117+
- **每次必传 `--build-arg BUILD_VERSION`**:否则前端徽章显示 `dev`
118+
- **`docker stop -t 120` 必须**:默认 10s 会强 kill 长 SSE 连接
119+
- **图像卷必须 `/data/codex2api-images`**:避免蓝绿 mv 时连带迁走
120+
- **每次必跑 `docker tag latest`**
121+
- **取版本登 `docker ps`,不信 doc**
122+
123+
## 5. 应急回滚
124+
125+
```bash
126+
$SSH "docker run -d --name codex2api-rollback \
127+
--network codex2api_codex2api-net \
128+
-p 127.0.0.1:<OLD_PORT>:<OLD_PORT> \
129+
--env-file /data/codex2api-old-YYYYMMDD-HHMMSS/.env \
130+
-v /data/codex2api-images:/app/images \
131+
-v /data/codex2api-old-YYYYMMDD-HHMMSS/logs:/app/logs \
132+
--restart unless-stopped \
133+
codex2api:<OLD_TAG>"
134+
$SSH "sed -i 's|:<NEW_PORT>|:<OLD_PORT>|' /www/server/panel/vhost/nginx/cx.wyzai.top.conf && nginx -s reload"
135+
```
136+
137+
## 6. 常用运维
138+
139+
```bash
140+
# 健康
141+
curl -s http://127.0.0.1:8122/health
142+
docker logs codex2api --tail 200 2>&1 | grep -vE '历史数据修复'
143+
144+
# plan_type 实时同步日志(v1.7.29+)
145+
docker logs codex2api -f 2>&1 | grep '同步上游 plan_type'
146+
147+
# AT-only active 账号 + 最早到期
148+
docker exec codex2api-postgres psql -U codex2api -d codex2api -c \
149+
"SELECT COUNT(*), MIN(NULLIF(credentials->>'expires_at','')::timestamptz) earliest \
150+
FROM accounts WHERE COALESCE(credentials->>'refresh_token','')='' AND status='active'"
151+
152+
# plan 分布
153+
docker exec codex2api-postgres psql -U codex2api -d codex2api -c \
154+
"SELECT COALESCE(NULLIF(credentials->>'plan_type',''),'(empty)') plan, \
155+
COUNT(*) total, COUNT(*) FILTER (WHERE status='active') active \
156+
FROM accounts GROUP BY 1 ORDER BY total DESC"
157+
```
158+
159+
## 7. 版本史
160+
161+
| 版本 | 端口 | 日期 | 关键改动 |
162+
|---|---|---|---|
163+
| v1.7.39-batch-add-3000 | 8120 | - | 批量添加 3000 账号 |
164+
| v1.7.40-ua-proxy | 8121 | - | UA 池 20→93 + 前端代理归一化 |
165+
| v1.7.41-dedupe | 8122 | - | 邮箱去重 |
166+
| v1.7.42-dual-fp | 8123 | - | 双轨 TLS + UA 双层保险 |
167+
| v1.7.43-free-55 | 8120 | - | free 账号承接 gpt-5.5 运行时开关 |
168+
| v1.7.44-unlock-banned | 8121 | - | 封禁 plus 账号自动解锁可清理 |
169+
| v1.7.45-sse-keepalive | 8122 | 2026-05-04 | SSE/JSON 双路径 keepalive 防 CF 100s idle |
170+
| **v1.7.46-upstream-may10** | **8123** | **2026-05-10** | **port 4 上游 commit:流式 usage 追踪 / 5h 紧迫性 / 工具参数剥离 / validation 扩充** |
171+
172+
## 8. 不要再做的事
173+
174+
-**基于 cooldown 时长推断 plan**:v1.7.29 之前误伤过 plus 被 7d ban 的合法账号
175+
-**id_token 解析回滚 plan_type**:用户拒绝,已废弃
176+
-**多步骤 bash 命令串成一行**:stdout buffer 误判卡死
177+
-**全局改 `/` 的 buffering**:会关掉 new-api UI 静态资源缓冲
178+
-**删除跳板 `sub_filter`**:前端暗色主题依赖它
179+
-**plan_type 校正的正道**:让 OpenAI 的 429 `error.plan_type` 自动同步(v1.7.29 已实现)
180+
181+
## 9. 常量速查
182+
183+
| 常量 || 位置 |
184+
|---|---|---|
185+
| shutdownTimeout | 90s | `main.go` |
186+
| freeUsageRateLimitThresholdPct | 90.0 | `auth/store.go` |
187+
| team score_bias | +100 | v1.7.28+ |
188+
| plus/pro score_bias | +50 | - |
189+
| AT-only 过期窗口 | 5 min | - |
190+
| MaxRequestBodySize 默认 | 32 MB | `security/validator.go` |
191+
| CF idle timeout | 100s | Cloudflare 硬限 |
192+
193+
## 10. 本次(v1.7.46)新增环境变量
194+
195+
```bash
196+
# /data/codex2api/.env 追加
197+
CODEX_MAX_REQUEST_BODY_SIZE_MB=64 # 默认 32MB,调到 64MB 容纳长上下文 / 图片
198+
```

ops/NGINX.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# nginx 配置要点
2+
3+
## 1. 真实链路
4+
5+
```
6+
客户端 → CF(橙云, 100s idle)
7+
→ 跳板(.23 / .55 CF DNS 轮询)
8+
→ new-api-horizon:30002
9+
→ cx.wyzai.top
10+
→ codex2api:8122
11+
→ OpenAI
12+
```
13+
14+
shell.wyzai.top 的 CF DNS 为两条 A 记录负载均衡:
15+
- 156.238.226.23(SSH 18604)
16+
- 156.238.226.55(SSH 24598)
17+
18+
## 2. 跳板 nginx · 关 SSE buffer
19+
20+
**文件**`/www/server/panel/vhost/nginx/proxy/shell.wyzai.top/api-sse.conf`
21+
22+
**根因**:宝塔默认 `proxy_buffering on + 16KB buffer + 300s timeout`。对 reasoning=xhigh 这种常 >60s 的长 SSE:
23+
- 单个 event 被积攒到 16KB 才下发
24+
- 300s 超时 nginx 断连
25+
- buffer 里半个 `data: ...` 送给客户端 → Rust reqwest 报 `error decoding response body`
26+
27+
**修复**:给 `/v1/``/hf/v1/``/pg/` 加独立 location(原 `/` 保持,内置 `sub_filter` 暗色主题注入仅对 HTML 生效,nginx 按最长前缀匹配):
28+
29+
```nginx
30+
location ^~ /v1/ {
31+
proxy_pass http://152.53.240.159:30002;
32+
client_max_body_size 100m;
33+
proxy_set_header Host $host;
34+
proxy_set_header X-Real-IP $remote_addr;
35+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
36+
proxy_http_version 1.1;
37+
38+
proxy_buffering off;
39+
proxy_cache off;
40+
proxy_request_buffering off;
41+
42+
proxy_connect_timeout 60s;
43+
proxy_read_timeout 86400s;
44+
proxy_send_timeout 86400s;
45+
send_timeout 86400s;
46+
47+
proxy_set_header Accept-Encoding "identity";
48+
}
49+
# /hf/v1/ 和 /pg/ 同构
50+
```
51+
52+
**两台跳板都要改**:CF 轮询会漏流量到另一台。
53+
54+
## 3. Node2 · cx.wyzai.top
55+
56+
**文件**`/www/server/panel/vhost/nginx/cx.wyzai.top.conf`
57+
58+
**client_max_body_size 500m**(2026-04-27 修,之前继承全局 50m 经常 413)
59+
60+
**关键配置(在 `proxy_pass http://127.0.0.1:8122;` 之后)**
61+
62+
```nginx
63+
proxy_buffering off;
64+
proxy_cache off;
65+
proxy_request_buffering off;
66+
proxy_read_timeout 86400s;
67+
proxy_send_timeout 86400s;
68+
send_timeout 86400s;
69+
```
70+
71+
## 4. 关键细节
72+
73+
- **`sub_filter` 默认只处理 `text/html`**,对 `text/event-stream` 不介入,**本身不是罪魁**
74+
- 罪魁是 `proxy_buffering on + 16KB + 300s timeout` 的组合
75+
- `proxy_buffering off``sub_filter` 仍能工作(output filter 独立于 proxy 缓冲)
76+
- `Accept-Encoding: identity` 防止上游返回 gzip/br 被重压缩破坏 SSE 帧边界
77+
- nginx 前缀 `location ^~ /v1/``^~ /` 长,优先匹配;**无需删除原 `/`**
78+
- `codex2api` 代码层已发 `X-Accel-Buffering: no``proxy/handler.go`),无需改
79+
80+
## 5. 如果仍复现 decode error
81+
82+
按优先级排查:
83+
1. **Cloudflare 100s idle 超时**(橙云硬限):需要 CF DNS 灰云,或靠 `codex2api` 的 SSE/JSON keepalive ticker(v1.7.45+ 已发 10s 间隔空 chunk)
84+
2. **new-api-horizon 渠道 timeout**:K8s Pod 默认渠道超时,管理后台把 cx.wyzai.top 渠道 timeout 调到 300s+
85+
3. **代码层**:v1.7.45 已发 `X-Accel-Buffering: no` 和 keepalive,不必再改
86+
87+
## 6. 验证 command
88+
89+
```bash
90+
# 跳板
91+
curl https://shell.wyzai.top/v1/models # 401(缺 key 正常),首字节 0.21-0.30s
92+
93+
# Node2
94+
curl -s https://cx.wyzai.top/health
95+
```

0 commit comments

Comments
 (0)