fix: allow HTTP for configurable service endpoints

DeepSeek Harness and Weixin binding configuration rejected non-loopback HTTP endpoints even when users explicitly selected those internal services. Configurable endpoints now accept valid HTTP and HTTPS URLs without host-location restrictions, while retaining the required protocol and API-key checks.

Release note: 模型连接、DeepSeek Harness 与微信绑定现在可直接使用局域网或公网 HTTP/HTTPS 服务地址,不再强制公网 HTTPS 或本机回环地址。
This commit is contained in:
mesalogo
2026-08-30 16:49:47 +08:00
parent 6dc219f3a3
commit 7c6f15d3c0
18 changed files with 42 additions and 75 deletions
@@ -163,7 +163,7 @@ Electron utilityProcess
├─ 本地 Shell / Filesystem Provider ├─ 本地 Shell / Filesystem Provider
├─ 最小工具集与 Main 代理 MCP ├─ 最小工具集与 Main 代理 MCP
└─ Main 明确启用的第三方 Cordis 插件 └─ Main 明确启用的第三方 Cordis 插件
│ HTTPS │ HTTP/HTTPS
▼ ▼
用户选择的 OpenAI 兼容模型连接 用户选择的 OpenAI 兼容模型连接
``` ```
@@ -426,8 +426,7 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
- 协议必须是 `openai-chat-completions`。 - 协议必须是 `openai-chat-completions`。
- 认证必须是 API Key。 - 认证必须是 API Key。
- 公网服务地址必须使用 HTTPS;`localhost`、`127.0.0.1` 和 `::1` 回环地址可以使用 HTTP。 - 服务地址支持 HTTP 和 HTTPS,不根据主机位置、端口、凭据、查询参数或片段增加额外限制。
- 服务地址可以使用自定义主机、端口和部署路径,但不得包含用户名、密码、查询参数或片段。
- 模型名称不限制为 DeepSeek 品牌,由所选 OpenAI 兼容服务决定。 - 模型名称不限制为 DeepSeek 品牌,由所选 OpenAI 兼容服务决定。
- 模型名称和服务地址由 Main 传入受控 Host。 - 模型名称和服务地址由 Main 传入受控 Host。
- 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。 - 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。
+1 -1
View File
@@ -266,7 +266,7 @@ GoodBuddy 应能够:
1. 所有新 IPC 继续执行 Zod 校验和可信主窗口校验。 1. 所有新 IPC 继续执行 Zod 校验和可信主窗口校验。
2. 项目根目录、成果路径和上下文路径必须 canonicalize 并验证目录包含关系。 2. 项目根目录、成果路径和上下文路径必须 canonicalize 并验证目录包含关系。
3. Child Runtime 环境变量改用最小 allowlist,避免继承无关密钥。 3. Child Runtime 环境变量改用最小 allowlist,避免继承无关密钥。
4. 远程委派仅允许 HTTPS,开发环境只放行 loopback。 4. 远程委派支持用户配置的 HTTP 和 HTTPS 地址,不按主机位置增加额外限制。
5. 语音、窗口捕获和浏览器上下文分别授权。 5. 语音、窗口捕获和浏览器上下文分别授权。
6. 自动化不得绕过工具审批和项目权限。 6. 自动化不得绕过工具审批和项目权限。
7. 记忆必须保留来源和作用域。 7. 记忆必须保留来源和作用域。
+1 -1
View File
@@ -100,7 +100,7 @@ describe('createAgentRuntime model compatibility', () => {
{ deepseekHarnessLauncher: vi.fn() } { deepseekHarnessLauncher: vi.fn() }
) )
).toThrow( ).toThrow(
'DeepSeek Harness 需要使用 API Key 的安全 OpenAI 兼容 Chat Completions 模型连接' 'DeepSeek Harness 需要使用 API Key 的 OpenAI 兼容 Chat Completions 模型连接'
) )
}) })
+1 -1
View File
@@ -190,7 +190,7 @@ export function createAgentRuntime(
const profile = settings?.deepseekHarnessModelProfile const profile = settings?.deepseekHarnessModelProfile
if (!profile || !isDeepSeekHarnessModelProfile(profile)) { if (!profile || !isDeepSeekHarnessModelProfile(profile)) {
throw new Error( throw new Error(
'DeepSeek Harness 需要使用 API Key 的安全 OpenAI 兼容 Chat Completions 模型连接' 'DeepSeek Harness 需要使用 API Key 的 OpenAI 兼容 Chat Completions 模型连接'
) )
} }
if (!profile.apiKey) { if (!profile.apiKey) {
@@ -50,7 +50,7 @@ async function fixture() {
launchOptions: { launchOptions: {
cwd: workspace, cwd: workspace,
signal: new AbortController().signal, signal: new AbortController().signal,
baseUrl: 'https://gateway.example/openai/v1', baseUrl: 'http://gateway.example/openai/v1?api-version=1',
model: 'qwen-plus', model: 'qwen-plus',
supportsImageInput: false, supportsImageInput: false,
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF], credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF],
@@ -102,7 +102,7 @@ describe('DeepSeek Harness utility launcher', () => {
expect(utility.messages[0]).toMatchObject({ expect(utility.messages[0]).toMatchObject({
type: 'start', type: 'start',
config: { config: {
baseUrl: 'https://gateway.example/openai/v1', baseUrl: 'http://gateway.example/openai/v1?api-version=1',
model: 'qwen-plus', model: 'qwen-plus',
supportsImageInput: false, supportsImageInput: false,
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF] credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF]
@@ -235,10 +235,9 @@ describe('DeepSeek Harness utility launcher', () => {
}) })
it.each([ it.each([
'http://gateway.example/v1', 'file:///private/config',
'https://user:secret@gateway.example/v1', 'ftp://gateway.example/v1'
'https://gateway.example/v1?api-version=2025-01-01' ])('rejects unsupported endpoint %s before forking', async (baseUrl) => {
])('rejects unsafe endpoint %s before forking', async (baseUrl) => {
const { dshHome, hostPath, launchOptions } = await fixture() const { dshHome, hostPath, launchOptions } = await fixture()
const fork = vi.fn() const fork = vi.fn()
const launcher = createDeepSeekHarnessUtilityLauncher({ const launcher = createDeepSeekHarnessUtilityLauncher({
@@ -250,7 +249,7 @@ describe('DeepSeek Harness utility launcher', () => {
await expect( await expect(
launcher({ ...launchOptions, baseUrl }) launcher({ ...launchOptions, baseUrl })
).rejects.toThrow('HTTPS') ).rejects.toThrow('HTTP 或 HTTPS')
expect(fork).not.toHaveBeenCalled() expect(fork).not.toHaveBeenCalled()
}) })
}) })
@@ -149,7 +149,7 @@ export function createDeepSeekHarnessUtilityLauncher(
} }
if (!isDeepSeekHarnessCompatibleBaseUrl(options.baseUrl)) { if (!isDeepSeekHarnessCompatibleBaseUrl(options.baseUrl)) {
throw new Error( throw new Error(
'DeepSeek Harness 模型地址必须使用 HTTPS 或本机回环 HTTP,且不得包含凭据、查询参数或片段' 'DeepSeek Harness 模型地址必须使用 HTTP 或 HTTPS'
) )
} }
if ( if (
+1 -1
View File
@@ -139,7 +139,7 @@ export function applyRuntimeSelection(
!isDeepSeekHarnessModelProfile(selectedProfile) !isDeepSeekHarnessModelProfile(selectedProfile)
) { ) {
throw new Error( throw new Error(
'DeepSeek Harness 仅支持使用 API Key 的安全 OpenAI 兼容 Chat Completions 连接' 'DeepSeek Harness 仅支持使用 API Key 的 OpenAI 兼容 Chat Completions 连接'
) )
} }
return { return {
@@ -231,7 +231,7 @@ describe('ChannelSettingsStore', () => {
const bound = await store.saveWeixinBinding({ const bound = await store.saveWeixinBinding({
accountId: 'account-123456', accountId: 'account-123456',
userId: 'user-654321', userId: 'user-654321',
baseUrl: 'https://ilinkai.weixin.qq.com', baseUrl: 'http://ilinkai.weixin.qq.com',
token: 'weixin-private-token' token: 'weixin-private-token'
}) })
expect(bound.weixin).toMatchObject({ expect(bound.weixin).toMatchObject({
+7 -3
View File
@@ -300,9 +300,13 @@ const weixinBindingSchema = z
.string() .string()
.url() .url()
.max(2_048) .max(2_048)
.refine((value) => new URL(value).protocol === 'https:', { .refine(
message: '微信服务地址必须使用 HTTPS' (value) =>
}), ['http:', 'https:'].includes(new URL(value).protocol),
{
message: '微信服务地址必须使用 HTTP 或 HTTPS'
}
),
token: z token: z
.string() .string()
.trim() .trim()
+1 -1
View File
@@ -39,7 +39,7 @@ describe('controlled DeepSeek Harness host', () => {
dshHome: 'C:\\controlled-dsh-home', dshHome: 'C:\\controlled-dsh-home',
skillPackages: [] skillPackages: []
}) })
).rejects.toThrow('secure OpenAI-compatible') ).rejects.toThrow('HTTP or HTTPS OpenAI-compatible')
}) })
it('suppresses console payloads instead of contaminating stdout', () => { it('suppresses console payloads instead of contaminating stdout', () => {
+1 -1
View File
@@ -93,7 +93,7 @@ function validateHostConfig(
!isDeepSeekHarnessCompatibleBaseUrl(config.baseUrl) !isDeepSeekHarnessCompatibleBaseUrl(config.baseUrl)
) { ) {
throw new Error( throw new Error(
'Controlled Harness requires a secure OpenAI-compatible Chat Completions endpoint' 'Controlled Harness requires an HTTP or HTTPS OpenAI-compatible Chat Completions endpoint'
) )
} }
if (!config.credentialRefs.length) { if (!config.credentialRefs.length) {
+4 -21
View File
@@ -458,7 +458,7 @@ describe('RuntimeSettingsStore', () => {
const { store } = await createStore({ const { store } = await createStore({
GOODBUDDY_MODEL_API_KEY: apiKey, GOODBUDDY_MODEL_API_KEY: apiKey,
GOODBUDDY_MODEL_BASE_URL: GOODBUDDY_MODEL_BASE_URL:
'https://gateway.example/openai/v1', 'http://gateway.example/openai/v1?api-version=1',
GOODBUDDY_MODEL_NAME: 'qwen-plus' GOODBUDDY_MODEL_NAME: 'qwen-plus'
}) })
@@ -467,7 +467,7 @@ describe('RuntimeSettingsStore', () => {
deepseekHarnessModelProfile: { deepseekHarnessModelProfile: {
id: 'goodbuddy-platform-harness', id: 'goodbuddy-platform-harness',
name: '管理员预置模型', name: '管理员预置模型',
baseUrl: 'https://gateway.example/openai/v1', baseUrl: 'http://gateway.example/openai/v1?api-version=1',
modelName: 'qwen-plus', modelName: 'qwen-plus',
protocol: 'openai-chat-completions', protocol: 'openai-chat-completions',
authentication: 'api-key', authentication: 'api-key',
@@ -486,28 +486,11 @@ describe('RuntimeSettingsStore', () => {
it.each([ it.each([
[ [
'an insecure public endpoint', 'an unsupported endpoint protocol',
{
GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL: 'http://gateway.example/v1',
GOODBUDDY_MODEL_NAME: 'qwen-plus'
}
],
[
'an endpoint with embedded credentials',
{ {
GOODBUDDY_MODEL_API_KEY: 'platform-key', GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL: GOODBUDDY_MODEL_BASE_URL:
'https://user:secret@gateway.example/v1', 'ftp://gateway.example/v1',
GOODBUDDY_MODEL_NAME: 'qwen-plus'
}
],
[
'an endpoint with a query string',
{
GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL:
'https://gateway.example/v1?api-version=1',
GOODBUDDY_MODEL_NAME: 'qwen-plus' GOODBUDDY_MODEL_NAME: 'qwen-plus'
} }
], ],
+1 -1
View File
@@ -2290,7 +2290,7 @@ export class RuntimeSettingsStore {
} }
if (!isDeepSeekHarnessModelProfile(profile)) { if (!isDeepSeekHarnessModelProfile(profile)) {
throw new Error( throw new Error(
'DeepSeek Harness 仅支持使用 API Key 的安全 OpenAI 兼容 Chat Completions 连接' 'DeepSeek Harness 仅支持使用 API Key 的 OpenAI 兼容 Chat Completions 连接'
) )
} }
} }
@@ -742,7 +742,7 @@ export const settings = {
incompatibleImageProtocol: incompatibleImageProtocol:
'Incompatible (image generation protocol is unsupported)', 'Incompatible (image generation protocol is unsupported)',
incompatibleHarnessProtocol: incompatibleHarnessProtocol:
'Incompatible (requires Chat Completions, an API key, and a secure endpoint)', 'Incompatible (requires Chat Completions, an API key, and an HTTP/HTTPS endpoint)',
secureStorageWarning: secureStorageWarning:
'Secure system key storage is unavailable. Use an environment variable to avoid storing an API Key in plaintext.' 'Secure system key storage is unavailable. Use an environment variable to avoid storing an API Key in plaintext.'
}, },
@@ -681,7 +681,7 @@ export const settings = {
incompatible: '不兼容', incompatible: '不兼容',
incompatibleImageProtocol: '不兼容(不支持图像生成协议)', incompatibleImageProtocol: '不兼容(不支持图像生成协议)',
incompatibleHarnessProtocol: incompatibleHarnessProtocol:
'不兼容(需 Chat Completions、API Key 和安全地址)', '不兼容(需 Chat Completions、API Key 和 HTTP/HTTPS 地址)',
secureStorageWarning: secureStorageWarning:
'当前系统密钥服务不可用。为了避免明文落盘,请使用环境变量提供 API Key。' '当前系统密钥服务不可用。为了避免明文落盘,请使用环境变量提供 API Key。'
}, },
+1 -1
View File
@@ -830,7 +830,7 @@ export const runtimeSettingsInputSchema = z
code: 'custom', code: 'custom',
path: ['deepseekHarnessModelSource'], path: ['deepseekHarnessModelSource'],
message: message:
'DeepSeek Harness 仅支持使用 API Key 的安全 OpenAI 兼容 Chat Completions 连接' 'DeepSeek Harness 仅支持使用 API Key 的 OpenAI 兼容 Chat Completions 连接'
}) })
} }
const compressionSource = settings.contextCompression?.modelSource const compressionSource = settings.contextCompression?.modelSource
@@ -9,9 +9,14 @@ describe('DeepSeek Harness model compatibility', () => {
'https://api.deepseek.com', 'https://api.deepseek.com',
'https://gateway.example/openai/v1', 'https://gateway.example/openai/v1',
'https://gateway.example:8443/v1', 'https://gateway.example:8443/v1',
'http://gateway.example/v1',
'http://192.168.1.50:11434/v1',
'http://127.0.0.1:11434/v1', 'http://127.0.0.1:11434/v1',
'http://localhost:1234/v1', 'http://localhost:1234/v1',
'http://[::1]:8080/v1' 'http://[::1]:8080/v1',
'https://gateway.example/v1?api-version=2025-01-01',
'https://gateway.example/v1#chat',
'http://user:password@gateway.example/v1'
])('accepts OpenAI-compatible base URL %s', (baseUrl) => { ])('accepts OpenAI-compatible base URL %s', (baseUrl) => {
expect(isDeepSeekHarnessCompatibleBaseUrl(baseUrl)).toBe(true) expect(isDeepSeekHarnessCompatibleBaseUrl(baseUrl)).toBe(true)
expect( expect(
@@ -24,12 +29,10 @@ describe('DeepSeek Harness model compatibility', () => {
}) })
it.each([ it.each([
'http://gateway.example/v1',
'file:///private/config', 'file:///private/config',
'https://user:secret@gateway.example/v1', 'ftp://gateway.example/v1',
'https://gateway.example/v1?api-version=2025-01-01', 'not-a-url'
'https://gateway.example/v1#chat' ])('rejects unsupported base URL %s', (baseUrl) => {
])('rejects unsafe or unsupported base URL %s', (baseUrl) => {
expect(isDeepSeekHarnessCompatibleBaseUrl(baseUrl)).toBe(false) expect(isDeepSeekHarnessCompatibleBaseUrl(baseUrl)).toBe(false)
}) })
+2 -23
View File
@@ -4,35 +4,14 @@ type DeepSeekHarnessModelProfile = {
authentication: 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( export function isDeepSeekHarnessCompatibleBaseUrl(
value: string value: string
): boolean { ): boolean {
try { try {
const url = new URL(value) const url = new URL(value)
if (
!url.hostname ||
url.username ||
url.password ||
url.search ||
url.hash
) {
return false
}
return ( return (
url.protocol === 'https:' || Boolean(url.hostname) &&
(url.protocol === 'http:' && (url.protocol === 'http:' || url.protocol === 'https:')
isLoopbackHostname(url.hostname))
) )
} catch { } catch {
return false return false