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
+1 -1
View File
@@ -100,7 +100,7 @@ describe('createAgentRuntime model compatibility', () => {
{ deepseekHarnessLauncher: vi.fn() }
)
).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
if (!profile || !isDeepSeekHarnessModelProfile(profile)) {
throw new Error(
'DeepSeek Harness 需要使用 API Key 的安全 OpenAI 兼容 Chat Completions 模型连接'
'DeepSeek Harness 需要使用 API Key 的 OpenAI 兼容 Chat Completions 模型连接'
)
}
if (!profile.apiKey) {
@@ -50,7 +50,7 @@ async function fixture() {
launchOptions: {
cwd: workspace,
signal: new AbortController().signal,
baseUrl: 'https://gateway.example/openai/v1',
baseUrl: 'http://gateway.example/openai/v1?api-version=1',
model: 'qwen-plus',
supportsImageInput: false,
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF],
@@ -102,7 +102,7 @@ describe('DeepSeek Harness utility launcher', () => {
expect(utility.messages[0]).toMatchObject({
type: 'start',
config: {
baseUrl: 'https://gateway.example/openai/v1',
baseUrl: 'http://gateway.example/openai/v1?api-version=1',
model: 'qwen-plus',
supportsImageInput: false,
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF]
@@ -235,10 +235,9 @@ describe('DeepSeek Harness utility launcher', () => {
})
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) => {
'file:///private/config',
'ftp://gateway.example/v1'
])('rejects unsupported endpoint %s before forking', async (baseUrl) => {
const { dshHome, hostPath, launchOptions } = await fixture()
const fork = vi.fn()
const launcher = createDeepSeekHarnessUtilityLauncher({
@@ -250,7 +249,7 @@ describe('DeepSeek Harness utility launcher', () => {
await expect(
launcher({ ...launchOptions, baseUrl })
).rejects.toThrow('HTTPS')
).rejects.toThrow('HTTP 或 HTTPS')
expect(fork).not.toHaveBeenCalled()
})
})
@@ -149,7 +149,7 @@ export function createDeepSeekHarnessUtilityLauncher(
}
if (!isDeepSeekHarnessCompatibleBaseUrl(options.baseUrl)) {
throw new Error(
'DeepSeek Harness 模型地址必须使用 HTTPS 或本机回环 HTTP,且不得包含凭据、查询参数或片段'
'DeepSeek Harness 模型地址必须使用 HTTP 或 HTTPS'
)
}
if (
+1 -1
View File
@@ -139,7 +139,7 @@ export function applyRuntimeSelection(
!isDeepSeekHarnessModelProfile(selectedProfile)
) {
throw new Error(
'DeepSeek Harness 仅支持使用 API Key 的安全 OpenAI 兼容 Chat Completions 连接'
'DeepSeek Harness 仅支持使用 API Key 的 OpenAI 兼容 Chat Completions 连接'
)
}
return {
@@ -231,7 +231,7 @@ describe('ChannelSettingsStore', () => {
const bound = await store.saveWeixinBinding({
accountId: 'account-123456',
userId: 'user-654321',
baseUrl: 'https://ilinkai.weixin.qq.com',
baseUrl: 'http://ilinkai.weixin.qq.com',
token: 'weixin-private-token'
})
expect(bound.weixin).toMatchObject({
+7 -3
View File
@@ -300,9 +300,13 @@ const weixinBindingSchema = z
.string()
.url()
.max(2_048)
.refine((value) => new URL(value).protocol === 'https:', {
message: '微信服务地址必须使用 HTTPS'
}),
.refine(
(value) =>
['http:', 'https:'].includes(new URL(value).protocol),
{
message: '微信服务地址必须使用 HTTP 或 HTTPS'
}
),
token: z
.string()
.trim()
+1 -1
View File
@@ -39,7 +39,7 @@ describe('controlled DeepSeek Harness host', () => {
dshHome: 'C:\\controlled-dsh-home',
skillPackages: []
})
).rejects.toThrow('secure OpenAI-compatible')
).rejects.toThrow('HTTP or HTTPS OpenAI-compatible')
})
it('suppresses console payloads instead of contaminating stdout', () => {
+1 -1
View File
@@ -93,7 +93,7 @@ function validateHostConfig(
!isDeepSeekHarnessCompatibleBaseUrl(config.baseUrl)
) {
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) {
+4 -21
View File
@@ -458,7 +458,7 @@ describe('RuntimeSettingsStore', () => {
const { store } = await createStore({
GOODBUDDY_MODEL_API_KEY: apiKey,
GOODBUDDY_MODEL_BASE_URL:
'https://gateway.example/openai/v1',
'http://gateway.example/openai/v1?api-version=1',
GOODBUDDY_MODEL_NAME: 'qwen-plus'
})
@@ -467,7 +467,7 @@ describe('RuntimeSettingsStore', () => {
deepseekHarnessModelProfile: {
id: 'goodbuddy-platform-harness',
name: '管理员预置模型',
baseUrl: 'https://gateway.example/openai/v1',
baseUrl: 'http://gateway.example/openai/v1?api-version=1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key',
@@ -486,28 +486,11 @@ describe('RuntimeSettingsStore', () => {
it.each([
[
'an insecure public endpoint',
{
GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL: 'http://gateway.example/v1',
GOODBUDDY_MODEL_NAME: 'qwen-plus'
}
],
[
'an endpoint with embedded credentials',
'an unsupported endpoint protocol',
{
GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL:
'https://user:secret@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',
'ftp://gateway.example/v1',
GOODBUDDY_MODEL_NAME: 'qwen-plus'
}
],
+1 -1
View File
@@ -2290,7 +2290,7 @@ export class RuntimeSettingsStore {
}
if (!isDeepSeekHarnessModelProfile(profile)) {
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:
'Incompatible (image generation protocol is unsupported)',
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:
'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: '不兼容',
incompatibleImageProtocol: '不兼容(不支持图像生成协议)',
incompatibleHarnessProtocol:
'不兼容(需 Chat Completions、API Key 和安全地址)',
'不兼容(需 Chat Completions、API Key 和 HTTP/HTTPS 地址)',
secureStorageWarning:
'当前系统密钥服务不可用。为了避免明文落盘,请使用环境变量提供 API Key。'
},
+1 -1
View File
@@ -830,7 +830,7 @@ export const runtimeSettingsInputSchema = z
code: 'custom',
path: ['deepseekHarnessModelSource'],
message:
'DeepSeek Harness 仅支持使用 API Key 的安全 OpenAI 兼容 Chat Completions 连接'
'DeepSeek Harness 仅支持使用 API Key 的 OpenAI 兼容 Chat Completions 连接'
})
}
const compressionSource = settings.contextCompression?.modelSource
@@ -9,9 +9,14 @@ describe('DeepSeek Harness model compatibility', () => {
'https://api.deepseek.com',
'https://gateway.example/openai/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://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) => {
expect(isDeepSeekHarnessCompatibleBaseUrl(baseUrl)).toBe(true)
expect(
@@ -24,12 +29,10 @@ describe('DeepSeek Harness model compatibility', () => {
})
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) => {
'ftp://gateway.example/v1',
'not-a-url'
])('rejects unsupported base URL %s', (baseUrl) => {
expect(isDeepSeekHarnessCompatibleBaseUrl(baseUrl)).toBe(false)
})
+2 -23
View File
@@ -4,35 +4,14 @@ type DeepSeekHarnessModelProfile = {
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))
Boolean(url.hostname) &&
(url.protocol === 'http:' || url.protocol === 'https:')
)
} catch {
return false