feat: expand DeepSeek Harness compatibility
This commit is contained in:
@@ -177,10 +177,10 @@ async function run() {
|
||||
config: {
|
||||
workspace,
|
||||
dshHome,
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
baseUrl: 'https://gateway.example/openai/v1',
|
||||
api: 'openai-completions',
|
||||
provider: 'goodbuddy',
|
||||
model: 'deepseek-chat',
|
||||
model: 'qwen-plus',
|
||||
harnessVersion: '0.1.0-rc.6',
|
||||
sandbox: {
|
||||
provider:
|
||||
@@ -192,7 +192,7 @@ async function run() {
|
||||
enforcement:
|
||||
process.platform === 'win32' ? 'partial' : 'full'
|
||||
},
|
||||
credentialRefs: ['GOODBUDDY_DEEPSEEK_API_KEY'],
|
||||
credentialRefs: ['GOODBUDDY_HARNESS_MODEL_API_KEY'],
|
||||
skillPackages: [],
|
||||
maxFrameBytes: 1024 * 1024
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ GoodBuddy 不急于把该 Runtime 包装成标准 DSH 插件,也不以进入
|
||||
- 支持多会话、同会话串行、跨会话并行。
|
||||
- 支持按请求取消、超时、会话释放和应用退出时完整回收。
|
||||
- 输出文本、推理、工具参数、工具结果、stderr 和协议队列全部有界。
|
||||
- 使用真实 DeepSeek 模型验证调用,而不在日志、测试产物或提交中暴露凭据。
|
||||
- 使用真实 OpenAI 兼容 Chat Completions 模型验证调用,而不在日志、测试产物或提交中暴露凭据。
|
||||
- 保留 Windows、macOS、Linux 的 x64 和 arm64 发布能力。
|
||||
|
||||
### 4.2 首版非目标
|
||||
@@ -153,7 +153,7 @@ Electron utilityProcess
|
||||
└─ 最小工具集
|
||||
│ HTTPS
|
||||
▼
|
||||
用户选择的 DeepSeek 兼容模型连接
|
||||
用户选择的 OpenAI 兼容模型连接
|
||||
```
|
||||
|
||||
### 6.1 信任边界
|
||||
@@ -369,7 +369,7 @@ Windows ACL 和旧 Linux Landlock 可能只报告 `partial`。界面和诊断必
|
||||
|
||||
- Agent、Session、LLM 和 Tool Registry 基础服务。
|
||||
- GoodBuddy Harness Control Plane。
|
||||
- DeepSeek 兼容 LLM 适配器。
|
||||
- OpenAI 兼容 Chat Completions LLM 适配器。
|
||||
- Sandbox Policy 与平台 Sandbox Provider。
|
||||
- 平台对应的受沙箱 Shell。
|
||||
- 受沙箱 Filesystem。
|
||||
@@ -396,11 +396,13 @@ Windows ACL 和旧 Linux Landlock 可能只报告 `partial`。界面和诊断必
|
||||
|
||||
### 12.1 配置来源
|
||||
|
||||
DeepSeek Harness 首版只使用 GoodBuddy 模型连接:
|
||||
DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
|
||||
- 协议必须是 `openai-chat-completions`。
|
||||
- 认证必须是 API Key。
|
||||
- 服务地址必须是 `https://api.deepseek.com`,且不得包含用户信息。
|
||||
- 公网服务地址必须使用 HTTPS;`localhost`、`127.0.0.1` 和 `::1` 回环地址可以使用 HTTP。
|
||||
- 服务地址可以使用自定义主机、端口和部署路径,但不得包含用户名、密码、查询参数或片段。
|
||||
- 模型名称不限制为 DeepSeek 品牌,由所选 OpenAI 兼容服务决定。
|
||||
- 模型名称和服务地址由 Main 传入受控 Host。
|
||||
- API Key 继续保存在 GoodBuddy 加密设置中。
|
||||
- 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。
|
||||
@@ -458,7 +460,7 @@ Runtime 的概览、模型配置和检测信息放在同一张详情卡中。当
|
||||
|
||||
```text
|
||||
Runtime: GoodBuddy 内置 DeepSeek Harness
|
||||
模型配置: 跟随 GoodBuddy · dsv4flash(deepseek-v4-flash)
|
||||
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
|
||||
状态: 已就绪
|
||||
路径: <受控 Host 路径>
|
||||
版本: 0.1.0-rc.6
|
||||
@@ -528,6 +530,7 @@ Renderer 只接收脱敏状态。任何凭据、完整环境、启动参数或
|
||||
- 确认这些模块不会被加载。
|
||||
- 评估它们带来的 audit 和体积风险。
|
||||
- 在后续上游版本允许时改为最小包族。
|
||||
- 确认 `tests/fixtures` 以及 Web3D 测试 Skill/MCP 不进入正式发布资源。
|
||||
|
||||
### 16.4 漏洞门禁
|
||||
|
||||
@@ -549,6 +552,7 @@ Renderer 只接收脱敏状态。任何凭据、完整环境、启动参数或
|
||||
- 平台原生 Sandbox/PTY 依赖架构正确。
|
||||
- Harness、ACP SDK 和其他新增第三方许可证已打包。
|
||||
- `app.asar` 外需要执行或动态加载的资源位于预期目录。
|
||||
- Web3D Skill/MCP 等测试 fixture 不在 `app.asar` 或 `extraResources` 中。
|
||||
|
||||
## 17. 测试策略
|
||||
|
||||
@@ -582,7 +586,7 @@ Renderer 只接收脱敏状态。任何凭据、完整环境、启动参数或
|
||||
|
||||
### 17.3 真实模型测试
|
||||
|
||||
真实测试已经获得用户授权,但必须由显式环境门禁启用。至少验证:
|
||||
真实测试已经获得用户授权,但必须由显式环境门禁启用。Web3D Skill 和 MCP 仅作为 `tests/fixtures` 下的测试资产使用,不属于内置发布能力。至少验证:
|
||||
|
||||
1. 文本问答成功,并记录正确 Runtime 和模型用量。
|
||||
2. Ask 可以读取工作区,但写入被拒绝,且不会弹出权限对话框。
|
||||
@@ -625,7 +629,7 @@ npm run build
|
||||
- 用户 DSH 配置、`.env`、遥测和 Web UI 未被加载。
|
||||
- API Key 不进入 Renderer、配置文件、日志、错误文本或测试产物。
|
||||
- 全量测试、类型检查、Lint 和生产构建通过。
|
||||
- 真实 DeepSeek 请求成功。
|
||||
- 真实 OpenAI 兼容 Chat Completions 请求成功。
|
||||
- 真实请求调用已分配 Skill 和 MCP,并生成、启动和实际游玩一个可用的 3D 游戏项目。
|
||||
- 新增第三方许可证和发布校验完整。
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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: '管理员预置模型'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -63,12 +63,8 @@ describe('bundled skills', () => {
|
||||
expect(snapshot.skills.map((skill) => skill.id)).toContain(
|
||||
'product-marketing'
|
||||
)
|
||||
expect(snapshot.skills).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'web-3d-game',
|
||||
name: 'Web 3D Game',
|
||||
assignments: expect.arrayContaining(['deepseek-harness'])
|
||||
})
|
||||
expect(snapshot.skills.map((skill) => skill.id)).not.toContain(
|
||||
'web-3d-game'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -84,7 +80,7 @@ describe('bundled skills', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('exposes the 3D game Skill as a native Harness package', async () => {
|
||||
it('exposes bundled Skills as native Harness packages', async () => {
|
||||
const service = await createService()
|
||||
|
||||
await expect(
|
||||
@@ -92,8 +88,8 @@ describe('bundled skills', () => {
|
||||
).resolves.toMatchObject({
|
||||
packages: expect.arrayContaining([
|
||||
{
|
||||
id: 'web-3d-game',
|
||||
directory: join(builtinSkillsRoot, 'web-3d-game')
|
||||
id: 'product-marketing',
|
||||
directory: join(builtinSkillsRoot, 'product-marketing')
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
@@ -12,9 +12,12 @@ import {
|
||||
startControlledDeepSeekHarnessHost,
|
||||
type ControlledHarnessHost
|
||||
} from './deepseek-harness-host'
|
||||
import { installHarnessChildProcessWindowGuard } from './agent/deepseek-harness-child-process'
|
||||
|
||||
const parentPort = process.parentPort
|
||||
const restoreDiagnostics = installHarnessDiagnosticGuard()
|
||||
const restoreChildProcessWindowGuard =
|
||||
installHarnessChildProcessWindowGuard()
|
||||
// The Windows ACL sandbox launches its JavaScript runner through
|
||||
// `process.execPath`. Inside an Electron UtilityProcess that path is Electron,
|
||||
// so descendants must opt into Electron's supported Node execution mode.
|
||||
@@ -41,6 +44,7 @@ async function close(): Promise<void> {
|
||||
closed = true
|
||||
await host?.dispose().catch(() => undefined)
|
||||
transport?.dispose()
|
||||
restoreChildProcessWindowGuard()
|
||||
restoreDiagnostics()
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('controlled DeepSeek Harness host', () => {
|
||||
dshHome: 'C:\\controlled-dsh-home',
|
||||
skillPackages: []
|
||||
})
|
||||
).rejects.toThrow('trusted HTTPS DeepSeek endpoint')
|
||||
).rejects.toThrow('secure OpenAI-compatible')
|
||||
})
|
||||
|
||||
it('suppresses console payloads instead of contaminating stdout', () => {
|
||||
@@ -84,10 +84,10 @@ describe('controlled DeepSeek Harness host', () => {
|
||||
const host = await startControlledDeepSeekHarnessHost({
|
||||
workspace: root,
|
||||
dshHome: root,
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
baseUrl: 'https://gateway.example/openai/v1',
|
||||
api: 'openai-completions',
|
||||
provider: 'goodbuddy',
|
||||
model: 'deepseek-test',
|
||||
model: 'qwen-plus',
|
||||
harnessVersion: '0.1.0-rc.6',
|
||||
sandbox: expectedSandbox,
|
||||
credentialRefs: ['GOODBUDDY_API_KEY'],
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from './agent/goodbuddy-harness-control-plane'
|
||||
import type { Stream } from '@agentclientprotocol/sdk'
|
||||
import type { SandboxEnforcement } from '@deepseek-ai/dsh-sandbox'
|
||||
import { isDeepSeekHarnessCompatibleBaseUrl } from '../shared/deepseek-harness-compatibility'
|
||||
|
||||
const DEFAULT_MAX_FRAME_BYTES = 1024 * 1024
|
||||
const MAX_DIAGNOSTIC_BYTES = 64 * 1024
|
||||
@@ -133,19 +134,12 @@ type PluginSpec = {
|
||||
function validateHostConfig(
|
||||
config: ControlledHarnessHostConfig
|
||||
): void {
|
||||
const endpoint = URL.canParse(config.baseUrl)
|
||||
? new URL(config.baseUrl)
|
||||
: undefined
|
||||
if (
|
||||
config.api !== 'openai-completions' ||
|
||||
!endpoint ||
|
||||
endpoint.protocol !== 'https:' ||
|
||||
endpoint.hostname.toLowerCase() !== 'api.deepseek.com' ||
|
||||
endpoint.username ||
|
||||
endpoint.password
|
||||
!isDeepSeekHarnessCompatibleBaseUrl(config.baseUrl)
|
||||
) {
|
||||
throw new Error(
|
||||
'Controlled Harness requires the trusted HTTPS DeepSeek endpoint'
|
||||
'Controlled Harness requires a secure OpenAI-compatible Chat Completions endpoint'
|
||||
)
|
||||
}
|
||||
if (!config.credentialRefs.length) {
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('RuntimeSettingsStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('migrates DeepSeek Harness to controlled platform mode and stores an official profile', async () => {
|
||||
it('migrates DeepSeek Harness to controlled platform mode and stores a compatible profile', async () => {
|
||||
const { filePath, store } = await createStore()
|
||||
await store.update(settings())
|
||||
const versionFourteen = JSON.parse(
|
||||
@@ -127,9 +127,9 @@ describe('RuntimeSettingsStore', () => {
|
||||
modelProfiles: [
|
||||
{
|
||||
id: profileId,
|
||||
name: 'DeepSeek',
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
modelName: 'deepseek-chat',
|
||||
name: 'OpenAI-compatible gateway',
|
||||
baseUrl: 'https://gateway.example/openai/v1',
|
||||
modelName: 'qwen-plus',
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'api-key',
|
||||
imageGenerationQuality: 'auto',
|
||||
@@ -149,21 +149,22 @@ describe('RuntimeSettingsStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves a controlled platform DeepSeek profile without exposing its credential', async () => {
|
||||
const apiKey = 'platform-deepseek-secret'
|
||||
it('resolves a controlled platform Harness profile without exposing its credential', async () => {
|
||||
const apiKey = 'platform-harness-secret'
|
||||
const { store } = await createStore({
|
||||
GOODBUDDY_MODEL_API_KEY: apiKey,
|
||||
GOODBUDDY_MODEL_BASE_URL: 'https://api.deepseek.com/',
|
||||
GOODBUDDY_MODEL_NAME: 'deepseek-v4-flash'
|
||||
GOODBUDDY_MODEL_BASE_URL:
|
||||
'https://gateway.example/openai/v1',
|
||||
GOODBUDDY_MODEL_NAME: 'qwen-plus'
|
||||
})
|
||||
|
||||
await expect(store.getResolvedSettings()).resolves.toMatchObject({
|
||||
modelProtocol: 'anthropic-messages',
|
||||
deepseekHarnessModelProfile: {
|
||||
id: 'goodbuddy-platform-deepseek',
|
||||
name: '平台 DeepSeek',
|
||||
baseUrl: 'https://api.deepseek.com/',
|
||||
modelName: 'deepseek-v4-flash',
|
||||
id: 'goodbuddy-platform-harness',
|
||||
name: '管理员预置模型',
|
||||
baseUrl: 'https://gateway.example/openai/v1',
|
||||
modelName: 'qwen-plus',
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'api-key',
|
||||
supportsImageInput: false,
|
||||
@@ -179,37 +180,39 @@ describe('RuntimeSettingsStore', () => {
|
||||
|
||||
it.each([
|
||||
[
|
||||
'a non-DeepSeek endpoint',
|
||||
'an insecure public endpoint',
|
||||
{
|
||||
GOODBUDDY_MODEL_API_KEY: 'platform-key',
|
||||
GOODBUDDY_MODEL_BASE_URL: 'https://deepseek.example',
|
||||
GOODBUDDY_MODEL_NAME: 'deepseek-chat'
|
||||
GOODBUDDY_MODEL_BASE_URL: 'http://gateway.example/v1',
|
||||
GOODBUDDY_MODEL_NAME: 'qwen-plus'
|
||||
}
|
||||
],
|
||||
[
|
||||
'an insecure DeepSeek endpoint',
|
||||
'an endpoint with embedded credentials',
|
||||
{
|
||||
GOODBUDDY_MODEL_API_KEY: 'platform-key',
|
||||
GOODBUDDY_MODEL_BASE_URL: 'http://api.deepseek.com',
|
||||
GOODBUDDY_MODEL_NAME: 'deepseek-chat'
|
||||
GOODBUDDY_MODEL_BASE_URL:
|
||||
'https://user:secret@gateway.example/v1',
|
||||
GOODBUDDY_MODEL_NAME: 'qwen-plus'
|
||||
}
|
||||
],
|
||||
[
|
||||
'a DeepSeek endpoint path',
|
||||
'an endpoint with a query string',
|
||||
{
|
||||
GOODBUDDY_MODEL_API_KEY: 'platform-key',
|
||||
GOODBUDDY_MODEL_BASE_URL: 'https://api.deepseek.com/v1',
|
||||
GOODBUDDY_MODEL_NAME: 'deepseek-chat'
|
||||
GOODBUDDY_MODEL_BASE_URL:
|
||||
'https://gateway.example/v1?api-version=1',
|
||||
GOODBUDDY_MODEL_NAME: 'qwen-plus'
|
||||
}
|
||||
],
|
||||
[
|
||||
'a missing API key',
|
||||
{
|
||||
GOODBUDDY_MODEL_BASE_URL: 'https://api.deepseek.com',
|
||||
GOODBUDDY_MODEL_NAME: 'deepseek-chat'
|
||||
GOODBUDDY_MODEL_BASE_URL: 'https://gateway.example/v1',
|
||||
GOODBUDDY_MODEL_NAME: 'qwen-plus'
|
||||
}
|
||||
]
|
||||
])('does not resolve platform DeepSeek from %s', async (_, environment) => {
|
||||
])('does not resolve platform Harness from %s', async (_, environment) => {
|
||||
const { store } = await createStore(environment)
|
||||
|
||||
await expect(store.getResolvedSettings()).resolves.toMatchObject({
|
||||
@@ -253,7 +256,7 @@ describe('RuntimeSettingsStore', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects incompatible DeepSeek Harness model profiles', () => {
|
||||
it('accepts compatible gateways and rejects incompatible Harness profiles', () => {
|
||||
const profileId = '00000000-0000-4000-8000-000000000045'
|
||||
expect(
|
||||
runtimeSettingsInputSchema.safeParse(
|
||||
@@ -261,9 +264,9 @@ describe('RuntimeSettingsStore', () => {
|
||||
modelProfiles: [
|
||||
{
|
||||
id: profileId,
|
||||
name: 'Other compatible API',
|
||||
name: 'Compatible API',
|
||||
baseUrl: 'https://other.example/v1',
|
||||
modelName: 'deepseek-chat',
|
||||
modelName: 'qwen-plus',
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'api-key',
|
||||
imageGenerationQuality: 'auto',
|
||||
@@ -274,16 +277,16 @@ describe('RuntimeSettingsStore', () => {
|
||||
deepseekHarnessModelSource: { kind: 'profile', profileId }
|
||||
})
|
||||
).success
|
||||
).toBe(false)
|
||||
).toBe(true)
|
||||
expect(
|
||||
runtimeSettingsInputSchema.safeParse(
|
||||
settings({
|
||||
modelProfiles: [
|
||||
{
|
||||
id: profileId,
|
||||
name: 'DeepSeek without API key',
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
modelName: 'deepseek-chat',
|
||||
name: 'Gateway without API key',
|
||||
baseUrl: 'https://other.example/v1',
|
||||
modelName: 'qwen-plus',
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'none',
|
||||
imageGenerationQuality: 'auto',
|
||||
|
||||
@@ -263,7 +263,7 @@ const embeddingCredentialPayloadSchema = z.object({
|
||||
})
|
||||
|
||||
const rerankCredentialPayloadSchema = embeddingCredentialPayloadSchema
|
||||
const platformDeepSeekProfileId = 'goodbuddy-platform-deepseek'
|
||||
const platformHarnessProfileId = 'goodbuddy-platform-harness'
|
||||
|
||||
export type CredentialCipher = SettingsCredentialCipher
|
||||
|
||||
@@ -1041,33 +1041,25 @@ export class RuntimeSettingsStore {
|
||||
)
|
||||
}
|
||||
|
||||
private resolvePlatformDeepSeekProfile(): ResolvedModelProfile | undefined {
|
||||
private resolvePlatformHarnessProfile(): ResolvedModelProfile | undefined {
|
||||
const apiKey = this.environment.GOODBUDDY_MODEL_API_KEY?.trim()
|
||||
const baseUrl = this.environment.GOODBUDDY_MODEL_BASE_URL?.trim()
|
||||
const modelName = this.environment.GOODBUDDY_MODEL_NAME?.trim()
|
||||
if (!apiKey || !baseUrl || !modelName) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
const endpoint = new URL(baseUrl)
|
||||
if (
|
||||
endpoint.protocol !== 'https:' ||
|
||||
endpoint.hostname.toLowerCase() !== 'api.deepseek.com' ||
|
||||
endpoint.port ||
|
||||
endpoint.pathname !== '/' ||
|
||||
endpoint.search ||
|
||||
endpoint.hash ||
|
||||
endpoint.username ||
|
||||
endpoint.password
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
} catch {
|
||||
if (
|
||||
!isDeepSeekHarnessModelProfile({
|
||||
baseUrl,
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'api-key'
|
||||
})
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
id: platformDeepSeekProfileId,
|
||||
name: '平台 DeepSeek',
|
||||
id: platformHarnessProfileId,
|
||||
name: '管理员预置模型',
|
||||
baseUrl,
|
||||
modelName,
|
||||
protocol: 'openai-chat-completions',
|
||||
@@ -1406,7 +1398,7 @@ export class RuntimeSettingsStore {
|
||||
? profilesById.get(
|
||||
settings.deepseekHarnessModelSource.profileId
|
||||
)
|
||||
: this.resolvePlatformDeepSeekProfile()
|
||||
: this.resolvePlatformHarnessProfile()
|
||||
return {
|
||||
provider: settings.provider,
|
||||
modelBaseUrl: effective.baseUrl,
|
||||
@@ -1726,7 +1718,7 @@ export class RuntimeSettingsStore {
|
||||
}
|
||||
if (!isDeepSeekHarnessModelProfile(profile)) {
|
||||
throw new Error(
|
||||
'DeepSeek Harness 模型连接仅支持 api.deepseek.com 的 OpenAI Chat Completions 协议'
|
||||
'DeepSeek Harness 仅支持使用 API Key 的安全 OpenAI 兼容 Chat Completions 连接'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +503,7 @@ describe('ChannelSettingsSection', () => {
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
within(backend).getByRole('option', {
|
||||
name: 'DeepSeek Harness(预览 · 仅 DeepSeek)'
|
||||
name: 'DeepSeek Harness(预览 · OpenAI 兼容)'
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
|
||||
|
||||
@@ -1372,7 +1372,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('configures DeepSeek Harness with a compatible GoodBuddy connection', async () => {
|
||||
it('configures DeepSeek Harness with an OpenAI-compatible gateway', async () => {
|
||||
const harnessProfileId =
|
||||
'00000000-0000-4000-8000-000000000051'
|
||||
getRuntime.mockResolvedValueOnce({
|
||||
@@ -1382,9 +1382,9 @@ describe('SettingsPanel runtime files', () => {
|
||||
{
|
||||
...runtimeSettings.modelProfiles[0]!,
|
||||
id: harnessProfileId,
|
||||
name: 'DeepSeek Chat',
|
||||
baseUrl: 'https://api.deepseek.com/v1',
|
||||
modelName: 'deepseek-chat',
|
||||
name: 'Compatible Gateway',
|
||||
baseUrl: 'https://gateway.example/openai/v1',
|
||||
modelName: 'qwen-plus',
|
||||
protocol: 'openai-chat-completions'
|
||||
}
|
||||
],
|
||||
@@ -1409,10 +1409,10 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
)
|
||||
expect(
|
||||
screen.getByText('开发者预览 · 仅支持 DeepSeek')
|
||||
screen.getByText('开发者预览 · OpenAI 兼容')
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(/当前仅支持 DeepSeek 模型/)
|
||||
screen.getByText(/公网地址必须使用 HTTPS/)
|
||||
).toBeInTheDocument()
|
||||
const harnessOverview = screen
|
||||
.getByText('GoodBuddy 内置 DeepSeek Harness')
|
||||
@@ -1451,17 +1451,19 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
).not.toBeInTheDocument()
|
||||
const source = screen.getByLabelText(
|
||||
'DeepSeek Harness DeepSeek 模型连接'
|
||||
'DeepSeek Harness OpenAI 兼容模型连接'
|
||||
)
|
||||
expect(screen.queryByRole('radio')).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('使用平台 DeepSeek 环境配置')
|
||||
screen.queryByText('使用管理员预置模型连接')
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
within(source).getByRole('option', { name: '默认模型(不兼容)' })
|
||||
).toBeDisabled()
|
||||
expect(
|
||||
within(source).getByRole('option', { name: 'DeepSeek Chat' })
|
||||
within(source).getByRole('option', {
|
||||
name: 'Compatible Gateway'
|
||||
})
|
||||
).not.toBeDisabled()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
|
||||
@@ -1480,14 +1482,14 @@ describe('SettingsPanel runtime files', () => {
|
||||
it('keeps an environment-managed source compatible without exposing it as an option', async () => {
|
||||
getRuntime.mockResolvedValueOnce({
|
||||
...runtimeSettings,
|
||||
modelBaseUrl: 'https://api.deepseek.com',
|
||||
modelName: 'deepseek-chat',
|
||||
modelBaseUrl: 'https://gateway.example/openai/v1',
|
||||
modelName: 'qwen-plus',
|
||||
modelProtocol: 'openai-chat-completions',
|
||||
modelProfiles: [
|
||||
{
|
||||
...runtimeSettings.modelProfiles[0]!,
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
modelName: 'deepseek-chat',
|
||||
baseUrl: 'https://gateway.example/openai/v1',
|
||||
modelName: 'qwen-plus',
|
||||
protocol: 'openai-chat-completions',
|
||||
credentialSource: 'environment'
|
||||
}
|
||||
@@ -1531,10 +1533,10 @@ describe('SettingsPanel runtime files', () => {
|
||||
)
|
||||
expect(screen.queryByRole('radio')).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('管理员预置的 DeepSeek 连接')
|
||||
screen.getByText('管理员预置的 OpenAI 兼容连接')
|
||||
).toBeInTheDocument()
|
||||
const source = screen.getByLabelText(
|
||||
'DeepSeek Harness DeepSeek 模型连接'
|
||||
'DeepSeek Harness OpenAI 兼容模型连接'
|
||||
)
|
||||
expect(source).toHaveValue('')
|
||||
fireEvent.change(source, {
|
||||
|
||||
@@ -1973,7 +1973,7 @@ export function SettingsPanel({
|
||||
</div>
|
||||
</div>
|
||||
<p className="settings-notice">
|
||||
{t('runtime.deepseekHarness.deepseekOnlyNotice')}
|
||||
{t('runtime.deepseekHarness.compatibilityNotice')}
|
||||
</p>
|
||||
<RuntimeOverviewCard
|
||||
detection={detection?.deepseekHarness}
|
||||
|
||||
@@ -118,7 +118,7 @@ export const app = {
|
||||
picker: 'Runtime and model',
|
||||
directModels: 'Direct models',
|
||||
deepseekHarnessGroup:
|
||||
'DeepSeek Harness (Developer preview · DeepSeek only)',
|
||||
'DeepSeek Harness (Developer preview · OpenAI-compatible)',
|
||||
manage: 'Manage Runtime and model connections',
|
||||
errors: {
|
||||
readStatus: 'Failed to read Agent Runtime status',
|
||||
|
||||
@@ -32,7 +32,7 @@ export const integrations = {
|
||||
missingProfile: 'The previous direct model no longer exists',
|
||||
noTextModels: 'No text models are available',
|
||||
deepseekHarnessOption:
|
||||
'DeepSeek Harness (Preview · DeepSeek only)',
|
||||
'DeepSeek Harness (Preview · OpenAI-compatible)',
|
||||
missingSelection:
|
||||
'The selected direct model no longer exists. Choose another model.',
|
||||
imageOnlySelection:
|
||||
|
||||
@@ -237,16 +237,18 @@ export const settings = {
|
||||
deepseekHarness: {
|
||||
selectorLabel: 'DeepSeek Harness (Preview)',
|
||||
title: 'DeepSeek Harness',
|
||||
previewDescription: 'Developer preview · DeepSeek only',
|
||||
deepseekOnlyNotice:
|
||||
'DeepSeek Harness currently supports DeepSeek models only and is not intended for other model providers.',
|
||||
previewDescription: 'Developer preview · OpenAI-compatible',
|
||||
compatibilityNotice:
|
||||
'Supports OpenAI-compatible Chat Completions connections with an API key. Public endpoints must use HTTPS; loopback endpoints may use HTTP.',
|
||||
description:
|
||||
'GoodBuddy maintains the fixed Host and control protocol internally and uses pinned Harness libraries underneath. Execute tool calls receive automatic one-time authorization, Ask remains read-only, and cancellation and workspace safety boundaries remain in place. It does not integrate with the DSH plugin or marketplace mechanisms.',
|
||||
managedSource: 'Administrator-provided DeepSeek connection',
|
||||
connection: 'DeepSeek model connection',
|
||||
connectionPlaceholder: 'Select a DeepSeek model connection',
|
||||
managedSource:
|
||||
'Administrator-provided OpenAI-compatible connection',
|
||||
connection: 'OpenAI-compatible model connection',
|
||||
connectionPlaceholder:
|
||||
'Select an OpenAI-compatible model connection',
|
||||
connectionDescription:
|
||||
'Choose a GoodBuddy model connection. Only OpenAI-compatible Chat Completions connections that point to DeepSeek are supported.',
|
||||
'Choose a GoodBuddy model connection. It must use OpenAI Chat Completions with API-key authentication.',
|
||||
advancedDescription:
|
||||
'This Runtime always uses GoodBuddy’s bundled, version-pinned Host. It does not load external DSH plugins, marketplace packages, user profiles, or custom Hosts.'
|
||||
}
|
||||
@@ -504,7 +506,7 @@ export const settings = {
|
||||
incompatibleImageProtocol:
|
||||
'Incompatible (image generation protocol is unsupported)',
|
||||
incompatibleHarnessProtocol:
|
||||
'Incompatible (Chat Completions only)',
|
||||
'Incompatible (requires Chat Completions, an API key, and a secure endpoint)',
|
||||
secureStorageWarning:
|
||||
'Secure system key storage is unavailable. Use an environment variable to avoid storing an API Key in plaintext.'
|
||||
},
|
||||
|
||||
@@ -113,7 +113,8 @@ export const app = {
|
||||
switching: '切换中…',
|
||||
picker: 'Runtime 和模型',
|
||||
directModels: '直连模型',
|
||||
deepseekHarnessGroup: 'DeepSeek Harness(开发者预览 · 仅 DeepSeek)',
|
||||
deepseekHarnessGroup:
|
||||
'DeepSeek Harness(开发者预览 · OpenAI 兼容)',
|
||||
manage: '管理 Runtime 和模型连接',
|
||||
errors: {
|
||||
readStatus: 'Agent Runtime 状态读取失败',
|
||||
|
||||
@@ -26,7 +26,8 @@ export const integrations = {
|
||||
unavailableProfile: '{{name}} · {{modelName}}(不可用)',
|
||||
missingProfile: '原直连模型已不存在',
|
||||
noTextModels: '暂无可用文本模型',
|
||||
deepseekHarnessOption: 'DeepSeek Harness(预览 · 仅 DeepSeek)',
|
||||
deepseekHarnessOption:
|
||||
'DeepSeek Harness(预览 · OpenAI 兼容)',
|
||||
missingSelection: '所选直连模型已不存在,请重新选择。',
|
||||
imageOnlySelection:
|
||||
'所选连接仅支持图片生成,请选择文本模型或 Agent Runtime。',
|
||||
|
||||
@@ -215,16 +215,16 @@ export const settings = {
|
||||
deepseekHarness: {
|
||||
selectorLabel: 'DeepSeek Harness(预览)',
|
||||
title: 'DeepSeek Harness',
|
||||
previewDescription: '开发者预览 · 仅支持 DeepSeek',
|
||||
deepseekOnlyNotice:
|
||||
'DeepSeek Harness 当前仅支持 DeepSeek 模型,不适用于其他模型提供商。',
|
||||
previewDescription: '开发者预览 · OpenAI 兼容',
|
||||
compatibilityNotice:
|
||||
'支持使用 API Key 的 OpenAI 兼容 Chat Completions 连接;公网地址必须使用 HTTPS,本机回环地址可使用 HTTP。',
|
||||
description:
|
||||
'由 GoodBuddy 内部维护固定 Host 与控制协议,复用锁定的 Harness 底层库;Execute 工具调用自动单次授权,Ask 保持只读,并保留取消和工作区安全边界;不接入 DSH 插件或市场机制。',
|
||||
managedSource: '管理员预置的 DeepSeek 连接',
|
||||
connection: 'DeepSeek 模型连接',
|
||||
connectionPlaceholder: '选择 DeepSeek 模型连接',
|
||||
managedSource: '管理员预置的 OpenAI 兼容连接',
|
||||
connection: 'OpenAI 兼容模型连接',
|
||||
connectionPlaceholder: '选择 OpenAI 兼容模型连接',
|
||||
connectionDescription:
|
||||
'从 GoodBuddy 模型连接中选择;仅支持指向 DeepSeek 的 OpenAI 兼容 Chat Completions 连接。',
|
||||
'从 GoodBuddy 模型连接中选择;协议必须为 OpenAI Chat Completions,并使用 API Key。',
|
||||
advancedDescription:
|
||||
'该 Runtime 始终使用 GoodBuddy 内置并固定版本的 Host,不加载外部 DSH 插件、市场包、用户 profile 或自定义 Host。'
|
||||
}
|
||||
@@ -462,7 +462,7 @@ export const settings = {
|
||||
incompatible: '不兼容',
|
||||
incompatibleImageProtocol: '不兼容(不支持图像生成协议)',
|
||||
incompatibleHarnessProtocol:
|
||||
'不兼容(仅支持 Chat Completions)',
|
||||
'不兼容(需 Chat Completions、API Key 和安全地址)',
|
||||
secureStorageWarning:
|
||||
'当前系统密钥服务不可用。为了避免明文落盘,请使用环境变量提供 API Key。'
|
||||
},
|
||||
|
||||
+6
-21
@@ -121,6 +121,11 @@ import {
|
||||
agentRuntimeSelectionSchema,
|
||||
type AgentRuntimeSelection
|
||||
} from './runtime-selection-contracts'
|
||||
import { isDeepSeekHarnessModelProfile } from './deepseek-harness-compatibility'
|
||||
export {
|
||||
isDeepSeekHarnessCompatibleBaseUrl,
|
||||
isDeepSeekHarnessModelProfile
|
||||
} from './deepseek-harness-compatibility'
|
||||
|
||||
export const workspaceRelativePathSchema = z
|
||||
.string()
|
||||
@@ -392,26 +397,6 @@ export const runtimeModelSourceSchema = z.discriminatedUnion('kind', [
|
||||
.strict()
|
||||
])
|
||||
|
||||
export function isDeepSeekHarnessModelProfile(
|
||||
profile: Pick<
|
||||
ModelConnectionSettings,
|
||||
'baseUrl' | 'protocol' | 'authentication'
|
||||
>
|
||||
): boolean {
|
||||
if (
|
||||
profile.protocol !== 'openai-chat-completions' ||
|
||||
profile.authentication !== 'api-key'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return new URL(profile.baseUrl).hostname.toLowerCase() ===
|
||||
'api.deepseek.com'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const runtimeSettingsInputSchema = z
|
||||
.object({
|
||||
provider: runtimeProviderSchema,
|
||||
@@ -590,7 +575,7 @@ export const runtimeSettingsInputSchema = z
|
||||
code: 'custom',
|
||||
path: ['deepseekHarnessModelSource'],
|
||||
message:
|
||||
'DeepSeek Harness 独立模型连接仅支持 api.deepseek.com 的 OpenAI Chat Completions 协议'
|
||||
'DeepSeek Harness 仅支持使用 API Key 的安全 OpenAI 兼容 Chat Completions 连接'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isDeepSeekHarnessCompatibleBaseUrl,
|
||||
isDeepSeekHarnessModelProfile
|
||||
} from './deepseek-harness-compatibility'
|
||||
|
||||
describe('DeepSeek Harness model compatibility', () => {
|
||||
it.each([
|
||||
'https://api.deepseek.com',
|
||||
'https://gateway.example/openai/v1',
|
||||
'https://gateway.example:8443/v1',
|
||||
'http://127.0.0.1:11434/v1',
|
||||
'http://localhost:1234/v1',
|
||||
'http://[::1]:8080/v1'
|
||||
])('accepts OpenAI-compatible base URL %s', (baseUrl) => {
|
||||
expect(isDeepSeekHarnessCompatibleBaseUrl(baseUrl)).toBe(true)
|
||||
expect(
|
||||
isDeepSeekHarnessModelProfile({
|
||||
baseUrl,
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'api-key'
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'http://gateway.example/v1',
|
||||
'file:///private/config',
|
||||
'https://user:secret@gateway.example/v1',
|
||||
'https://gateway.example/v1?api-version=2025-01-01',
|
||||
'https://gateway.example/v1#chat'
|
||||
])('rejects unsafe or unsupported base URL %s', (baseUrl) => {
|
||||
expect(isDeepSeekHarnessCompatibleBaseUrl(baseUrl)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects other protocols and unauthenticated profiles', () => {
|
||||
expect(
|
||||
isDeepSeekHarnessModelProfile({
|
||||
baseUrl: 'https://gateway.example/v1',
|
||||
protocol: 'openai-responses',
|
||||
authentication: 'api-key'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isDeepSeekHarnessModelProfile({
|
||||
baseUrl: 'https://gateway.example/v1',
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'none'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
type DeepSeekHarnessModelProfile = {
|
||||
baseUrl: string
|
||||
protocol: string
|
||||
authentication: string
|
||||
}
|
||||
|
||||
function isLoopbackHostname(hostname: string): boolean {
|
||||
const normalized = hostname
|
||||
.toLowerCase()
|
||||
.replace(/^\[|\]$/gu, '')
|
||||
return (
|
||||
normalized === 'localhost' ||
|
||||
normalized === '127.0.0.1' ||
|
||||
normalized === '::1'
|
||||
)
|
||||
}
|
||||
|
||||
export function isDeepSeekHarnessCompatibleBaseUrl(
|
||||
value: string
|
||||
): boolean {
|
||||
try {
|
||||
const url = new URL(value)
|
||||
if (
|
||||
!url.hostname ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
url.search ||
|
||||
url.hash
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
url.protocol === 'https:' ||
|
||||
(url.protocol === 'http:' &&
|
||||
isLoopbackHostname(url.hostname))
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function isDeepSeekHarnessModelProfile(
|
||||
profile: DeepSeekHarnessModelProfile
|
||||
): boolean {
|
||||
return (
|
||||
profile.protocol === 'openai-chat-completions' &&
|
||||
profile.authentication === 'api-key' &&
|
||||
isDeepSeekHarnessCompatibleBaseUrl(profile.baseUrl)
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
import { isDeepSeekHarnessModelProfile } from './deepseek-harness-compatibility'
|
||||
|
||||
const runtimeSelectionProfileIdSchema = z.string().uuid()
|
||||
|
||||
@@ -82,12 +83,11 @@ function isDeepSeekHarnessRepairProfileUsable(
|
||||
) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return new URL(profile.baseUrl).hostname.toLowerCase() ===
|
||||
'api.deepseek.com'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return isDeepSeekHarnessModelProfile({
|
||||
baseUrl: profile.baseUrl,
|
||||
protocol: profile.protocol,
|
||||
authentication: profile.authentication
|
||||
})
|
||||
}
|
||||
|
||||
export function repairChannelRuntimeSelection(
|
||||
|
||||
@@ -295,6 +295,43 @@ describe('release build arguments', () => {
|
||||
'@koromix/koffi-win32-x64': '3.1.4'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps Web3D test fixtures out of release resources', () => {
|
||||
const packageJson = require('../package.json') as {
|
||||
build: {
|
||||
files: string[]
|
||||
extraResources: Array<{ from: string }>
|
||||
}
|
||||
}
|
||||
const resourceSources = packageJson.build.extraResources.map(
|
||||
(entry) => entry.from
|
||||
)
|
||||
|
||||
expect(packageJson.build.files).toEqual([
|
||||
'out/**/*',
|
||||
'package.json'
|
||||
])
|
||||
expect(resourceSources).toContain('resources/skills')
|
||||
expect(
|
||||
resourceSources.some((source) => source.startsWith('tests/'))
|
||||
).toBe(false)
|
||||
expect(
|
||||
existsSync(join('resources', 'skills', 'web-3d-game'))
|
||||
).toBe(false)
|
||||
expect(
|
||||
existsSync(join('resources', 'web-3d-game-mcp.mjs'))
|
||||
).toBe(false)
|
||||
expect(
|
||||
existsSync(
|
||||
join(
|
||||
'tests',
|
||||
'fixtures',
|
||||
'web-3d-game-skill',
|
||||
'SKILL.md'
|
||||
)
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('release binary architecture detection', () => {
|
||||
|
||||
Reference in New Issue
Block a user