# 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）
