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:
@@ -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,并通过完整内部能力握手。
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user