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:
@@ -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 模型输入模态必须使用同一个布尔值。
|
||||||
|
|||||||
@@ -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. 记忆必须保留来源和作用域。
|
||||||
|
|||||||
@@ -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 模型连接'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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。'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user