- docs/02 架构设计: 新增 aichat审查/异步审批构想/流式渲染调研/generating状态机/密钥迁移健壮性/工作流脚本执行边界/条件表达式引擎/F-07 trait下沉/Agent架构说明/任务推进构想/功能创意池;更新功能决策记录+归档/对抗论证/文档记录规范/经验记录
- docs/03 模块文档: 新增 AI对话引擎/DAG引擎详解;更新 df-knowledge/df-nodes/df-storage/df-workflow/df-ai
- docs/05 代码审查: 新增 全栈审查/全局review/架构审查/近期改动审查/工作区多角度走查/自研memo流式渲染审查
- docs/09 问题排查: 新增 aichat-apikey-401
- docs/INDEX+README 索引同步;docs/todo 待办看板(2026-06-15 汇总)
- PROGRESS.md Sprint 22-25;URGENT.md 加急清单快照(5 项 P0 已全修)
- scripts/cleanup_orphan_tasks.{py,sh} 孤儿任务清理工具
- .gitignore 补 *.broken.bak + tmp/ 噪音排除
126 lines
5.5 KiB
Markdown
126 lines
5.5 KiB
Markdown
# aichat API Key 鉴权失败排查
|
||
|
||
> 2026-06-15 排查 | 看板索引:`docs/todo.md`「🔴 aichat API Key 401 排查」区块
|
||
|
||
## 现象
|
||
|
||
- aichat 对话失败,提示「调用失败: API Key 无效或无权限」
|
||
- 重新设置 apikey 后仍无效
|
||
|
||
## 错误来源
|
||
|
||
- `stream_recv.rs:107-113`:`provider.stream(request)` 直接返回 `Err` 分支
|
||
- 错误文本:`format!("AI 调用失败: {}", e)`,`e` 为服务端返回的 401/403
|
||
- 即:请求发出去了,被服务端以鉴权/权限为由拒绝。**非流式中断、非网络断连。**
|
||
|
||
## 排查结论:代码链路全对(排除代码 bug)
|
||
|
||
### 保存链路(正确)
|
||
|
||
- `commands.rs ai_save_provider:329-333`:`api_key` 非空 → `set_provider_secret` 写 OS keyring
|
||
- 写失败会返回「密钥保存到系统钥匙串失败」(用户未报此错 → **写入成功**)
|
||
- `:334`:DB `api_key` 列恒空(FR-S1 设计,真实密钥唯一源 = OS keyring)
|
||
|
||
### 读取链路(正确)
|
||
|
||
所有 `build_provider` 调用点均经 `resolve_provider_secret`(keyring 优先,fallback DB):
|
||
|
||
| 调用点 | 行号 | 状态 |
|
||
|--------|------|------|
|
||
| agentic.rs(对话主循环) | :50 | ✓ |
|
||
| project.rs(技术栈扫描) | :378 | ✓ |
|
||
| knowledge_inject.rs(知识提炼/嵌入) | :33, :323 | ✓ |
|
||
| title.rs(标题生成) | :63 | ✓ |
|
||
|
||
- 例外:`df-nodes/ai_node.rs:118` 用 `p.api_key`(工作流 AI 节点独立配置,不经 keyring,与本问题无关)
|
||
|
||
### 鉴权头(正确)
|
||
|
||
- `openai_compat`:`Authorization: Bearer {api_key}`
|
||
- `anthropic_compat`:`x-api-key: {api_key}` + `anthropic-version`
|
||
|
||
### URL 智能拼接(正确,三规则容错)
|
||
|
||
- `openai_compat.chat_url`(openai_compat.rs:253-262):
|
||
- 已含 `/chat/completions` → 直接用
|
||
- 以 `/v<数字>` 结尾(如 `/v1` `/v4`,GLM 的 `/api/paas/v4`)→ 补 `/chat/completions`
|
||
- 仅域名(`api.openai.com` / `api.deepseek.com`)→ 补 `/v1/chat/completions`
|
||
- `anthropic_compat.messages_url`(anthropic_compat.rs:255-264):
|
||
- 已含 `/v1/messages` → 直接用
|
||
- 以 `/v1` 结尾 → 补 `/messages`
|
||
- 否则(`.../api/anthropic`、`api.anthropic.com`)→ 补 `/v1/messages`
|
||
|
||
**链路已全部验证无误。401 来自服务端,根因不在 devflow 代码。**
|
||
|
||
## 根因方向(服务端 401,按概率排序)
|
||
|
||
### 1. API Key 本身无效(最常见)
|
||
|
||
- 过期 / 欠费 / 额度用尽
|
||
- 粘贴时带空格、引号、回车或多余字符
|
||
- 中转 key 与官方 key 混淆(不同渠道 key 不通用)
|
||
|
||
### 2. provider_type 与端点不匹配
|
||
|
||
- `openai_compat` 用 `Bearer`,`anthropic_compat` 用 `x-api-key`,两者鉴权头互斥
|
||
- 若实际是 OpenAI 兼容端点却选 `anthropic_compat`(或反之)→ 鉴权头不被识别 → 401
|
||
- 典型:
|
||
- GLM 官方 chat 端点(`open.bigmodel.cn/api/paas`)→ `openai_compat`
|
||
- GLM 的 Anthropic 兼容端点(`open.bigmodel.cn/api/anthropic`)→ `anthropic_compat`
|
||
- Claude 官方(`api.anthropic.com`)→ `anthropic_compat`
|
||
- 各类中转/聚合站(one-api/new-api)→ 一般 `openai_compat`
|
||
|
||
### 3. base_url 填错
|
||
|
||
- 多/少 `/v1` 段(智能拼接已容错多数情况,但中转站路径各异仍可能错)
|
||
- 协议错(`http` vs `https`)
|
||
- 域名拼写错误
|
||
|
||
### 4. model 名错/无权限
|
||
|
||
- 部分 API 网关对无效 model 返回 401 而非 404
|
||
- model 名大小写、版本号、前缀错(如 `glm-4` vs `glm-4-flash`,`claude-3-5-sonnet` vs `claude-3-5-sonnet-20241022`)
|
||
|
||
## 验证步骤(直连测试,区分 key / 配置)
|
||
|
||
在终端用 curl 直连,绕开 devflow,精准定位是 key 还是配置问题:
|
||
|
||
### A. provider_type = openai_compat(Bearer 鉴权)
|
||
|
||
```bash
|
||
curl -s "{base_url}/v1/chat/completions" \
|
||
-H "Authorization: Bearer {KEY}" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"model":"{MODEL}","messages":[{"role":"user","content":"hi"}],"max_tokens":5}'
|
||
```
|
||
|
||
### B. provider_type = anthropic_compat(x-api-key 鉴权)
|
||
|
||
```bash
|
||
curl -s "{base_url}/v1/messages" \
|
||
-H "x-api-key: {KEY}" \
|
||
-H "anthropic-version: 2023-06-01" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"model":"{MODEL}","max_tokens":5,"messages":[{"role":"user","content":"hi"}]}'
|
||
```
|
||
|
||
> `{base_url}` `{KEY}` `{MODEL}` 替换为设置页实际填的值(base_url 按拼接规则:不带末尾的 `/v1/...`,但带了也兼容)。
|
||
|
||
### 结果判断
|
||
|
||
| 返回 | 含义 |
|
||
|------|------|
|
||
| 正常 completion JSON | key+url+type+model 全对 → 问题在 devflow 内部(概率极低,链路已验证;此时查 Rust 日志看实际请求) |
|
||
| `401` / `authentication_error` | key 无效,**或** provider_type 选错(鉴权头不匹配) |
|
||
| `404` | base_url 拼接错,端点不存在 |
|
||
| `400` model 相关 | model 名错/无权限 |
|
||
| 连接失败/超时 | base_url 域名或网络问题 |
|
||
|
||
## TODO
|
||
|
||
- [ ] S-260615-01 用户:执行直连测试(A 或 B),确认是 key、provider_type、base_url、model 哪一项的问题
|
||
- [ ] 用户:核对 provider_type 与实际端点匹配(见「根因方向 2」的典型对照)
|
||
- [ ] 用户:核对 model 名拼写(对照 API 提供商文档)
|
||
- [ ] 若直连测试通过、devflow 内仍 401:开 Rust tracing 日志,查实际发出的 url + header 是否与直连一致(排除 body 字段触发的鉴权问题)
|
||
- [ ] **B-260615-01(可选增强)** `stream_recv.rs:107` Err 分支增加诊断信息:把 provider_type + 实际请求 url + HTTP 状态码记入日志/错误返回,便于 401 快速定位(当前错误只透传服务端文本,看不出打了哪个 url)
|