故障排查
排查顺序
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 与网络
依次检查:
- base_url scheme/host/port,并确认非标准公开端口已显式加入
CHORUS_PROVIDER_ALLOWED_PORTS;未设置时只允许 80/443; - DNS 的每个 IPv4/IPv6 结果;
- DialContext 是否拒绝私网、回环、链路本地、组播和未指定地址;
- 每次 redirect 是否重新校验并限制次数;
- 环境代理是否绕开安全 Dial;
- 上游返回的结果 URL 是否复用相同客户端。
若 attempt latency 精确接近 Provider HTTP timeout,说明请求被本地切断,不能据此判断上游无响应。provider.http_timeout_seconds 作用于 http.Client.Timeout,是从发起请求到读完 body 的绝对总时长;上游可能先长时间不返回任何字节(生成阶段),再以低速流式传输大响应体,两段都计入其中。按以下顺序确认:
portal/config/settings.yml的provider.http_timeout_seconds,以及worker.lease_seconds是否比它大 5 秒以上,否则 Portal 拒绝启动;- 管理端的 ProviderModel timeout 不能突破更短的全局 HTTP timeout;
- 仍无法判断时,经授权对上游发一次不设客户端上限的受控请求,记录 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
- 确认数据库已执行到最新 migration,
sys_casbin_rule中存在chorus_operator对应/api/v1/chorus/*权限。 - 当前实现只读取
sys_casbin_rule并关闭 Casbin adapter AutoMigrate;若旧进程曾生成空的casbin_rule,它不是权限事实来源,不要向其中补数据。 - 确认 bootstrap 账号绑定启用的
chorus_operator,重新登录取得新 JWT,再检查/api/v1/menurole。 settings.yml出现Unknown database时先创建空库并执行版本化 migration;出现Access denied时修正本地凭据,不运行 AutoMigrate 绕过。
API Key 审计排查(#45)
- 先使用服务端
request_id、API Key 数据库编号或 generation 编号查询api_audit_events,不要用完整 Key、Prompt、文件名或文件内容搜索日志和数据库。 - OpenAPI 提交没有事件时,先判断请求是否通过 API Key 认证。缺失、未知、过期、已撤销或停用用户的 Key 按设计不写数据库审计;这不是审计丢失。
- 已认证提交或限流拒绝没有事件时,检查
api_audit_events写权限、外键目标、JSON CHECK 和应用日志中的api audit write failed request_id=...。不要临时关闭审计或把请求正文写入日志。 - 管理员撤销应同时出现
admin_audit_events.action=api_key.revoke和api_audit_events.action=api_key.admin_revoked。只出现一类表示事务没有按设计提交,应停止重复操作并检查数据库错误。 last_used_at在一分钟内不变化是节流行为,不代表认证未发生。判断调用结果使用请求状态和对应提交审计。- go-admin 鉴权失败可能使用 HTTP 200 包装 JSON
code=401|403;排查权限时同时检查 JSON 业务码、JWT 和 Casbin,不把 HTTP 200 误判为已授权。 - 任何 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)
- 确认数据库已到 migration 10 且 dirty=0,四个生成记录 GET API 已登记并授予当前角色。
- 确认
settings.extend.chorus.storage_root指向 Portal 实际使用的生成物目录;相对路径按 Admin API 工作目录解析。 - 如果 Admin API 启动时报存储目录不可用,先检查目录存在和进程只读权限,不要让 Admin 自动创建目录。
- 如果详情能打开但单张图片失败,检查该 input/output 是否属于 URL 中的 generation,以及对象 metadata 的 owner、generation、MIME 是否与数据库一致。接口会返回稳定的资源不存在,不会回显磁盘路径。
- 浏览器网络面板中媒体请求应携带 Authorization header,响应应包含
Cache-Control: no-store和X-Content-Type-Options: nosniff;媒体 URL 不应包含 Token。