Files
chorus/docs/06-troubleshooting.md
T

153 lines
9.1 KiB
Markdown
Raw Blame History

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.
<!-- gitea-wiki-mirror:start -->
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Troubleshooting
wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Troubleshooting
wiki_revision: 9803ea981876455054c5598a38b21fd030211d4c
synchronized_at: 2026-08-26T01:08:16Z
<!-- gitea-wiki-mirror:end -->
# 故障排查
## 排查顺序
### 1. 环境与工作区
```powershell
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. 查库但不手工改状态
```sql
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 都属于安全缺陷,应立即停止相关入口并建立缺陷工单,不直接清理审计数据。