From 45aeecb6dd1b6a47fede9fba59c6e6af972743e0 Mon Sep 17 00:00:00 2001 From: lofyer Date: Fri, 14 Aug 2026 12:38:01 +0800 Subject: [PATCH] feat: expand DeepSeek Harness compatibility --- build/deepseek-harness-utility-smoke.cjs | 6 +- docs/deepseek-harness-runtime-design.md | 20 +-- src/main/agent/create-runtime.test.ts | 29 ++++- src/main/agent/create-runtime.ts | 4 +- .../agent/deepseek-harness-acp-e2e.test.ts | 8 +- .../deepseek-harness-child-process.test.ts | 118 ++++++++++++++++++ .../agent/deepseek-harness-child-process.ts | 82 ++++++++++++ .../deepseek-harness-utility-launcher.test.ts | 27 +++- .../deepseek-harness-utility-launcher.ts | 20 +-- src/main/agent/runtime-selection.test.ts | 38 +++--- src/main/agent/runtime-selection.ts | 2 +- src/main/capabilities/builtin-skills.test.ts | 14 +-- src/main/deepseek-harness-host-bootstrap.ts | 4 + src/main/deepseek-harness-host.test.ts | 6 +- src/main/deepseek-harness-host.ts | 12 +- src/main/runtime-settings-store.test.ts | 65 +++++----- src/main/runtime-settings-store.ts | 34 ++--- .../src/ChannelSettingsSection.test.tsx | 2 +- src/renderer/src/SettingsPanel.test.tsx | 32 ++--- src/renderer/src/SettingsPanel.tsx | 2 +- src/renderer/src/i18n/locales/en-US/app.ts | 2 +- .../src/i18n/locales/en-US/integrations.ts | 2 +- .../src/i18n/locales/en-US/settings.ts | 18 +-- src/renderer/src/i18n/locales/zh-CN/app.ts | 3 +- .../src/i18n/locales/zh-CN/integrations.ts | 3 +- .../src/i18n/locales/zh-CN/settings.ts | 16 +-- src/shared/contracts.ts | 27 +--- .../deepseek-harness-compatibility.test.ts | 52 ++++++++ src/shared/deepseek-harness-compatibility.ts | 50 ++++++++ src/shared/runtime-selection-contracts.ts | 12 +- tests/build-release.test.ts | 37 ++++++ .../fixtures/web-3d-game-skill}/SKILL.md | 0 32 files changed, 554 insertions(+), 193 deletions(-) create mode 100644 src/main/agent/deepseek-harness-child-process.test.ts create mode 100644 src/main/agent/deepseek-harness-child-process.ts create mode 100644 src/shared/deepseek-harness-compatibility.test.ts create mode 100644 src/shared/deepseek-harness-compatibility.ts rename {resources/skills/web-3d-game => tests/fixtures/web-3d-game-skill}/SKILL.md (100%) diff --git a/build/deepseek-harness-utility-smoke.cjs b/build/deepseek-harness-utility-smoke.cjs index 4f44e7f..6f248f9 100644 --- a/build/deepseek-harness-utility-smoke.cjs +++ b/build/deepseek-harness-utility-smoke.cjs @@ -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 } diff --git a/docs/deepseek-harness-runtime-design.md b/docs/deepseek-harness-runtime-design.md index 388f15f..0d42521 100644 --- a/docs/deepseek-harness-runtime-design.md +++ b/docs/deepseek-harness-runtime-design.md @@ -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 游戏项目。 - 新增第三方许可证和发布校验完整。 diff --git a/src/main/agent/create-runtime.test.ts b/src/main/agent/create-runtime.test.ts index 13a8de7..926fb54 100644 --- a/src/main/agent/create-runtime.test.ts +++ b/src/main/agent/create-runtime.test.ts @@ -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()) diff --git a/src/main/agent/create-runtime.ts b/src/main/agent/create-runtime.ts index 584d641..e46df02 100644 --- a/src/main/agent/create-runtime.ts +++ b/src/main/agent/create-runtime.ts @@ -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', diff --git a/src/main/agent/deepseek-harness-acp-e2e.test.ts b/src/main/agent/deepseek-harness-acp-e2e.test.ts index 9facb11..ed94541 100644 --- a/src/main/agent/deepseek-harness-acp-e2e.test.ts +++ b/src/main/agent/deepseek-harness-acp-e2e.test.ts @@ -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' ) } ], diff --git a/src/main/agent/deepseek-harness-child-process.test.ts b/src/main/agent/deepseek-harness-child-process.test.ts new file mode 100644 index 0000000..464537e --- /dev/null +++ b/src/main/agent/deepseek-harness-child-process.test.ts @@ -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 + spawn: ReturnType + spawnSync: ReturnType +} { + 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) + }) +}) diff --git a/src/main/agent/deepseek-harness-child-process.ts b/src/main/agent/deepseek-harness-child-process.ts new file mode 100644 index 0000000..a505d4b --- /dev/null +++ b/src/main/agent/deepseek-harness-child-process.ts @@ -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() + } +} diff --git a/src/main/agent/deepseek-harness-utility-launcher.test.ts b/src/main/agent/deepseek-harness-utility-launcher.test.ts index 5311d89..ce325c4 100644 --- a/src/main/agent/deepseek-harness-utility-launcher.test.ts +++ b/src/main/agent/deepseek-harness-utility-launcher.test.ts @@ -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() + }) }) diff --git a/src/main/agent/deepseek-harness-utility-launcher.ts b/src/main/agent/deepseek-harness-utility-launcher.ts index 00de576..51906ea 100644 --- a/src/main/agent/deepseek-harness-utility-launcher.ts +++ b/src/main/agent/deepseek-harness-utility-launcher.ts @@ -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 ( diff --git a/src/main/agent/runtime-selection.test.ts b/src/main/agent/runtime-selection.test.ts index f613997..f5efb6a 100644 --- a/src/main/agent/runtime-selection.test.ts +++ b/src/main/agent/runtime-selection.test.ts @@ -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 = {} @@ -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: '管理员预置模型' } }) diff --git a/src/main/agent/runtime-selection.ts b/src/main/agent/runtime-selection.ts index 5e0874b..754c5c4 100644 --- a/src/main/agent/runtime-selection.ts +++ b/src/main/agent/runtime-selection.ts @@ -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 { diff --git a/src/main/capabilities/builtin-skills.test.ts b/src/main/capabilities/builtin-skills.test.ts index d5a1140..aacab6e 100644 --- a/src/main/capabilities/builtin-skills.test.ts +++ b/src/main/capabilities/builtin-skills.test.ts @@ -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') } ]) }) diff --git a/src/main/deepseek-harness-host-bootstrap.ts b/src/main/deepseek-harness-host-bootstrap.ts index a5c2463..ed675cd 100644 --- a/src/main/deepseek-harness-host-bootstrap.ts +++ b/src/main/deepseek-harness-host-bootstrap.ts @@ -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 { closed = true await host?.dispose().catch(() => undefined) transport?.dispose() + restoreChildProcessWindowGuard() restoreDiagnostics() } diff --git a/src/main/deepseek-harness-host.test.ts b/src/main/deepseek-harness-host.test.ts index 2f9d651..3fd4840 100644 --- a/src/main/deepseek-harness-host.test.ts +++ b/src/main/deepseek-harness-host.test.ts @@ -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'], diff --git a/src/main/deepseek-harness-host.ts b/src/main/deepseek-harness-host.ts index 6f623b2..02fbe23 100644 --- a/src/main/deepseek-harness-host.ts +++ b/src/main/deepseek-harness-host.ts @@ -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) { diff --git a/src/main/runtime-settings-store.test.ts b/src/main/runtime-settings-store.test.ts index d8f3d8f..7cfe585 100644 --- a/src/main/runtime-settings-store.test.ts +++ b/src/main/runtime-settings-store.test.ts @@ -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', diff --git a/src/main/runtime-settings-store.ts b/src/main/runtime-settings-store.ts index 9fe5c37..275c0f5 100644 --- a/src/main/runtime-settings-store.ts +++ b/src/main/runtime-settings-store.ts @@ -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 连接' ) } } diff --git a/src/renderer/src/ChannelSettingsSection.test.tsx b/src/renderer/src/ChannelSettingsSection.test.tsx index 251a546..98542f9 100644 --- a/src/renderer/src/ChannelSettingsSection.test.tsx +++ b/src/renderer/src/ChannelSettingsSection.test.tsx @@ -503,7 +503,7 @@ describe('ChannelSettingsSection', () => { ).toBeInTheDocument() expect( within(backend).getByRole('option', { - name: 'DeepSeek Harness(预览 · 仅 DeepSeek)' + name: 'DeepSeek Harness(预览 · OpenAI 兼容)' }) ).toBeInTheDocument() diff --git a/src/renderer/src/SettingsPanel.test.tsx b/src/renderer/src/SettingsPanel.test.tsx index a73fc9b..21faeed 100644 --- a/src/renderer/src/SettingsPanel.test.tsx +++ b/src/renderer/src/SettingsPanel.test.tsx @@ -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, { diff --git a/src/renderer/src/SettingsPanel.tsx b/src/renderer/src/SettingsPanel.tsx index 0eef3c6..130121e 100644 --- a/src/renderer/src/SettingsPanel.tsx +++ b/src/renderer/src/SettingsPanel.tsx @@ -1973,7 +1973,7 @@ export function SettingsPanel({

- {t('runtime.deepseekHarness.deepseekOnlyNotice')} + {t('runtime.deepseekHarness.compatibilityNotice')}

-): 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 连接' }) } } diff --git a/src/shared/deepseek-harness-compatibility.test.ts b/src/shared/deepseek-harness-compatibility.test.ts new file mode 100644 index 0000000..0ac3262 --- /dev/null +++ b/src/shared/deepseek-harness-compatibility.test.ts @@ -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) + }) +}) diff --git a/src/shared/deepseek-harness-compatibility.ts b/src/shared/deepseek-harness-compatibility.ts new file mode 100644 index 0000000..05a18e5 --- /dev/null +++ b/src/shared/deepseek-harness-compatibility.ts @@ -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) + ) +} diff --git a/src/shared/runtime-selection-contracts.ts b/src/shared/runtime-selection-contracts.ts index 8157703..18bc318 100644 --- a/src/shared/runtime-selection-contracts.ts +++ b/src/shared/runtime-selection-contracts.ts @@ -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( diff --git a/tests/build-release.test.ts b/tests/build-release.test.ts index 2182116..beb9f00 100644 --- a/tests/build-release.test.ts +++ b/tests/build-release.test.ts @@ -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', () => { diff --git a/resources/skills/web-3d-game/SKILL.md b/tests/fixtures/web-3d-game-skill/SKILL.md similarity index 100% rename from resources/skills/web-3d-game/SKILL.md rename to tests/fixtures/web-3d-game-skill/SKILL.md