feat: expand DeepSeek Harness compatibility

This commit is contained in:
lofyer
2026-08-14 12:38:01 +08:00
parent 36e05d45fa
commit 45aeecb6dd
32 changed files with 554 additions and 193 deletions
+28 -1
View File
@@ -99,10 +99,37 @@ describe('createAgentRuntime model compatibility', () => {
{ deepseekHarnessLauncher: vi.fn() }
)
).toThrow(
'DeepSeek Harness 需要 api.deepseek.com 的 OpenAI Chat Completions 模型连接'
'DeepSeek Harness 需要使用 API Key 的安全 OpenAI 兼容 Chat Completions 模型连接'
)
})
it('creates DeepSeek Harness with a compatible HTTPS gateway profile', async () => {
const profile = {
id: '00000000-0000-4000-8000-000000000006',
name: 'OpenAI-compatible gateway',
baseUrl: 'https://gateway.example/openai/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions' as const,
authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const,
apiKey: 'gateway-key'
}
const runtime = createAgentRuntime(
process.cwd(),
settings({
provider: 'deepseek-harness',
modelProfiles: [profile],
defaultModelProfileId: profile.id,
deepseekHarnessModelProfile: profile,
runtimeSandboxMode: 'auto'
}),
{ deepseekHarnessLauncher: vi.fn() }
)
expect(runtime.runtimeId).toBe('deepseek-harness')
await runtime.dispose()
})
it('creates an available direct runtime for a no-auth model', async () => {
const runtime = createAgentRuntime(process.cwd(), settings())
+2 -2
View File
@@ -113,7 +113,7 @@ export function createAgentRuntime(
const profile = settings?.deepseekHarnessModelProfile
if (!profile || !isDeepSeekHarnessModelProfile(profile)) {
throw new Error(
'DeepSeek Harness 需要 api.deepseek.com 的 OpenAI Chat Completions 模型连接'
'DeepSeek Harness 需要使用 API Key 的安全 OpenAI 兼容 Chat Completions 模型连接'
)
}
if (!profile.apiKey) {
@@ -131,7 +131,7 @@ export function createAgentRuntime(
model: profile.modelName,
launch: capabilities.deepseekHarnessLauncher,
credentialRefs: {
GOODBUDDY_DEEPSEEK_API_KEY: profile.apiKey
GOODBUDDY_HARNESS_MODEL_API_KEY: profile.apiKey
},
requiredSandboxEnforcement:
sandboxMode === 'strict' ? 'full' : 'partial',
@@ -26,7 +26,7 @@ import {
import { GOODBUDDY_HARNESS_MAX_STEP_TOKENS } from './goodbuddy-harness-control-plane'
const MAX_FRAME_BYTES = 1024 * 1024
const CREDENTIAL_REF = 'GOODBUDDY_DEEPSEEK_API_KEY'
const CREDENTIAL_REF = 'GOODBUDDY_HARNESS_MODEL_API_KEY'
const SKILL_CALL_ID = 'e2e-skill-call'
const MCP_CALL_ID = 'e2e-mcp-call'
const ASK_MCP_CALL_ID = 'e2e-ask-mcp-call'
@@ -536,9 +536,9 @@ describe('DeepSeek Harness real ACP control-plane E2E', () => {
{
id: 'web-3d-game',
directory: resolve(
'resources',
'skills',
'web-3d-game'
'tests',
'fixtures',
'web-3d-game-skill'
)
}
],
@@ -0,0 +1,118 @@
import type childProcess from 'node:child_process'
import { describe, expect, it, vi } from 'vitest'
import { installHarnessChildProcessWindowGuard } from './deepseek-harness-child-process'
type HarnessChildProcessModule = Pick<
typeof childProcess,
'execFileSync' | 'spawn' | 'spawnSync'
>
function fakeChildProcessModule(): {
target: HarnessChildProcessModule
execFileSync: ReturnType<typeof vi.fn>
spawn: ReturnType<typeof vi.fn>
spawnSync: ReturnType<typeof vi.fn>
} {
const execFileSync = vi.fn(() => 'output')
const spawn = vi.fn(() => ({ pid: 1 }))
const spawnSync = vi.fn(() => ({ status: 0 }))
return {
target: {
execFileSync:
execFileSync as unknown as HarnessChildProcessModule['execFileSync'],
spawn: spawn as unknown as HarnessChildProcessModule['spawn'],
spawnSync:
spawnSync as unknown as HarnessChildProcessModule['spawnSync']
},
execFileSync,
spawn,
spawnSync
}
}
describe('DeepSeek Harness child process window guard', () => {
it('does not alter child process launches outside Windows', () => {
const { target, spawn } = fakeChildProcessModule()
const originalSpawn = target.spawn
const syncExports = vi.fn()
const restore = installHarnessChildProcessWindowGuard(
'linux',
target,
syncExports
)
expect(target.spawn).toBe(originalSpawn)
expect(syncExports).not.toHaveBeenCalled()
restore()
expect(spawn).not.toHaveBeenCalled()
expect(syncExports).not.toHaveBeenCalled()
})
it('forces hidden Windows launches and restores the original functions', () => {
const { target, execFileSync, spawn, spawnSync } =
fakeChildProcessModule()
const originals = { ...target }
const syncExports = vi.fn()
const restore = installHarnessChildProcessWindowGuard(
'win32',
target,
syncExports
)
target.spawn('runner.exe', ['--probe'], {
cwd: 'C:\\workspace',
windowsHide: false
})
target.spawnSync('taskkill.exe', {
stdio: 'ignore'
})
target.spawnSync('where.exe', undefined, {
encoding: 'utf8'
})
target.execFileSync('where.exe', ['pwsh.exe'], {
encoding: 'utf8',
windowsHide: false
})
expect(spawn).toHaveBeenCalledWith(
'runner.exe',
['--probe'],
expect.objectContaining({
cwd: 'C:\\workspace',
windowsHide: true
})
)
expect(spawnSync).toHaveBeenCalledWith(
'where.exe',
undefined,
expect.objectContaining({
encoding: 'utf8',
windowsHide: true
})
)
expect(spawnSync).toHaveBeenCalledWith(
'taskkill.exe',
expect.objectContaining({
stdio: 'ignore',
windowsHide: true
})
)
expect(execFileSync).toHaveBeenCalledWith(
'where.exe',
['pwsh.exe'],
expect.objectContaining({
encoding: 'utf8',
windowsHide: true
})
)
expect(syncExports).toHaveBeenCalledTimes(1)
restore()
restore()
expect(target).toMatchObject(originals)
expect(syncExports).toHaveBeenCalledTimes(2)
})
})
@@ -0,0 +1,82 @@
import childProcess from 'node:child_process'
import { syncBuiltinESMExports } from 'node:module'
type HarnessChildProcessModule = Pick<
typeof childProcess,
'execFileSync' | 'spawn' | 'spawnSync'
>
type SyncBuiltinExports = () => void
function withHiddenWindow(args: unknown[]): unknown[] {
const next = [...args]
const optionsIndex =
Array.isArray(next[1]) ||
(next[1] === undefined && next.length >= 3)
? 2
: 1
const options = next[optionsIndex]
next[optionsIndex] = {
...(options && typeof options === 'object' ? options : {}),
windowsHide: true
}
return next
}
/**
* DeepSeek Harness 0.1.0-rc.6 omits `windowsHide` when its local subprocess
* service starts the ACL runner and PowerShell. In an Electron GUI process
* that can briefly create a visible console window. Keep this override scoped
* to the isolated Harness UtilityProcess and synchronize the built-in ESM
* bindings already captured by the bundled Harness modules.
*/
export function installHarnessChildProcessWindowGuard(
platform: NodeJS.Platform = process.platform,
target: HarnessChildProcessModule = childProcess,
syncExports: SyncBuiltinExports = syncBuiltinESMExports
): () => void {
if (platform !== 'win32') {
return () => undefined
}
const originals = {
execFileSync: target.execFileSync,
spawn: target.spawn,
spawnSync: target.spawnSync
}
const guardedExecFileSync = ((...args: unknown[]) =>
Reflect.apply(
originals.execFileSync,
target,
withHiddenWindow(args)
)) as typeof target.execFileSync
const guardedSpawn = ((...args: unknown[]) =>
Reflect.apply(
originals.spawn,
target,
withHiddenWindow(args)
)) as typeof target.spawn
const guardedSpawnSync = ((...args: unknown[]) =>
Reflect.apply(
originals.spawnSync,
target,
withHiddenWindow(args)
)) as typeof target.spawnSync
target.execFileSync = guardedExecFileSync
target.spawn = guardedSpawn
target.spawnSync = guardedSpawnSync
syncExports()
let restored = false
return () => {
if (restored) {
return
}
restored = true
target.execFileSync = originals.execFileSync
target.spawn = originals.spawn
target.spawnSync = originals.spawnSync
syncExports()
}
}
@@ -49,8 +49,8 @@ async function fixture() {
launchOptions: {
cwd: workspace,
signal: new AbortController().signal,
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
baseUrl: 'https://gateway.example/openai/v1',
model: 'qwen-plus',
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF],
skillPackages: []
}
@@ -97,7 +97,8 @@ describe('DeepSeek Harness utility launcher', () => {
expect(utility.messages[0]).toMatchObject({
type: 'start',
config: {
baseUrl: 'https://api.deepseek.com',
baseUrl: 'https://gateway.example/openai/v1',
model: 'qwen-plus',
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF]
}
})
@@ -144,4 +145,24 @@ describe('DeepSeek Harness utility launcher', () => {
await expect(launching).rejects.toThrow('启动协议无效')
expect(terminateProcess).toHaveBeenCalledOnce()
})
it.each([
'http://gateway.example/v1',
'https://user:secret@gateway.example/v1',
'https://gateway.example/v1?api-version=2025-01-01'
])('rejects unsafe endpoint %s before forking', async (baseUrl) => {
const { dshHome, hostPath, launchOptions } = await fixture()
const fork = vi.fn()
const launcher = createDeepSeekHarnessUtilityLauncher({
bundledHostPath: hostPath,
dshHome,
environment: {},
fork
})
await expect(
launcher({ ...launchOptions, baseUrl })
).rejects.toThrow('HTTPS')
expect(fork).not.toHaveBeenCalled()
})
})
@@ -3,6 +3,7 @@ import { realpath, stat } from 'node:fs/promises'
import { isAbsolute } from 'node:path'
import type { UtilityProcess } from 'electron'
import { z } from 'zod'
import { isDeepSeekHarnessCompatibleBaseUrl } from '../../shared/deepseek-harness-compatibility'
import type {
DeepSeekHarnessChild,
DeepSeekHarnessLaunchOptions
@@ -14,7 +15,7 @@ export const DEEPSEEK_HARNESS_CONTROL_PROTOCOL =
export const DEEPSEEK_HARNESS_CONTROL_VERSION = 1
export const DEEPSEEK_HARNESS_HOST_VERSION = '0.1.0-rc.6'
export const DEEPSEEK_HARNESS_CREDENTIAL_REF =
'GOODBUDDY_DEEPSEEK_API_KEY'
'GOODBUDDY_HARNESS_MODEL_API_KEY'
const sandboxSchema = z
.object({
@@ -41,15 +42,7 @@ export const controlledHarnessHostConfigSchema = z
baseUrl: z
.url()
.max(2_048)
.refine((value) => {
const url = new URL(value)
return (
url.protocol === 'https:' &&
url.hostname.toLowerCase() === 'api.deepseek.com' &&
!url.username &&
!url.password
)
}),
.refine(isDeepSeekHarnessCompatibleBaseUrl),
api: z.literal('openai-completions'),
provider: z.literal('goodbuddy'),
model: z.string().min(1).max(128),
@@ -240,12 +233,9 @@ export function createDeepSeekHarnessUtilityLauncher(
'DeepSeek Harness 当前平台只能提供部分沙箱强制'
)
}
if (
options.baseUrl !== 'https://api.deepseek.com' &&
options.baseUrl !== 'https://api.deepseek.com/'
) {
if (!isDeepSeekHarnessCompatibleBaseUrl(options.baseUrl)) {
throw new Error(
'DeepSeek Harness 仅允许 api.deepseek.com'
'DeepSeek Harness 模型地址必须使用 HTTPS 或本机回环 HTTP,且不得包含凭据、查询参数或片段'
)
}
if (
+19 -19
View File
@@ -10,7 +10,7 @@ const defaultProfileId = '00000000-0000-4000-8000-000000000001'
const secondProfileId = '00000000-0000-4000-8000-000000000002'
const responsesProfileId = '00000000-0000-4000-8000-000000000003'
const imageProfileId = '00000000-0000-4000-8000-000000000004'
const deepseekProfileId = '00000000-0000-4000-8000-000000000005'
const harnessProfileId = '00000000-0000-4000-8000-000000000005'
function settings(
overrides: Partial<ResolvedRuntimeSettings> = {}
@@ -65,10 +65,10 @@ function settings(
apiKey: 'image-key'
},
{
id: deepseekProfileId,
name: 'DeepSeek',
baseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
id: harnessProfileId,
name: 'OpenAI-compatible gateway',
baseUrl: 'https://gateway.example/openai/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
@@ -165,31 +165,31 @@ describe('runtime selection', () => {
).toThrow('自动启动')
})
it('selects DeepSeek Harness only with an official compatible profile', () => {
it('selects DeepSeek Harness with a compatible gateway profile', () => {
const selected = applyRuntimeSelection(settings(), {
provider: 'deepseek-harness',
profileId: deepseekProfileId
profileId: harnessProfileId
})
expect(selected.target).toBe('deepseek-harness')
expect(selected.settings).toMatchObject({
provider: 'deepseek-harness',
deepseekHarnessModelProfile: { id: deepseekProfileId }
deepseekHarnessModelProfile: { id: harnessProfileId }
})
expect(() =>
applyRuntimeSelection(settings(), {
provider: 'deepseek-harness',
profileId: secondProfileId
})
).toThrow('api.deepseek.com')
).toThrow('API Key')
})
it('keeps the controlled platform DeepSeek profile when selected without a profile ID', () => {
it('keeps the controlled platform Harness profile when selected without a profile ID', () => {
const base = settings()
const platformProfile = {
...base.modelProfiles[4]!,
id: 'goodbuddy-platform-deepseek',
name: '平台 DeepSeek',
modelName: 'deepseek-v4-flash'
id: 'goodbuddy-platform-harness',
name: '管理员预置模型',
modelName: 'qwen-plus'
}
const selected = applyRuntimeSelection(
settings({ deepseekHarnessModelProfile: platformProfile }),
@@ -199,8 +199,8 @@ describe('runtime selection', () => {
expect(selected.settings).toMatchObject({
provider: 'deepseek-harness',
deepseekHarnessModelProfile: {
id: 'goodbuddy-platform-deepseek',
modelName: 'deepseek-v4-flash'
id: 'goodbuddy-platform-harness',
modelName: 'qwen-plus'
}
})
})
@@ -219,7 +219,7 @@ describe('runtime selection', () => {
})
).toEqual({
provider: 'deepseek-harness',
profileId: deepseekProfileId
profileId: harnessProfileId
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
@@ -249,13 +249,13 @@ describe('runtime selection', () => {
})
})
it('keeps the controlled platform DeepSeek source profile-free across configured selection repair', () => {
it('keeps the controlled platform Harness source profile-free across configured selection repair', () => {
const base = settings()
const configured = settings({
deepseekHarnessModelProfile: {
...base.modelProfiles[4]!,
id: 'goodbuddy-platform-deepseek',
name: '平台 DeepSeek'
id: 'goodbuddy-platform-harness',
name: '管理员预置模型'
}
})
+1 -1
View File
@@ -139,7 +139,7 @@ export function applyRuntimeSelection(
!isDeepSeekHarnessModelProfile(selectedProfile)
) {
throw new Error(
'DeepSeek Harness 独立模型连接仅支持 api.deepseek.com 的 OpenAI Chat Completions 协议'
'DeepSeek Harness 仅支持使用 API Key 的安全 OpenAI 兼容 Chat Completions 连接'
)
}
return {