diff --git a/docs/features/deepseek-harness/technical-design.md b/docs/features/deepseek-harness/technical-design.md index 811569b0..76972f1c 100644 --- a/docs/features/deepseek-harness/technical-design.md +++ b/docs/features/deepseek-harness/technical-design.md @@ -433,6 +433,7 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接: - 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。 - API Key 继续保存在 GoodBuddy 加密设置中。 - 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。 +- 未显式指定 Harness 模型且没有可用的管理员预置连接时,Main 优先使用当前默认的兼容 GoodBuddy 模型连接;默认连接不兼容时使用首个兼容连接。只有不存在任何兼容连接时才提示用户前往模型或 Runtime 设置。 不允许选择 Harness 自有的用户配置文件或自定义 Host。Runtime 始终使用随当前 GoodBuddy 版本发布的内置 Host,并通过完整内部能力握手。 diff --git a/src/main/runtime-settings-store.test.ts b/src/main/runtime-settings-store.test.ts index 66168355..01360d3d 100644 --- a/src/main/runtime-settings-store.test.ts +++ b/src/main/runtime-settings-store.test.ts @@ -408,6 +408,78 @@ describe('RuntimeSettingsStore', () => { }) }) + it('uses a compatible GoodBuddy model for Harness when no platform model is configured', async () => { + const profileId = '00000000-0000-4000-8000-000000000044' + const { store } = await createStore() + await store.update( + settings({ + modelProfiles: [ + { + id: profileId, + name: 'OpenAI-compatible gateway', + baseUrl: 'https://gateway.example/openai/v1', + modelName: 'qwen-plus', + protocol: 'openai-chat-completions', + authentication: 'api-key', + imageGenerationQuality: 'auto', + apiKey: { action: 'replace', value: 'deepseek-secret' } + } + ], + defaultModelProfileId: profileId + }) + ) + + await expect(store.getPublicSettings()).resolves.toMatchObject({ + deepseekHarnessModelSource: { kind: 'profile', profileId }, + configured: { + deepseekHarnessModelSource: { kind: 'platform' } + } + }) + await expect(store.getResolvedSettings()).resolves.toMatchObject({ + deepseekHarnessModelProfile: { + id: profileId, + apiKey: 'deepseek-secret' + } + }) + }) + + it('uses the first compatible Harness model when the default model is incompatible', async () => { + const defaultId = '00000000-0000-4000-8000-000000000043' + const harnessId = '00000000-0000-4000-8000-000000000044' + const { store } = await createStore() + await store.update( + settings({ + modelProfiles: [ + { + id: defaultId, + name: 'Anthropic model', + baseUrl: 'https://anthropic.example', + modelName: 'claude', + protocol: 'anthropic-messages', + authentication: 'api-key', + imageGenerationQuality: 'auto', + apiKey: { action: 'replace', value: 'anthropic-secret' } + }, + { + id: harnessId, + name: 'OpenAI-compatible gateway', + baseUrl: 'https://gateway.example/openai/v1', + modelName: 'qwen-plus', + protocol: 'openai-chat-completions', + authentication: 'api-key', + imageGenerationQuality: 'auto', + apiKey: { action: 'replace', value: 'deepseek-secret' } + } + ], + defaultModelProfileId: defaultId + }) + ) + + await expect(store.getResolvedSettings()).resolves.toMatchObject({ + deepseekHarnessModelProfile: { id: harnessId } + }) + }) + it('migrates DeepSeek Harness to controlled platform mode and stores a compatible profile', async () => { const { filePath, store } = await createStore() await store.update(settings()) diff --git a/src/main/runtime-settings-store.ts b/src/main/runtime-settings-store.ts index bae77a36..187e525c 100644 --- a/src/main/runtime-settings-store.ts +++ b/src/main/runtime-settings-store.ts @@ -1423,6 +1423,30 @@ export class RuntimeSettingsStore { } } + private resolveDeepSeekHarnessModelProfile( + settings: StoredSettings, + modelProfiles: readonly ResolvedModelProfile[] + ): ResolvedModelProfile | undefined { + const source = settings.deepseekHarnessModelSource + if (source.kind === 'profile') { + return modelProfiles.find( + (profile) => profile.id === source.profileId + ) + } + const platformProfile = this.resolvePlatformHarnessProfile() + if (platformProfile) { + return platformProfile + } + return ( + modelProfiles.find( + (profile) => + profile.id === settings.defaultModelProfileId && + isDeepSeekHarnessModelProfile(profile) + ) ?? + modelProfiles.find(isDeepSeekHarnessModelProfile) + ) + } + private resolveModelCredentials( settings: StoredSettings ): Map { @@ -1692,6 +1716,20 @@ export class RuntimeSettingsStore { credentialSource: credential.source } }) + const deepseekHarnessModelProfile = + this.resolveDeepSeekHarnessModelProfile( + settings, + resolvedModelProfiles + ) + const deepseekHarnessModelSource = + settings.deepseekHarnessModelSource.kind === 'platform' && + deepseekHarnessModelProfile && + resolvedProfilesById.has(deepseekHarnessModelProfile.id) + ? { + kind: 'profile' as const, + profileId: deepseekHarnessModelProfile.id + } + : settings.deepseekHarnessModelSource const embeddingEnvironmentApiKey = this.environment.GOODBUDDY_EMBEDDING_API_KEY?.trim() const embeddingStoredApiKey = embeddingEnvironmentApiKey @@ -1797,8 +1835,7 @@ export class RuntimeSettingsStore { ? { kind: 'platform' } : settings.opencodeModelSource, continueModelSource: settings.continueModelSource, - deepseekHarnessModelSource: - settings.deepseekHarnessModelSource, + deepseekHarnessModelSource, secureStorageAvailable: this.cipher.isAvailable(), toolApproval: settings.toolApproval, configured: { @@ -1892,11 +1929,7 @@ export class RuntimeSettingsStore { ? profilesById.get(settings.continueModelSource.profileId) : undefined const deepseekHarnessModelProfile = - settings.deepseekHarnessModelSource.kind === 'profile' - ? profilesById.get( - settings.deepseekHarnessModelSource.profileId - ) - : this.resolvePlatformHarnessProfile() + this.resolveDeepSeekHarnessModelProfile(settings, modelProfiles) const embeddingConnections = this.resolveEmbeddingConnections(settings) const activeEmbeddingConnection = diff --git a/src/renderer/src/SettingsPanel.test.tsx b/src/renderer/src/SettingsPanel.test.tsx index 9c8b449d..e9675bcf 100644 --- a/src/renderer/src/SettingsPanel.test.tsx +++ b/src/renderer/src/SettingsPanel.test.tsx @@ -3057,7 +3057,9 @@ describe('SettingsPanel runtime files', () => { ) expect(screen.queryByRole('radio')).not.toBeInTheDocument() expect( - screen.getByText('管理员预置的 OpenAI 兼容连接') + screen.getByText( + '自动选择:优先使用管理员预置连接,否则跟随当前兼容模型' + ) ).toBeInTheDocument() const source = screen.getByLabelText( 'DeepSeek Harness OpenAI 兼容模型连接' diff --git a/src/renderer/src/i18n/locales/en-US/settings.ts b/src/renderer/src/i18n/locales/en-US/settings.ts index e547e1d6..50774999 100644 --- a/src/renderer/src/i18n/locales/en-US/settings.ts +++ b/src/renderer/src/i18n/locales/en-US/settings.ts @@ -395,12 +395,12 @@ export const settings = { description: 'GoodBuddy maintains the fixed Host and control protocol internally and uses pinned Harness libraries underneath. Ask can call native read/skill plus enabled Web Search/Fetch, while Execute can use every enabled tool and DSH plugin capability. Cancellation and workspace boundaries remain in place.', managedSource: - 'Administrator-provided OpenAI-compatible connection', + 'Automatic: prefer the administrator connection, otherwise follow a compatible current model', connection: 'OpenAI-compatible model connection', connectionPlaceholder: - 'Select an OpenAI-compatible model connection', + 'Automatically select a compatible model connection', connectionDescription: - 'Choose a GoodBuddy model connection. It must use OpenAI Chat Completions with API-key authentication.', + 'Optionally select a GoodBuddy model connection. Otherwise GoodBuddy uses the administrator connection, the compatible current default, or the first compatible connection. It must use OpenAI Chat Completions with API-key authentication.', advancedDescription: 'This Runtime always uses GoodBuddy’s bundled, version-pinned Host and does not load user profiles or custom Hosts. GoodBuddy manages enabled marketplace plugins and loads them with the Host.', marketplace: { diff --git a/src/renderer/src/i18n/locales/zh-CN/settings.ts b/src/renderer/src/i18n/locales/zh-CN/settings.ts index d9c896ee..10d69a36 100644 --- a/src/renderer/src/i18n/locales/zh-CN/settings.ts +++ b/src/renderer/src/i18n/locales/zh-CN/settings.ts @@ -362,11 +362,12 @@ export const settings = { previewDescription: '开发者预览 · OpenAI 兼容', description: '由 GoodBuddy 内部维护固定 Host 与控制协议,复用锁定的 Harness 底层库;Ask 可调用 Harness 原生 read/skill 与已启用的网页搜索/抓取,Execute 可调用全部已启用工具及 DSH 插件能力,并保留取消和工作区边界。', - managedSource: '管理员预置的 OpenAI 兼容连接', + managedSource: + '自动选择:优先使用管理员预置连接,否则跟随当前兼容模型', connection: 'OpenAI 兼容模型连接', - connectionPlaceholder: '选择 OpenAI 兼容模型连接', + connectionPlaceholder: '自动选择兼容模型连接', connectionDescription: - '从 GoodBuddy 模型连接中选择;协议必须为 OpenAI Chat Completions,并使用 API Key。', + '可从 GoodBuddy 模型连接中显式指定;未指定时自动使用管理员预置连接、当前默认兼容连接或首个兼容连接。协议必须为 OpenAI Chat Completions,并使用 API Key。', advancedDescription: '该 Runtime 始终使用 GoodBuddy 内置并固定版本的 Host,不加载用户 profile 或自定义 Host;已启用的市场插件由 GoodBuddy 托管并随 Host 启动。', marketplace: {