bug: 移动端新建会话使用默认模型时不携带 providerId,导致 provider_id 落库为 NULL 并回落路由
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
## 问题描述 / What happened
通过 device-link 从移动端远程控制桌面端时,**新建会话页如果不主动点选模型(直接使用界面上已显示的默认模型)**,创建请求不会携带 `providerId`,导致被控端 `sessions.provider_id` 落库为 `NULL`。该会话随后回落到默认凭证路由,请求走到不支持 Opus 的网关路由,首条消息即失败。
界面上此时显示的是一个具体模型名(例如 `Claude Opus 5`),用户没有任何提示表明"来源尚未选择"。只要在同一个新建页**重新点选一次同一个模型**,`providerId` 就会被携带,`provider_id` 正常落库,会话立即可用。
**期望行为**:新建会话时界面所显示的默认模型,应当与其所属供应商一同生效;不应出现"模型有默认值、来源没有默认值"的不对称,使用户在毫无察觉的情况下创建一个来源为空的会话。
## 对照数据 / Evidence
同一台被控端、同一账号、同一晚连续操作,唯一变量是「是否主动点选模型」。数据取自被控端 `sessions` 表:
| 会话 | 创建时间 | model | provider_id | 操作方式 | 结果 |
|---|---|---|---|---|---|
| d71819ae | 22:58:34 | `claude-opus-5` | **NULL** | **未动默认模型** | **失败** |
| 8159f06b | 22:56:41 | `claude-opus-4-8` | `anthropic` | 主动改选模型 | 成功 |
| f783b9b7 | 23:10:58 | `claude-opus-4-6` | `anthropic` | 主动点选模型 | 成功 |
| b1a538cc | 23:28:34 | `claude-opus-5` | `anthropic` | 主动点选模型 | 成功 |
| d18f3d2c | 23:29:03 | `claude-opus-5` | `anthropic` | 重新点选同一模型 | 成功 |
关键对照:**b1a538cc / d18f3d2c 与 d71819ae 使用的是同一个模型 `claude-opus-5`**,前两者主动点选后 `provider_id=anthropic` 且可用,后者未动默认值则 `provider_id=NULL` 且失败。这排除了「该模型本身不可用」或「账号套餐不支持」的可能,唯一分离变量就是 `provider_id` 是否有值。
补充:该账号为支持 Opus 的套餐,桌面端历史上 `claude-opus-5` + `provider_id=anthropic` 的会话一直正常。
## 根因定位 / Root cause
链路上存在一处不对称:**模型有默认值,来源没有默认值**,且下游每一层都以「未提供」为由跳过。
1. 移动端新建页仅在用户**主动选中某一行**时才把来源写入草稿 —— `apps/mobile/app/sessions/new.tsx:1468-1492` 调用 `resolveRowSelection`,其返回值取自被选中行:
`apps/mobile/src/session/providerModelSections.ts:244-250`
```ts
return {
model: row.model.id,
providerId: row.provider.id,
effort,
fastMode,
};
```
2. 组装创建参数时,`providerId` 是**条件展开** —— 草稿里没有就整个字段不发:
`apps/mobile/src/session/newSession.ts:439-458`
```ts
const providerId = draft.providerId?.trim();
const base = {
agentKind: draft.agentKind,
model: draft.model.trim(),
// ...
...(providerId ? { providerId } : {}),
};
```
注意此处 `model` 无条件携带(有默认值),而 `providerId` 依赖草稿是否被主动赋值。
3. 被控端初次 INSERT 本就不写 `provider_id`(`SessionMeta` 不含该字段),依赖创建后的一次补写:
`packages/maker-core/src/interfaces/session-storage.ts:10-31` — `SessionMeta` 无 `providerId`
`apps/desktop/src/main/maker-host/session-storage.ts:68-94` — INSERT 不含 `provider_id`
`apps/desktop/src/main/maker-ipc/register.ts:5713-5729` — 代码注释已明确记录此结构缺口:
> device-link 远程 create(P2):DesktopSessionStorage.create 从 maker-core SessionMeta 建行,SessionMeta 不含 providerId → 远程新建行 provider_id 恒 NULL。这里在 hydrate 前用 create opts 的 providerId 补写 DB。
4. 而该补写以 `!== undefined` 为前提,字段缺席时整段跳过:
`apps/desktop/src/main/maker-ipc/sessionProviderBootstrap.ts:9-25`
```ts
if (input.providerId !== undefined) {
await input.updateProviderId(input.sessionId, createProviderId);
}
```
5. `provider_id` 为空的运行时后果:
`packages/maker-core/src/agents/credential-mode.ts:20-32`
```ts
if (options.agentKind === 'claude-code' && providerId === 'anthropic') {
return 'oauth-bearer';
}
if (!providerId) {
// 普通 Claude 模型进入既有 fallback
return undefined;
}
```
综上:默认模型未经主动选择 → 草稿无 `providerId` → 创建请求不含该字段 → 补写跳过 → `provider_id` 保持 `NULL` → 回落默认凭证路由 → 请求走到不支持 Opus 的网关并返回 400。
## 可观测性问题 / Observability gap
排查过程中发现两处使该问题难以自助诊断:
1. `create-session invoked` 的日志字段清单不包含 `providerId`(`apps/desktop/src/main/maker-ipc/sessionCreateHandler.ts:100-109`),无法从日志判断创建请求究竟是否携带了来源。
2. 创建后补写失败被视为 non-fatal 且仅记 `log.debug`(`apps/desktop/src/main/maker-ipc/register.ts:5749-5753`),在默认日志级别下完全不可见(实测该机 DEBUG 输出为 0 条)。
建议在 `create-session invoked` 中加入 `providerId`(或至少 `hasProviderId` 布尔),并将补写失败提升到 `warn`。
## 环境 / Environment
- Cindy 版本 / version:v0.1.38
- 被控端 / controlled host:macOS(Darwin 25.5.0),Apple Silicon
- 控制端 / controller:iOS,通过 device-link 远程控制
- 安装方式 / install method:官方 release
## 复现步骤 / Steps to reproduce
1. 移动端通过 device-link 远程控制一台桌面端
2. 在移动端新建会话页,**不点选模型**,直接使用界面上已显示的默认模型(例如 `Claude Opus 5`)
3. 创建会话并发送第一条消息
4. 观察被控端 `sessions` 表中该会话 `provider_id` 为 `NULL`,且消息失败
5. 对照组:重复步骤 1-3,但在新建页**主动点选一次同一个模型**,`provider_id` 落库为具体来源,消息正常
## 相关 / Related
- #1939:本问题失败后,移动端透传的是 Claude Code 的原始错误文案(提示用户执行 `/logout`、`/login`),而非 Cindy 桌面端已有的正确归因。两者是根因与症状的关系:本 issue 解释「为什么请求会走错路由」,#1939 解释「走错之后用户为何无法自行定位」。实际影响叠加后,用户可连续多日无法使用且被引导至错误的恢复操作。
Contributor guide
Research direction
Start at apps/mobile/app/sessions/new.tsx:1468-1492 and apps/mobile/src/session/newSession.ts:439-458, then trace provider handling through apps/desktop/src/main/maker-ipc/sessionProviderBootstrap.ts and register.ts. Verify the default model's provider is preserved when no row is selected, provider_id is populated, and creation logs expose the relevant provider state; use the listed reproduction steps to confirm the first message succeeds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, database, mobile, observability
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100