feat: 标题生成收敛到通用轻量模型路由,供应商支持不再逐个硬编码
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
## 使用场景 / Use case
给任务起名(自动起名 + 重命名输入框的 ✨ AI 重新命名)应该对用户已连接的任意供应商都可用。用户连了 DeepSeek 就该能用 DeepSeek 起名,不需要为此再连一家。
## 当前问题 / Current limitation
`apps/desktop/src/main/maker-host/title-one-shot.ts` 的 `buildTitleTarget()` 用 `switch (provider.id)` 逐家硬编码,只覆盖 `anthropic` / `openai` / `xd`,其余落 `default: return null`。文件注释自己写着「新增供应商的标题支持 = 加一个 case」。
同时,仓库里已经有一套数据驱动的通用轻量模型 one-shot 能力:`apps/desktop/src/main/utility-model/oneShotCandidates.ts`。它从 catalog 的 `routing.wireProtocol / authStrategy / upstream` 解析路由、经 `readCustomProviderKey` 取凭证,支持预设与自定义供应商,且显式传 `providerId` 时**禁止跨来源兜底**——恰好就是标题通道需要的约束。帮助兜底、任务摘要、插件 `cindy.text.oneshot`、语音精修都走它。
也就是说:**标题生成是这个通用能力的一份更窄的重复实现**。`oneShotCandidates.ts:355` 已经把 `provider.titleModel` 注为 legacy field 并明确禁止在该路径使用。
这个重复已经导致两次同类故障:
- #1891 — xd 分支直发静态 `titleModel: gpt-5.4-mini`,网关实时清单里早已没有该模型 → 标题硬失败。已用 `pickXdTitleModel()` 单点修复。
- #2046 — DeepSeek 落进 `default: return null`,功能对该供应商始终不可用。
下一家新供应商会第三次踩同一个坑。#2063 已经把失败语义和 i18n 门禁修好(空任务 / 供应商不支持现在有可操作提示),但它刻意没有动这个结构性根因,明确写了「不包含:为 DeepSeek 等供应商新增标题模型或凭证路由;这需要单独设计」。
## 期望方案 / Proposed solution
标题通道复用通用路由,删除私有分派表。需要维护者确认的契约共 5 条:
| # | 契约 | 建议口径 |
|---|---|---|
| C1 | **标题模型选择** | `provider.titleModel` 若存在且当前可选,作为**偏好**优先命中;否则从该 (供应商, agent) 的可选聊天模型里取最经济者(把 `pickXdTitleModel` 的成本排序 + `isModelSelectableForNewRoute` 过滤泛化到所有供应商)。这样 anthropic 仍钉 haiku、openai 仍钉 gpt-5.4-mini(零回归),DeepSeek / 自定义供应商自动获得支持 |
| C2 | **titleModel 字段去留** | 保留为「偏好提示」,语义从「唯一来源」降级为「优先项」,`catalog.ts` 现有校验不变。不建议本次删除——anthropic / openai 的模型清单是运行期注入(静态为空),字段是那两家的确定性锚点 |
| C3 | **不跨供应商兜底** | 不变。走 `requestUtilityText` 的**显式 providerId 路径**,该路径已 fail-closed,不会把 prompt 漏给 XD fallback chain |
| C4 | **凭证新鲜度** | 由直读 `readCodexOneShotCreds()`(读 auth.json)改为通用层的 `getChatgptBridgeAuth()`(带 refresh + 连接态门)。这是**行为改善**(少一类静默 401),但属可观察变化,需明示 |
| C5 | **失败语义映射** | `UtilityTextResult` → 现有 `TitleOneShotResult`(#2063 引入):无可路由 wire / 无可选模型 → `unsupported-provider`;凭证缺失 / 被停用 / HTTP / 超时 / 空响应 → `failed`。**不新增 IPC 错误码**,device-link 协议面零变化 |
副产品:`xai` 在通用层已可执行但标题通道没接,收敛后自动获得支持。
实现会保留标题特有的塑造(max_tokens ≤ 32、12s 超时、codex `instructions` 输出约束、`validateTitleOutput` ≤20 字校验),只把路由解析 / wire fetch / 凭证获取这三件事下沉到通用层,不为逐字节等价而保留重复代码。
## 已考虑的替代方案 / Alternatives considered
1. **给 builtin.ts 加静态 titleModel + switch 加 case** — 只是把技术债再摊薄一层,预设供应商(DeepSeek 在 providers.json 的 presets 里,不是 builtin)根本无处安放。
2. **保留私有实现,只加一个通用 default 分支** — 两套凭证/停用/重查逻辑并存,PR #744 那 22 轮 review 攒下的停用轴不变量要在两处各维护一份,长期比现在更糟。
3. **不做,只在文档里写「新增供应商记得加 case」** — 已经写了,#1891 和 #2046 就是在有注释的情况下发生的。
---
关联:#2046、#2063、#2071、#1891、#1370、#744。等契约确认后我提 PR,不会先写代码。
Contributor guide
Research direction
Start with apps/desktop/src/main/maker-host/title-one-shot.ts and its buildTitleTarget() dispatch, then compare it with apps/desktop/src/main/utility-model/oneShotCandidates.ts and the model-selection logic around pickXdTitleModel. Confirm the five proposed contracts with maintainers before changing the design. Done means connected providers use the shared route, existing Anthropic and OpenAI preferences remain supported, failures map to the existing title results, and no IPC protocol changes are needed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- ai, desktop-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100