fix: align DeepSeek Harness model selection

DeepSeek Harness previously required a separate Runtime model selection when no administrator-provided connection was available, even if the user had already selected a compatible GoodBuddy model. It now prefers the administrator connection, then follows the compatible default model or first compatible connection across conversations, projects, and messaging channels.

Release note: 修复 DeepSeek Harness 未自动使用已选兼容模型的问题;没有管理员预置连接时,现在会跟随当前默认或首个兼容模型,无需再到 Harness 页面重复指定。
This commit is contained in:
mesalogo
2026-09-02 16:45:42 +08:00
parent 42cbfa43f1
commit 5fac1d3c41
6 changed files with 123 additions and 14 deletions
@@ -433,6 +433,7 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
- 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。 - 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。
- API Key 继续保存在 GoodBuddy 加密设置中。 - API Key 继续保存在 GoodBuddy 加密设置中。
- 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。 - 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。
- 未显式指定 Harness 模型且没有可用的管理员预置连接时,Main 优先使用当前默认的兼容 GoodBuddy 模型连接;默认连接不兼容时使用首个兼容连接。只有不存在任何兼容连接时才提示用户前往模型或 Runtime 设置。
不允许选择 Harness 自有的用户配置文件或自定义 Host。Runtime 始终使用随当前 GoodBuddy 版本发布的内置 Host,并通过完整内部能力握手。 不允许选择 Harness 自有的用户配置文件或自定义 Host。Runtime 始终使用随当前 GoodBuddy 版本发布的内置 Host,并通过完整内部能力握手。
+72
View File
@@ -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 () => { it('migrates DeepSeek Harness to controlled platform mode and stores a compatible profile', async () => {
const { filePath, store } = await createStore() const { filePath, store } = await createStore()
await store.update(settings()) await store.update(settings())
+40 -7
View File
@@ -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( private resolveModelCredentials(
settings: StoredSettings settings: StoredSettings
): Map<string, ResolvedModelCredential> { ): Map<string, ResolvedModelCredential> {
@@ -1692,6 +1716,20 @@ export class RuntimeSettingsStore {
credentialSource: credential.source 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 = const embeddingEnvironmentApiKey =
this.environment.GOODBUDDY_EMBEDDING_API_KEY?.trim() this.environment.GOODBUDDY_EMBEDDING_API_KEY?.trim()
const embeddingStoredApiKey = embeddingEnvironmentApiKey const embeddingStoredApiKey = embeddingEnvironmentApiKey
@@ -1797,8 +1835,7 @@ export class RuntimeSettingsStore {
? { kind: 'platform' } ? { kind: 'platform' }
: settings.opencodeModelSource, : settings.opencodeModelSource,
continueModelSource: settings.continueModelSource, continueModelSource: settings.continueModelSource,
deepseekHarnessModelSource: deepseekHarnessModelSource,
settings.deepseekHarnessModelSource,
secureStorageAvailable: this.cipher.isAvailable(), secureStorageAvailable: this.cipher.isAvailable(),
toolApproval: settings.toolApproval, toolApproval: settings.toolApproval,
configured: { configured: {
@@ -1892,11 +1929,7 @@ export class RuntimeSettingsStore {
? profilesById.get(settings.continueModelSource.profileId) ? profilesById.get(settings.continueModelSource.profileId)
: undefined : undefined
const deepseekHarnessModelProfile = const deepseekHarnessModelProfile =
settings.deepseekHarnessModelSource.kind === 'profile' this.resolveDeepSeekHarnessModelProfile(settings, modelProfiles)
? profilesById.get(
settings.deepseekHarnessModelSource.profileId
)
: this.resolvePlatformHarnessProfile()
const embeddingConnections = const embeddingConnections =
this.resolveEmbeddingConnections(settings) this.resolveEmbeddingConnections(settings)
const activeEmbeddingConnection = const activeEmbeddingConnection =
+3 -1
View File
@@ -3057,7 +3057,9 @@ describe('SettingsPanel runtime files', () => {
) )
expect(screen.queryByRole('radio')).not.toBeInTheDocument() expect(screen.queryByRole('radio')).not.toBeInTheDocument()
expect( expect(
screen.getByText('管理员预置的 OpenAI 兼容连接') screen.getByText(
'自动选择:优先使用管理员预置连接,否则跟随当前兼容模型'
)
).toBeInTheDocument() ).toBeInTheDocument()
const source = screen.getByLabelText( const source = screen.getByLabelText(
'DeepSeek Harness OpenAI 兼容模型连接' 'DeepSeek Harness OpenAI 兼容模型连接'
@@ -395,12 +395,12 @@ export const settings = {
description: 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.', '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: managedSource:
'Administrator-provided OpenAI-compatible connection', 'Automatic: prefer the administrator connection, otherwise follow a compatible current model',
connection: 'OpenAI-compatible model connection', connection: 'OpenAI-compatible model connection',
connectionPlaceholder: connectionPlaceholder:
'Select an OpenAI-compatible model connection', 'Automatically select a compatible model connection',
connectionDescription: 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: 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.', '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: { marketplace: {
@@ -362,11 +362,12 @@ export const settings = {
previewDescription: '开发者预览 · OpenAI 兼容', previewDescription: '开发者预览 · OpenAI 兼容',
description: description:
'由 GoodBuddy 内部维护固定 Host 与控制协议,复用锁定的 Harness 底层库;Ask 可调用 Harness 原生 read/skill 与已启用的网页搜索/抓取,Execute 可调用全部已启用工具及 DSH 插件能力,并保留取消和工作区边界。', '由 GoodBuddy 内部维护固定 Host 与控制协议,复用锁定的 Harness 底层库;Ask 可调用 Harness 原生 read/skill 与已启用的网页搜索/抓取,Execute 可调用全部已启用工具及 DSH 插件能力,并保留取消和工作区边界。',
managedSource: '管理员预置的 OpenAI 兼容连接', managedSource:
'自动选择:优先使用管理员预置连接,否则跟随当前兼容模型',
connection: 'OpenAI 兼容模型连接', connection: 'OpenAI 兼容模型连接',
connectionPlaceholder: '选择 OpenAI 兼容模型连接', connectionPlaceholder: '自动选择兼容模型连接',
connectionDescription: connectionDescription:
'从 GoodBuddy 模型连接中选择;协议必须为 OpenAI Chat Completions,并使用 API Key。', '可从 GoodBuddy 模型连接中显式指定;未指定时自动使用管理员预置连接、当前默认兼容连接或首个兼容连接。协议必须为 OpenAI Chat Completions,并使用 API Key。',
advancedDescription: advancedDescription:
'该 Runtime 始终使用 GoodBuddy 内置并固定版本的 Host,不加载用户 profile 或自定义 Host;已启用的市场插件由 GoodBuddy 托管并随 Host 启动。', '该 Runtime 始终使用 GoodBuddy 内置并固定版本的 Host,不加载用户 profile 或自定义 Host;已启用的市场插件由 GoodBuddy 托管并随 Host 启动。',
marketplace: { marketplace: {