Clone
10
Troubleshooting
ila edited this page 2026-08-27 09:56:33 +08:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

故障排查

排查顺序

1. 环境与工作区

git status --short --branch
go version
mysql --version
migrate -version
node --version
pnpm --version

产品构建目标为 Go 1.26.5、MySQL 8.0、Node >=22、pnpm 9.15.1。工具缺失或版本不符先记录环境问题;不要通过降低文档版本或改代码绕过。

2. 判断同步还是异步

现象 首查
提交 4xx、没有卡片 portal 认证/CSRF/上传/幂等
卡片出现但一直 queued worker、MySQL 版本、取任务索引
running 超时 lease owner/token/until、上游和存储超时
succeeded 但无内容 output 记录、原子文件、鉴权/缩略图
内容不符 rendered_prompt 与模板
管理端 CRUD 不符 migrations、导表元数据和生成差异

3. 查库但不手工改状态

SELECT id, user_id, kind, status, provider_model_id, attempt_count,
       error_code, lease_owner, lease_token, lease_until,
       latency_ms, created_at, finished_at
FROM generations
ORDER BY id DESC
LIMIT 10;
  • pending 不动:worker 未启动、数据库不支持 SKIP LOCKED 或索引/事务错误。
  • running 且租约过期:新 worker 应直接原子替换 token,不应先改回 pending。
  • 同一任务出现互相覆盖:检查最终 UPDATE 是否匹配当前 lease_token;陈旧 worker 应影响 0 行。
  • 400/401/策略拒绝却有多 Provider attempts:retryable 写反。
  • 429/5xx/超时/连接错误没有故障转移(MVP-1):候选池、上限或熔断状态错误。
  • failed 没有 error,或 succeeded 没有 rendered_prompt/attempts:违反持久化红线。

终态 error_code 的含义必须区分清楚,不要凭它跳过 attempts:

终态码 含义 该看什么
route_unavailable 一次上游都没调过——成员被禁用、熔断打开或能力不匹配,选不出候选 路由池成员、Provider/Model 启停、熔断状态
failover_exhausted 调过上游且失败可重试,但已经没有候选可换 attempts 中每次尝试自身的 error_code,那才是真实原因
upstream_* 不可重试失败,直接终止 该次尝试的错误码与 attempts 长度是否为 1

attempts 中每条尝试记录的 error_code 是该次调用自身的结果,不会被终态码覆盖。若看到某条尝试同时出现 error_code=route_unavailable 与 retryable=true、且有实际 latency_ms,说明运行的是修复前的版本(见 #63),该记录不可信。

lease_token 属运行控制信息,不在对外响应或普通日志完整输出。排查记录使用任务 id 和脱敏摘要。

4. 用 mock 隔离上游

用 mock 构造成功、429、5xx、超时、连接错误、400、401、内容拒绝和恶意结果 URL。mock 正常而真实异常,再查 ProviderModel、网络和上游;禁止用真实额度循环试错。真实连通性只通过受审计且有冷却的按钮触发。

5. SSRF 与网络

依次检查:

  1. base_url scheme/host/port,并确认非标准公开端口已显式加入 CHORUS_PROVIDER_ALLOWED_PORTS;未设置时只允许 80/443;
  2. DNS 的每个 IPv4/IPv6 结果;
  3. DialContext 是否拒绝私网、回环、链路本地、组播和未指定地址;
  4. 每次 redirect 是否重新校验并限制次数;
  5. 环境代理是否绕开安全 Dial;
  6. 上游返回的结果 URL 是否复用相同客户端。

若 attempt latency 精确接近 Provider HTTP timeout,说明请求被本地切断,不能据此判断上游无响应。provider.http_timeout_seconds 作用于 http.Client.Timeout,是从发起请求到读完 body 的绝对总时长;上游可能先长时间不返回任何字节(生成阶段),再以低速流式传输大响应体,两段都计入其中。按以下顺序确认:

  1. portal/config/settings.yml 的 provider.http_timeout_seconds,以及 worker.lease_seconds 是否比它大 5 秒以上,否则 Portal 拒绝启动;
  2. 管理端的 ProviderModel timeout 不能突破更短的全局 HTTP timeout;
  3. 仍无法判断时,经授权对上游发一次不设客户端上限的受控请求,记录 ttfb、总时长和响应体大小,据此定值;不要靠反复上调超时试错。

修改任何一层后必须重启 Portal。

参考基线(2026-08-25 实测,自定义 Provider 的 gpt-image-2 文生图):生成阶段约 160 秒无任何数据,随后以 17–25 KB/s 流式传输,单次总时长 385–465 秒,响应体 3.4–4.9 MB。同类任务报超时前,先用 attempts 中的 latency_ms 与该基线比较。

拦截私网是正确行为,不能为联调关闭。需要访问内部 mock 时把 mock 与测试进程放在专用测试网络,并使用测试专用注入点,不在生产配置放行私网。

6. 文件和权限

  • 检查临时文件与最终目录是否同文件系统,rename 是否原子;
  • 检查失败/陈旧 worker 是否只清理自己创建的临时文件;
  • 检查数据库 output 与实际文件一致;
  • 403 多为用户归属校验,404 多为记录/文件不一致;
  • 不得临时把存储目录改成无鉴权静态目录。

7. go-admin 与代码生成

现象 处理
生成器看不到表 先确认隔离库已执行 migrations,再导入元数据
“迁移脚本”没有建表 它主要生成菜单/API 配置,不是业务 DDL
生成后菜单立刻变化 可能调用了副作用 /gen/todb;停止并恢复隔离库
生成文件落错目录 检查固定提交的 generator 配置,人工审查差异
生产能访问 dev-tools 高风险发布阻断;下线路由而非只隐藏菜单
启动时表结构变化 检查是否误接 AutoMigrate,立即停止部署

8. 文档与 Harness

症状 处理
strict 缺章节 恢复精确标题
sync --check 不一致 先判断 Wiki 是否更新;不要直接改镜像
sync 因本地镜像改动停止 保留用户改动,查清来源后处理
Wiki 无 revision 停止同步,从失败页面继续
API 401 从安全环境重新加载有效 PAT,不打印令牌
MCP 连接失败 在工单记录原因后回退 API

9. 仍未定位

在工单记录最小复现、任务 id、脱敏 attempts/error、租约时间线、相关日志和已排除项。不得粘贴 API Key、Cookie、真实 prompt、用户文件或生产数据。

必须停止的情况

  • 需要修改 retryable、SSRF、密钥、租约、迁移、身份或权限;
  • 需要手工改共享/生产库、回退状态、删除数据或文件;
  • 想通过 AutoMigrate、关闭 SSRF、公开存储或暴露 dev-tools 临时绕过;
  • 需要反复真实上游调用;
  • 日志疑似泄密或包含个人/生产数据;
  • 同一阻塞尝试两次仍不能确认根因。

管理端登录后无菜单或 Chorus API 返回 403

  1. 确认数据库已执行到最新 migration,sys_casbin_rule 中存在 chorus_operator 对应 /api/v1/chorus/* 权限。
  2. 当前实现只读取 sys_casbin_rule 并关闭 Casbin adapter AutoMigrate;若旧进程曾生成空的 casbin_rule,它不是权限事实来源,不要向其中补数据。
  3. 确认 bootstrap 账号绑定启用的 chorus_operator,重新登录取得新 JWT,再检查 /api/v1/menurole。
  4. settings.yml 出现 Unknown database 时先创建空库并执行版本化 migration;出现 Access denied 时修正本地凭据,不运行 AutoMigrate 绕过。

API Key 审计排查(#45)

  1. 先使用服务端 request_id、API Key 数据库编号或 generation 编号查询 api_audit_events,不要用完整 Key、Prompt、文件名或文件内容搜索日志和数据库。
  2. OpenAPI 提交没有事件时,先判断请求是否通过 API Key 认证。缺失、未知、过期、已撤销或停用用户的 Key 按设计不写数据库审计;这不是审计丢失。
  3. 已认证提交或限流拒绝没有事件时,检查 api_audit_events 写权限、外键目标、JSON CHECK 和应用日志中的 api audit write failed request_id=...。不要临时关闭审计或把请求正文写入日志。
  4. 管理员撤销应同时出现 admin_audit_events.action=api_key.revoke 和 api_audit_events.action=api_key.admin_revoked。只出现一类表示事务没有按设计提交,应停止重复操作并检查数据库错误。
  5. last_used_at 在一分钟内不变化是节流行为,不代表认证未发生。判断调用结果使用请求状态和对应提交审计。
  6. go-admin 鉴权失败可能使用 HTTP 200 包装 JSON code=401|403;排查权限时同时检查 JSON 业务码、JWT 和 Casbin,不把 HTTP 200 误判为已授权。
  7. 任何 summary 出现 Prompt、完整 Key、public_id、secret_hash、Authorization、Cookie、响应正文或原始 IP 都属于安全缺陷,应立即停止相关入口并建立缺陷工单,不直接清理审计数据。

系统管理页面排错

  • 点击导航出现“Cannot find module”:先核对 migration 的 component 是否对应 admin-ui/src/views/<component>.vue,再运行 route-view 单测;不要拼接 @/views。
  • 系统页面返回 403:核对角色是否关联对应菜单、菜单是否关联 GET API、Casbin 是否存在同路径和方法。菜单、接口、登录日志不应出现 POST、PUT 或 DELETE 授权。
  • 停用或删除管理员返回 409:检查响应中的三个稳定保护标识;这是安全拒绝,不应通过改数据库或隐藏错误绕过。
  • Admin UI 默认双 bundle 构建出现 V8 OOM:先停止本项目开发服务、释放内存并设置 NODE_OPTIONS=--max-old-space-size=4096 后重跑。不得把只完成 legacy bundle 记录成完整构建通过。
  • Supervisor 被强制结束后无法重启:确认 PID 文件指向的进程确实不存在后,才能清理过期 PID 并重新启动;随后核对 Portal、Admin API、Admin UI 三个监听端口。

Admin 详情无法显示生成图片(#72)

  1. 确认数据库已到 migration 10 且 dirty=0,四个生成记录 GET API 已登记并授予当前角色。
  2. 确认 settings.extend.chorus.storage_root 指向 Portal 实际使用的生成物目录;相对路径按 Admin API 工作目录解析。
  3. 如果 Admin API 启动时报存储目录不可用,先检查目录存在和进程只读权限,不要让 Admin 自动创建目录。
  4. 如果详情能打开但单张图片失败,检查该 input/output 是否属于 URL 中的 generation,以及对象 metadata 的 owner、generation、MIME 是否与数据库一致。接口会返回稳定的资源不存在,不会回显磁盘路径。
  5. 浏览器网络面板中媒体请求应携带 Authorization header,响应应包含 Cache-Control: no-store 和 X-Content-Type-Options: nosniff;媒体 URL 不应包含 Token。