TROUBLESHOOTING.md 17 KB

zhuce6 Troubleshooting

先记住主线:

uv run python main.py init
uv run python main.py doctor --fix
uv run python main.py --mode lite
# 或
uv run python main.py --mode full

如果排障前还没跑 doctor --fix, 先跑它.

1. 基础检查

uv run python main.py status
uv run python main.py doctor --fix

启动后再看 API:

export ZHUCE6_BASE_URL="http://<dashboard-host>:<dashboard-port>"
curl -sS "$ZHUCE6_BASE_URL/api/runtime" | python3 -m json.tool | head -n 80
curl -sS "$ZHUCE6_BASE_URL/api/health/dependencies" | python3 -m json.tool | head -n 80

2. init 之后仍然跑不起来

优先检查:

  • .env 是否是本次向导写入的目标文件
  • doctor --fix 是否已执行
  • 当前启动命令是否显式带了 --mode lite--mode full
  • backend 是否与你填写的后端配置一致

3. cfmail 不可用

新配置路径

现在新配置支持两条 cfmail 初始化路径:

  • 从零部署 cfmail Worker: Cloudflare API Token + zone_name
  • 复用已部署 cfmail Worker: CF_AUTH_EMAIL + CF_AUTH_KEY + zone_name + worker_domain

如果向导已经跑过, 先检查这些结果是否存在:

  • config/cfmail_accounts.json
  • config/cfmail_provision.env
  • .env 中的:
    • ZHUCE6_CFMAIL_API_TOKEN
    • ZHUCE6_CFMAIL_CF_AUTH_EMAIL
    • ZHUCE6_CFMAIL_CF_AUTH_KEY
    • ZHUCE6_CFMAIL_CF_ACCOUNT_ID
    • ZHUCE6_CFMAIL_CF_ZONE_ID
    • ZHUCE6_CFMAIL_WORKER_NAME
    • ZHUCE6_CFMAIL_ZONE_NAME

已有配置路径

如果仓库内已有 cfmail 配置, init 默认会提示复用. 若你误选了重新生成, 应先对照现有 config/ 内容确认是否与 live 配置一致.

仍需手工排查时

uv run python scripts/setup_cfmail.py --help

注意:

  • scripts/setup_cfmail.py 的完整部署链依赖 wrangler.
  • wrangler 在非交互环境下要求 Cloudflare API Token.
  • 所以如果你只有 CF_AUTH_EMAIL + CF_AUTH_KEY, 不要指望它完成首次 Worker 部署, 应该在 init 时直接填写已部署的 worker_domain.
  • 只有需要从零部署 Worker 时, 才应走 uv run python scripts/setup_cfmail.py --api-token <token> --zone-name <zone> 这条路径.
  • 如果你填入的 email_domain 已经失效, init 在保存阶段会尝试自动轮换到新的可用子域名. 若仍失败, 再手工检查 DNS 与 Email Routing.

4. doctor --fix 之后还有依赖问题

doctor --fix 当前会自动处理:

  • uv sync
  • cfmail worker 目录下的 npm install --no-fund --no-audit

它不会替你全局安装:

  • git
  • node
  • npm
  • npx
  • sslocal

所以如果报告里仍有失败项, 直接按报告补齐系统依赖即可.

如果 d1_cleanup 一直跳过, 再检查:

  • ZHUCE6_D1_DATABASE_ID 是否为空
  • 该值是否由 init / cfmail 向导自动写入

5. 代理问题

直接代理 URL

优先检查:

  • ZHUCE6_REGISTER_PROXY
  • ZHUCE6_PROXY_POOL_DIRECT_URLS

Clash YAML

如果你选择 Clash YAML 模式, 还要确认:

command -v sslocal || command -v ss-local || true

如果没有 sslocal, 先安装它, 再重新执行:

uv run python main.py doctor --fix

6. full + cpa 不可用

重点检查:

  • ZHUCE6_BACKEND=cpa
  • ZHUCE6_CPA_MANAGEMENT_BASE_URL
  • ZHUCE6_CPA_MANAGEMENT_KEY

然后看:

curl -sS "$ZHUCE6_BASE_URL/api/health/dependencies" | python3 -m json.tool | head -n 80

7. full + sub2api 不可用

重点检查:

  • ZHUCE6_BACKEND=sub2api
  • ZHUCE6_SUB2API_BASE_URL
  • 认证是否完整:
    • ZHUCE6_SUB2API_API_KEY
    • ZHUCE6_SUB2API_ADMIN_EMAIL + ZHUCE6_SUB2API_ADMIN_PASSWORD

doctor/api/health/dependencies 都会明确显示 sub2api ready / unavailable.

8. Dashboard 正常, 但没有注册任务

先看 /api/runtime:

  • runtime_mode
  • registered_tasks
  • register_enabled

常见原因:

  • 当前模式是 dashboard
  • ZHUCE6_REGISTER_ENABLED=false
  • cfmail 运行时变量不完整
  • 代理未配置或不可达

9. Windows / Linux / WSL

Windows

  • 推荐 PowerShell 7.
  • 先保证 python, uv, node, npm, npx, git 可用.
  • 直接按 README 主线运行即可.

Linux

  • 按 README 主线运行.
  • 需要 Clash YAML 时, 记得额外准备 sslocal.

WSL

  • 与 Linux 路径一致.
  • 推荐依赖与仓库都放在 WSL 内执行.

如果你需要从别的前端域读取 /api/runtime/api/summary, 还要配置:

  • ZHUCE6_DASHBOARD_ALLOWED_ORIGINS=http://your-dashboard.example.com

10. cfmail OTP 收不到 / 全部 wait_otp 超时

现象

注册日志所有线程卡在 wait_otp, 180s 超时:

[zhuce6:register] [thread-1] ❌ failed [stage=wait_otp]: otp retrieval failed
  ↳ verification code timed out after 181.88s
  ↳ otp mailbox diagnostics: polls=45 scanned=0

根因

cfmail 子域名的 DNS 记录 (MX + SPF) 缺失. 没有 MX 记录, Cloudflare 无法接收邮件, OTP 永远到不了.

诊断

source config/cfmail_provision.env

# 检查子域名 DNS 记录数量 (应 >= 4: 3xMX + 1xTXT)
curl -s "https://api.cloudflare.com/client/v4/zones/$ZHUCE6_CFMAIL_CF_ZONE_ID/dns_records?name=<subdomain>.example.com" \
  -H "X-Auth-Email: $ZHUCE6_CFMAIL_CF_AUTH_EMAIL" \
  -H "X-Auth-Key: $ZHUCE6_CFMAIL_CF_AUTH_KEY" \
  | python3 -c "import sys,json; r=json.load(sys.stdin); print(f'records: {r[\"result_info\"][\"total_count\"]}')"

修复

source config/cfmail_provision.env
uv run python -c "
from core.cfmail_provisioner import CfmailProvisioner
p = CfmailProvisioner()
for acct in p._load_all_accounts():
    if not acct.get('enabled'): continue
    domain = acct['email_domain']
    label = acct['name'].replace('cfmail-', '')
    try:
        p._create_email_routing_rule(domain, label)
        p._create_dns_records(domain)
        p._update_worker_domains(domain)
        print(f'fixed {domain}')
    except Exception as e:
        print(f'error {domain}: {e}')
"

11. no proxy available in pool

现象

[zhuce6:register] [thread-13] ❌ exception: no proxy available in pool

根因

ZHUCE6_REGISTER_THREADS 大于 ZHUCE6_PROXY_POOL_SIZE. 多余线程拿不到代理.

修复

确保线程数 <= 代理池大小:

# .env
ZHUCE6_REGISTER_THREADS=12
ZHUCE6_PROXY_POOL_SIZE=12

12. sslocal 残留进程堆积

现象

13. fresh 号刚注册就很快 401 no_organization

先看 /api/account-survivalstate/responses_survival_tracker.json:

  • first_invalid_error_code
  • registration_proxy_region
  • registration_post_create_gate
  • first_use_proxy_key
  • first_use_proxy_region
  • fingerprint_consistent

当前优先排查顺序:

  1. fresh 注册是否落到 us
  2. 首用 probe 是否复用了注册代理
  3. 该号是否 registration_post_create_gate = add_phone

如果 fresh 号主要出现在 us, 先把:

ZHUCE6_REGISTER_FRESH_PROXY_REGIONS=tw,jp,hk,us

如果 live 日志里连续出现 registration_disallowed, 说明当前活动域已经被上游集中拒绝. 这时不要继续等满整个黑名单窗口, 应确认:

ZHUCE6_CFMAIL_REGISTRATION_DISALLOWED_THRESHOLD=2

这样同域连续 2 次 registration_disallowed 就会直接退域补位.

并重启 runtime.

14. survival 实验里混入很多旧号

如果 cohort 里大多数账号没有:

  • registration_fingerprint_profile
  • registration_proxy_region

说明 reseed 仍在吃旧历史号, 这会污染实验.

优先检查:

grep -n "ZHUCE6_RESPONSES_SURVIVAL" .env

建议:

ZHUCE6_RESPONSES_SURVIVAL_REQUIRE_PROVENANCE=true
ZHUCE6_RESPONSES_SURVIVAL_RECENT_WINDOW_SECONDS=1800

15. add_phone 成功号看起来能用, 但很快死

这类号现在不会走旧候选池, 但会在单池里带 warmup 风险字段:

  • warmup_required=true
  • warmup_state=pending|passed|failed

判断方法:

  • pending: 还在观察窗口内
  • passed: 已满足最小成功探测数或最小存活时间
  • failed: 首次 invalid 发生在 warmup 期间

如果这类号比例过高, 不要先怀疑 dashboard, 先看:

  • 注册代理区域
  • first_invalid_error_code
  • first_invalid_proxy_region

每次启动注册机生成 12 个 sslocal, 但停止时不清理. 长期积累后系统有上百个 sslocal 进程.

修复

重启前先清理:

pkill -f sslocal

13. create_account 已 200, 但 5 分钟 success 看起来是 0 / 全是 add_phone_gate

现象

  • logs/register.log 里能看到 create_account status: 200
  • 线程最终却频繁报:

    [zhuce6:register] [thread-1] ❌ failed [stage=add_phone_gate]: post-create flow requires phone gate
    
  • 同时日志里还能看到:

    📥 deferred token retry enqueued
    

根因

这通常不是 create_account 本身失败, 而是 add_phone 后的 token 恢复链被拖慢或被误判:

  • 账号创建已经成功, 但 direct session token / fresh login fallback 当次没有拿到 token.
  • 账号被放进 pending retry queue 后, 如果首次重试发生得太晚, 5 分钟窗口里就会看起来像 0%.

当前实现已经做了两层处理:

  1. 如果 create_account 已直接返回 https://chatgpt.com/api/auth/callback/openai?..., 先走 callback/openai -> /api/auth/session 直取 token.
  2. create_account 命中 add_phone 后, 再尝试 direct session token 提取.
  3. 如果当次线程仍未拿到 token, pending retry queue 会在短窗口内补取, 首轮基础延迟会钳到 60s.

先确认

优先同时看两类日志:

grep -n "add_phone_gate\\|deferred token retry enqueued\\|deferred token acquired\\|direct session token" logs/register.log | tail -n 80

如果你能看到:

  • post-create add_phone: attempting direct session token extraction
  • 📥 deferred token retry enqueued
  • [pending] ✅ deferred token acquired

说明问题在 token 恢复时序, 不是 create_account 没成功.

结论判断

  • 只有 add_phone_gate, 没有任何 [pending] 成功: 再检查代码是否已包含 direct session token 路径与 60s pending retry 限制.
  • [pending] ✅ deferred token acquired: 说明账号并非 0 成功, 只是不能只按线程即时结果统计.
  • 如果你在排查旧版本, 还要确认它是否在 create_account 后直接丢掉了 callback/session 结果, 又重新触发 fresh login, 这会显著提高 add_phone 命中率.

14. cfmail 全部报 account unavailable / mailbox 阶段持续失败

现象

注册日志持续出现:

[zhuce6:register] [thread-1] ❌ failed [stage=mailbox]: cfmail account unavailable, current accounts: 无

或者线程长时间停在 mailbox 失败, 没有新的 cfmail 子域名被切出.

根因

这是 cfmail 全域 cooldown 场景:

  • 当前 enabled 的 cfmail 账户全部进入 CfmailAccountManager cooldown.
  • select_account() 返回 None, register 在 mailbox 阶段直接失败.
  • 这类失败没有走到 OpenAI unsupported_email / registration_disallowed 信号时, 不会靠黑名单窗口自然触发 rotation.

当前实现已经在 register worker 顶部检测这个状态, 一旦发现所有 cfmail account 都不可选, 会主动调用 CfmailProvisioner.rotate_active_domain() 打破死锁.

先确认

logs/register.log 是否出现:

[cfmail] all accounts in cooldown, forcing domain rotation to break deadlock

如果有, 说明死锁检测已经触发, 接着只需要看 rotation 是否成功.

手工验证 rotation

set -a && source .env && set +a
PYTHONPATH=. uv run python -c "
from core.cfmail_provisioner import CfmailProvisioner
p = CfmailProvisioner()
result = p.rotate_active_domain()
print(f'success={result.success}, new={result.new_domain}, error={result.error}')
"

如果这里成功, 但 register 仍不恢复, 再检查:

  • config/cfmail_accounts.json 中是否已有新的 enabled 子域名
  • worker domain 是否仍可访问
  • 新子域名的 DNS / routing rule 是否已创建

15. rotate_active_domain() 因 DNS read-only / code 1043 报错

现象

手工调用或自动 rotation 时, 日志出现类似:

HTTP 400 {"errors":[{"code":1043,"message":"DNS record is read only"}]}

根因

Cloudflare 某些历史 DNS record 或 email routing rule 可能是只读或受保护资源. 这些资源常出现在旧 auto 域名被切换后的残留清理阶段.

当前实现里:

  • _delete_domain_artifacts() 对删除失败按 best-effort 处理
  • cleanup_stale_domains() 会跳过 read-only DNS / routing rule
  • rotate_active_domain() 在新域名已经完成 DNS + routing + worker binding + smoke test 后, 即使 cleanup 失败也不会回滚整个 rotation

所以 1043 更应被视为旧资源清理告警, 而不是新域名切换失败.

手工验证

如果怀疑 rotation 没真正切过去, 重点看最终结果而不是 cleanup 告警:

set -a && source .env && set +a
PYTHONPATH=. uv run python -c "
from core.cfmail_provisioner import CfmailProvisioner
p = CfmailProvisioner()
result = p.rotate_active_domain()
print(result)
"

只要输出里 success=True, 并且 new_domain 已变更, 就说明 rotation 主链成功.

16. CPA 里账号突然变少 / 重启后 inventory 丢失

现象

  • CPA auth-files 数量明显小于本地 pool/*.json
  • CPA 重启后 Dashboard 里的 cpa_count 突然下降
  • 注册虽然还在成功, 但旧账号像是消失了

正确理解

当前稳定结构不是“本地 pool 单池”, 而是:

  • pool/*.json: 持久化备份
  • CPA backend: 运行时主池

因此 CPA 丢库存时, 正确修复动作不是重新解释成双池晋升, 而是做 backup reconcile.

已有机制

  • register 启动时会先做一次 runtime reconcile
  • rotate 周期任务也会检测 drift
  • 若 backend 缺账号, 会从 pool/*.json 回灌到 CPA
  • 若 backend 有账号但本地没有备份, 会反向补写 pool

先确认

uv run python main.py status
curl -sS "$ZHUCE6_BASE_URL/api/summary" | python3 -m json.tool | head -n 80

重点看:

  • pool_count
  • cpa_count
  • register 概览里的 total_attempts / total_success
  • pool/cpa_runtime_reconcile_state.json

结论判断

  • pool_count 明显大于 cpa_count, 优先看 reconcile 是否正在补回.
  • total_attempts 持续增长但 total_success 不动, 且失败热点出现 cpa_sync_failed, 说明 register 新号写 backend 失败, 应先排查 CPA Management API 或网络错误.

17. validate 删除了 backend, 但本地 pool 还残留

当前实现中, validate, rotate, cleanup 都应双删 backend + pool.

如果你仍看到“backend 已删但 pool 还在”的残留, 先确认代码是否为最新版本, 再复查对应任务日志:

  • validate: deleted
  • rotate: deleted_401
  • cleanup: deleted

18. hard add_phone 为什么始终没有 workspace / org

现象

  • create_account 已 200, 但后续一直停在:

    https://auth.openai.com/add-phone
    
  • 日志里反复出现:

    workspace list missing in auth session payload
    solution D: no accessToken in session response, keys=['WARNING_BANNER']
    
  • fresh login fallback 做完后, 仍然没有:

    • callback url
    • session token
    • workspace select
    • organization select

当前已确认的根因

这类 hard add_phone 已被 live 证实为服务端会话状态问题, 不是本地漏流程:

  1. oai-client-auth-session 里没有 workspaces.
  2. https://auth.openai.com/api/accounts/client_auth_session_dump 已经可打通, 但返回仍然是:
    • status = 200
    • workspace_count = 0
  3. /api/auth/session 仍然只返回:
    • WARNING_BANNER
    • 没有 accessToken

这说明:

  • 当前 hard add_phone 的 full client auth session 本身不包含 workspaces / orgs.
  • 客户端因此没有可继续到 workspace / organization route 的材料.

先确认

优先看 live register 日志:

grep -n "client auth session dump\\|workspace list missing\\|WARNING_BANNER\\|add-phone trace saved" /home/sophomores/zhuce6/logs/main_full_4threads_*.log | tail -n 80

若能同时看到:

  • client auth session dump status: 200
  • client auth session dump workspace count: 0
  • workspace list missing in auth session payload
  • solution D: no accessToken in session response, keys=['WARNING_BANNER']

就说明当前不是本地漏掉 workspace route, 而是服务端会话本身没有给 workspace / org.

结论判断

  • 如果 client_auth_session_dump 已经是 200, 但 workspace_count 仍为 0:
    • 不要再把问题归因到 cookie 解析或 workspace select 漏调用.
  • 如果 add-phone trace 里:
    • auth_session_workspace_count = 0
    • auth_session_dump_workspace_count = 0
    • direct_session_keys = ["WARNING_BANNER"]
    • latest fresh login 仍停在 /add-phone 说明这就是 server-side gate.

当前可做与不可做

  • 可以做:
    • 继续保留 trace artifact 与吞吐优化.
    • 对 hopeless hard add_phone 提前 short-circuit 到 deferred retry.
  • 不要做:
    • 在没有新 live 证据前, 继续假设客户端还存在隐藏 continue path.
    • 把这个问题当成"再多做一次 fresh login 也许就能过".