Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions docs/specs/default-model-settings/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# 默认模型与默认视觉模型实施计划

## 1. 当前实现基线

### 1.1 新建会话模型来源(现状)

1. `src/renderer/src/components/NewThread.vue` 初始化模型时,优先“最近会话/偏好模型/第一个可用模型”。
2. `src/main/presenter/sessionPresenter/managers/conversationManager.ts` 在 `createConversation()` 中默认继承最近会话 `settings`。
3. 因此当前“新建会话默认模型”不稳定,会受最近会话影响。

### 1.2 imageServer 模型来源(现状)

1. `src/main/presenter/mcpPresenter/inMemoryServers/imageServer.ts` 构造函数接收 `provider/model`。
2. `src/main/presenter/mcpPresenter/inMemoryServers/builder.ts` 通过 `new ImageServer(args[0], args[1])` 传入。
3. `src/renderer/src/components/mcp-config/mcpServerForm.vue` 存在 `imageServer` 专属模型选择 UI,并把选择写入 server `args`。

## 2. 设计决策

### 2.1 设置数据结构

新增两个设置键(存储于 `app-settings`):

1. `defaultModel: { providerId: string; modelId: string }`
2. `defaultVisionModel: { providerId: string; modelId: string }`

说明:

1. 两者均通过现有 `configPresenter.getSetting/setSetting` 访问。
2. 不新增独立 store 文件,先沿用现有配置存储体系。

### 2.2 新建会话默认模型决策

会话创建链路分两层处理:

1. **Renderer 层(UI 体验)**:`NewThread.vue` 初始化时优先读 `defaultModel`(非 ACP)。
2. **Main 层(最终兜底)**:`conversationManager.createConversation` 在调用方未显式传 `providerId/modelId` 时应用 `defaultModel`(非 ACP)。

规则:

1. 显式传入 `providerId/modelId` 时不覆盖。
2. `chatMode === 'acp agent'` 或目标 provider 为 `acp` 时不应用 `defaultModel`。
3. `defaultModel` 未配置或无效时,回退到现有逻辑(保持兼容)。

### 2.3 默认视觉模型决策

1. `defaultVisionModel` 选择器只展示 `vision=true` 的已启用模型。
2. 保存时做前置校验(非视觉模型不可保存)。
3. `imageServer` 运行时读取 `defaultVisionModel`;不再依赖 `args`。

### 2.4 imageServer 架构调整

目标模块:

1. `src/main/presenter/mcpPresenter/inMemoryServers/imageServer.ts`
2. `src/main/presenter/mcpPresenter/inMemoryServers/builder.ts`
3. `src/renderer/src/components/mcp-config/mcpServerForm.vue`

调整方式:

1. `ImageServer` 构造函数去掉 provider/model 参数。
2. 每次视觉调用时动态读取 `defaultVisionModel` 并校验可用性。
3. `mcpServerForm.vue` 删除 `imageServer` 专属模型选择与 args 反解析逻辑。
4. `builder.ts` 改为 `new ImageServer()`。

### 2.5 兼容与迁移策略

1. 保留旧 `imageServer.args` 数据但不再使用(兼容读取,不破坏旧配置文件结构)。
2. 不做强制迁移脚本;缺失 `defaultVisionModel` 时由运行时错误提示引导用户配置。

## 3. 实施阶段

### Phase 1:配置与类型接入

1. 新增 `defaultModel/defaultVisionModel` 的读写与默认空值处理。
2. 补充必要类型定义(若现有类型未覆盖)。

### Phase 2:新建会话默认模型

1. 调整 `NewThread.vue` 初始化优先级(`defaultModel` 优先)。
2. 调整 `conversationManager.createConversation` 的兜底模型决策。
3. 校验 `fork` 路径未被覆盖。

### Phase 3:默认视觉模型与 imageServer

1. 设置页新增 `defaultVisionModel` 选择项(vision-only)。
2. 移除 `mcpServerForm.vue` 中 `imageServer` 模型配置 UI 与 args 绑定逻辑。
3. `imageServer` 改为全局读取 `defaultVisionModel`。
4. `builder.ts` 去除 `args[0]/args[1]` 注入。

### Phase 4:验证与收尾

1. 回归新建会话路径(UI 创建、主进程创建)。
2. 回归 `imageServer` 调用成功与失败场景。
3. 统一补 i18n 文案与错误提示。

## 4. 测试策略

### 4.1 Main 测试

1. `createConversation`:无显式模型时应用 `defaultModel`。
2. `createConversation`:ACP 模式不应用 `defaultModel`。
3. `forkConversation`:继承行为不变。
4. `imageServer`:读取 `defaultVisionModel` 成功/缺失/无效分支。

### 4.2 Renderer 测试

1. `NewThread` 初始化模型优先级验证(`defaultModel` 优先)。
2. 设置页视觉模型选择仅展示 vision 模型。
3. `mcpServerForm` 不再展示 `imageServer` 模型选择控件。

## 5. 风险与缓解

1. 风险:部分隐式创建会话路径未经过 UI,仍可能走旧默认。
缓解:在 `conversationManager.createConversation` 做主进程兜底。

2. 风险:用户升级后未配置 `defaultVisionModel` 导致 imageServer 报错。
缓解:统一错误文案,明确引导至设置页。

3. 风险:`defaultModel` 与 `preferredModel` 语义冲突。
缓解:明确优先级为 `defaultModel > preferredModel`(仅非 ACP)。

## 6. 质量门槛

1. `pnpm run format`
2. `pnpm run lint`
3. `pnpm run typecheck`
4. 关键 main/renderer 测试通过
89 changes: 89 additions & 0 deletions docs/specs/default-model-settings/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 默认模型与默认视觉模型规格

## 概述

新增两个全局设置项:

1. `默认模型`(`defaultModel`)
2. `默认视觉模型`(`defaultVisionModel`)

其中:

1. `默认模型`用于所有“新建会话”默认模型选择(`fork` 例外,`acp` 模式例外)。
2. `默认视觉模型`用于视觉场景,当前仅供内置 `imageServer` 使用。
3. `imageServer` 现有的“按服务器 args 配置模型”能力移除,统一改为读取全局 `defaultVisionModel`。
4. `默认视觉模型`只能选择具备 `vision` 能力的模型。

## 背景与动机

1. 当前新建会话模型会受到“最近会话/偏好模型”影响,缺少稳定的全局默认入口。
2. `imageServer` 以 MCP 服务器局部参数维护模型,配置分散,和全局模型管理不一致。
3. 视觉模型应统一做能力约束(`vision=true`),避免运行时才发现模型不支持图像输入。

## 用户故事

### US-1:新建会话统一默认模型

作为用户,我希望设置一次“默认模型”,以后新建会话时自动使用它,而不是被最近会话模型影响。

### US-2:ACP 模式不受影响

作为用户,我希望 ACP 会话仍按 ACP 机制选模型,不被“默认模型”覆盖。

### US-3:视觉能力统一入口

作为用户,我希望设置一个“默认视觉模型”,内置图片工具直接使用它,不再在 `imageServer` 里重复配置。

## 功能需求

### A. 新增全局设置项

- [ ] 新增 `defaultModel` 配置,数据结构为 `{ providerId: string, modelId: string }`
- [ ] 新增 `defaultVisionModel` 配置,数据结构为 `{ providerId: string, modelId: string }`
- [ ] 两项配置均通过 `configPresenter.getSetting/setSetting` 读写并持久化

### B. 新建会话默认模型规则

- [ ] 适用范围:所有“新建会话”路径(即调用 `createConversation` 创建新会话)
- [ ] 排除范围:`forkConversation`(以及基于分支语义的会话继承路径)不改,继续继承源会话模型
- [ ] ACP 例外:当会话处于 `acp agent` 模式时,不应用 `defaultModel`
- [ ] 优先级:当调用方未显式传入 `providerId/modelId` 时,`defaultModel` 优先于“最近会话/旧偏好模型”逻辑
- [ ] 当 `defaultModel` 未配置或已失效时,回退到当前现有兜底策略

### C. 默认视觉模型规则

- [ ] `defaultVisionModel` 的候选列表仅允许 `vision=true` 的已启用模型
- [ ] 若用户尝试保存非视觉模型,需阻止并给出明确提示
- [ ] 若 `defaultVisionModel` 未配置或失效,视觉调用返回可读错误并引导去设置页配置

### D. imageServer 统一使用全局视觉模型

- [ ] `imageServer` 不再从 MCP server `args` 读取 provider/model
- [ ] `imageServer` 每次视觉调用前从全局配置读取 `defaultVisionModel`
- [ ] `inMemoryServers/builder.ts` 中 `imageServer` 构造不再依赖 `args[0]/args[1]`
- [ ] MCP 配置表单中针对 `imageServer` 的模型选择 UI 移除

### E. 验收标准

- [ ] 在非 ACP 新建会话中,未手动改模型时默认使用 `defaultModel`
- [ ] `fork` 新会话继续继承原会话模型,不受 `defaultModel` 干预
- [ ] ACP 新建会话不受 `defaultModel` 影响
- [ ] `imageServer` 在已配置 `defaultVisionModel` 时可正常调用视觉能力
- [ ] `imageServer` 在未配置/配置无效时给出明确错误(非静默失败)
- [ ] `imageServer` 相关 MCP args 模型配置入口已移除

## 非目标

1. 不改标题生成链路的模型选择策略(本次仅新增会话默认模型与视觉默认模型)。
2. 不新增“按工具分别配置视觉模型”的能力(仅一个全局视觉模型)。
3. 不修改 ACP 模型管理机制。

## 约束

1. 保持现有 Presenter 架构与 IPC 类型边界,不引入新通信通道。
2. 保持设置持久化兼容,旧配置文件可继续加载。
3. UI 文案必须走 i18n。

## 开放问题

无。
57 changes: 57 additions & 0 deletions docs/specs/default-model-settings/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 默认模型与默认视觉模型 Tasks

## T0 规格确认

- [x] 完成 `spec.md`
- [x] 完成 `plan.md`
- [x] 完成 `tasks.md`

## T1 配置层

- [ ] 在配置体系新增 `defaultModel` 设置读写(`providerId/modelId`)。
- [ ] 在配置体系新增 `defaultVisionModel` 设置读写(`providerId/modelId`)。
- [ ] 为设置页提供读取/保存接口(复用 `configPresenter.getSetting/setSetting`)。

## T2 新建会话默认模型(Renderer + Main)

- [ ] 调整 `src/renderer/src/components/NewThread.vue` 初始化模型优先级:非 ACP 时优先 `defaultModel`。
- [ ] 保持手动选模可覆盖默认值(仅默认初始化受影响)。
- [ ] 在 `src/main/presenter/sessionPresenter/managers/conversationManager.ts` 中补主进程兜底:未显式模型且非 ACP 时应用 `defaultModel`。
- [ ] 验证主进程自动建会话入口(如 in-memory server 调用 `createConversation`)同样生效。
- [ ] 验证 `forkConversation` 路径不受影响。

## T3 设置页 UI

- [ ] 在设置页新增“默认模型”选择项(全模型,排除 ACP provider)。
- [ ] 在设置页新增“默认视觉模型”选择项(仅 `vision=true`)。
- [ ] 补齐 i18n 文案(至少 `zh-CN` + `en-US`)。
- [ ] 视觉模型保存时增加校验与错误提示。

## T4 imageServer 改造

- [ ] 修改 `src/main/presenter/mcpPresenter/inMemoryServers/imageServer.ts`:移除构造注入 provider/model,改为运行时读取 `defaultVisionModel`。
- [ ] 修改 `src/main/presenter/mcpPresenter/inMemoryServers/builder.ts`:`imageServer` 改为无参构造。
- [ ] 修改 `src/renderer/src/components/mcp-config/mcpServerForm.vue`:删除 `imageServer` 模型选择 UI、args 反解析与写回逻辑。
- [ ] 保持其他 inmemory server 的 args 行为不变。

## T5 失败处理与提示

- [ ] `defaultVisionModel` 缺失时,`imageServer` 返回可读错误。
- [ ] `defaultVisionModel` 指向非视觉或不可用模型时,`imageServer` 返回可读错误。
- [ ] 错误提示文案包含“去设置中配置默认视觉模型”。

## T6 测试

- [ ] Main:`createConversation` 非 ACP 默认模型应用测试。
- [ ] Main:ACP 场景不应用默认模型测试。
- [ ] Main:`forkConversation` 不受影响测试。
- [ ] Main:`imageServer` 读取 `defaultVisionModel` 成功/失败测试。
- [ ] Renderer:默认视觉模型只展示 vision 模型测试。
- [ ] Renderer:`mcpServerForm` 不再出现 `imageServer` 专属模型配置测试。

## T7 质量检查

- [ ] `pnpm run format`
- [ ] `pnpm run lint`
- [ ] `pnpm run typecheck`
- [ ] 跑相关测试并记录结果
43 changes: 38 additions & 5 deletions src/main/presenter/agentPresenter/utility/utilityHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,44 @@ export class UtilityHandler extends BaseHandler {
}
})
.filter((item) => item.formattedMessage.content.length > 0)
const title = await this.ctx.llmProviderPresenter.summaryTitles(
messagesWithLength.map((item) => item.formattedMessage),
conversation.settings.providerId,
conversation.settings.modelId
)
const assistantModel = this.ctx.configPresenter.getSetting<{
providerId: string
modelId: string
}>('assistantModel')
const fallbackProviderId = conversation.settings.providerId
const fallbackModelId = conversation.settings.modelId
const preferredProviderId = assistantModel?.providerId || fallbackProviderId
const preferredModelId = assistantModel?.modelId || fallbackModelId

let title: string
try {
title = await this.ctx.llmProviderPresenter.summaryTitles(
messagesWithLength.map((item) => item.formattedMessage),
preferredProviderId,
preferredModelId
)
} catch (error) {
const shouldFallback =
preferredProviderId !== fallbackProviderId || preferredModelId !== fallbackModelId
if (!shouldFallback) {
throw error
}
console.warn(
'[UtilityHandler] Failed to generate title with assistant model, fallback to conversation model',
{
preferredProviderId,
preferredModelId,
fallbackProviderId,
fallbackModelId,
error
}
)
title = await this.ctx.llmProviderPresenter.summaryTitles(
messagesWithLength.map((item) => item.formattedMessage),
fallbackProviderId,
fallbackModelId
)
}
let cleanedTitle = title.replace(/<think>.*?<\/think>/g, '').trim()
cleanedTitle = cleanedTitle.replace(/^<think>/, '').trim()
return cleanedTitle
Expand Down
18 changes: 18 additions & 0 deletions src/main/presenter/configPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ interface IAppSettings {
skillsPath?: string // Skills directory path
enableSkills?: boolean // Skills system global toggle
hooksNotifications?: HooksNotificationsSettings // Hooks & notifications settings
defaultModel?: { providerId: string; modelId: string } // Default model for new conversations
defaultVisionModel?: { providerId: string; modelId: string } // Default vision model for image tools
[key: string]: unknown // Allow arbitrary keys, using unknown type instead of any
}

Expand Down Expand Up @@ -1818,6 +1820,22 @@ export class ConfigPresenter implements IConfigPresenter {
getConfirmoHookStatus(): { available: boolean; path: string } {
return presenter.hooksNotifications.getConfirmoHookStatus()
}

getDefaultModel(): { providerId: string; modelId: string } | undefined {
return this.getSetting<{ providerId: string; modelId: string }>('defaultModel')
}

setDefaultModel(model: { providerId: string; modelId: string } | undefined): void {
this.setSetting('defaultModel', model)
}

getDefaultVisionModel(): { providerId: string; modelId: string } | undefined {
return this.getSetting<{ providerId: string; modelId: string }>('defaultVisionModel')
}

setDefaultVisionModel(model: { providerId: string; modelId: string } | undefined): void {
this.setSetting('defaultVisionModel', model)
}
}

export { defaultShortcutKey } from './shortcutKeySettings'
2 changes: 1 addition & 1 deletion src/main/presenter/mcpPresenter/inMemoryServers/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function getInMemoryServer(
case 'deepResearch':
return new DeepResearchServer(env)
case 'imageServer':
return new ImageServer(args[0], args[1])
return new ImageServer(args[0] || undefined, args[1] || undefined)
case 'powerpack':
return new PowerpackServer(env)
case 'difyKnowledge':
Expand Down
Loading