chore: prepare GoodBuddy 0.8.5

This commit is contained in:
lofyer
2026-08-07 10:45:01 +08:00
parent e20cb447af
commit 17e66a3369
54 changed files with 1306 additions and 1640 deletions
+1
View File
@@ -75,6 +75,7 @@ export const conversationSnapshotSchema = z
id: assistantIdSchema,
role: z.enum(['user', 'assistant']),
content: z.string().max(1_000_000),
reasoning: z.string().optional(),
createdAt: z.number().int().nonnegative(),
state: z.enum(['streaming', 'complete', 'error']),
status: z.string().max(4_000).optional(),
+2 -8
View File
@@ -217,16 +217,10 @@ const mcpRemoteUrlSchema = z
.url()
.max(2_048)
.superRefine((value, context) => {
const url = new URL(value)
if (
!['http:', 'https:'].includes(url.protocol) ||
url.username ||
url.password ||
url.hash
) {
if (!['http:', 'https:'].includes(new URL(value).protocol)) {
context.addIssue({
code: 'custom',
message: 'MCP URL 必须是无凭据和片段的 HTTP(S) 地址'
message: 'MCP URL 必须使用 HTTP 或 HTTPS'
})
}
})
+23 -74
View File
@@ -43,7 +43,6 @@ import type {
ManagedChannel,
WeComChannelSettingsInput
} from './channel-settings-contracts'
import { isIntranetHostname } from './intranet-hostname'
import type {
ApplicationSettings,
VersionCheckResult
@@ -203,7 +202,6 @@ export const defaultRuntimeSettings = {
continueMode: 'chat',
runtimeSandboxMode: 'auto',
subagentSmartRoutingEnabled: false,
intranetCompatibilityEnabled: false,
knowledgeEmbeddingEnabled: false,
knowledgeEmbeddingBaseUrl:
'http://127.0.0.1:11434/v1/embeddings',
@@ -324,7 +322,6 @@ export const runtimeSettingsInputSchema = z
continueMode: continueModeSchema,
runtimeSandboxMode: runtimeSandboxModeSchema,
subagentSmartRoutingEnabled: z.boolean().optional(),
intranetCompatibilityEnabled: z.boolean().default(false),
knowledgeEmbeddingEnabled: z.boolean(),
knowledgeEmbeddingBaseUrl: z.string().url().max(2_048),
knowledgeEmbeddingModel: z
@@ -359,32 +356,11 @@ export const runtimeSettingsInputSchema = z
value: profile.baseUrl
})) ?? [{ path: ['modelBaseUrl'], value: settings.modelBaseUrl }]
for (const endpoint of endpoints) {
const url = new URL(endpoint.value)
const hostname = url.hostname.toLowerCase()
const loopback =
hostname === 'localhost' ||
hostname === '::1' ||
hostname === '[::1]' ||
/^127(?:\.\d{1,3}){3}$/u.test(hostname)
if (
!(
url.protocol === 'https:' ||
(url.protocol === 'http:' &&
(loopback ||
(settings.intranetCompatibilityEnabled &&
isIntranetHostname(hostname))))
) ||
url.username ||
url.password ||
url.search ||
url.hash
) {
if (!['http:', 'https:'].includes(new URL(endpoint.value).protocol)) {
context.addIssue({
code: 'custom',
path: endpoint.path,
message: settings.intranetCompatibilityEnabled
? '模型服务地址必须使用 HTTP(S),且不得包含凭据、查询参数或片段'
: '模型服务地址必须使用 HTTPS;仅本机回环地址可使用 HTTP,且不得包含凭据、查询参数或片段'
message: '模型服务地址必须使用 HTTP 或 HTTPS'
})
}
}
@@ -473,58 +449,27 @@ export const runtimeSettingsInputSchema = z
})
}
}
if (settings.opencodeBaseUrl) {
const opencodeUrl = new URL(settings.opencodeBaseUrl)
if (
!['http:', 'https:'].includes(opencodeUrl.protocol) ||
opencodeUrl.username ||
opencodeUrl.password ||
opencodeUrl.search ||
opencodeUrl.hash ||
(opencodeUrl.pathname !== '/' && opencodeUrl.pathname !== '')
) {
context.addIssue({
code: 'custom',
path: ['opencodeBaseUrl'],
message: 'OpenCode 地址必须是无凭据和路径的 HTTP(S) origin'
})
}
}
const embeddingUrl = new URL(settings.knowledgeEmbeddingBaseUrl)
const embeddingHost = embeddingUrl.hostname.toLowerCase()
const privateIpv4 =
/^10(?:\.\d{1,3}){3}$/u.test(embeddingHost) ||
/^192\.168(?:\.\d{1,3}){2}$/u.test(embeddingHost) ||
/^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/u.test(
embeddingHost
)
const loopback =
embeddingHost === 'localhost' ||
embeddingHost === '::1' ||
embeddingHost === '[::1]' ||
/^127(?:\.\d{1,3}){3}$/u.test(embeddingHost)
if (
!(
embeddingUrl.protocol === 'https:' ||
(embeddingUrl.protocol === 'http:' &&
((settings.intranetCompatibilityEnabled &&
isIntranetHostname(embeddingHost)) ||
loopback ||
privateIpv4))
) ||
embeddingUrl.username ||
embeddingUrl.password ||
embeddingUrl.search ||
embeddingUrl.hash ||
embeddingUrl.pathname === '/' ||
embeddingUrl.pathname === ''
settings.opencodeBaseUrl &&
!['http:', 'https:'].includes(
new URL(settings.opencodeBaseUrl).protocol
)
) {
context.addIssue({
code: 'custom',
path: ['opencodeBaseUrl'],
message: 'OpenCode 地址必须使用 HTTP 或 HTTPS'
})
}
if (
!['http:', 'https:'].includes(
new URL(settings.knowledgeEmbeddingBaseUrl).protocol
)
) {
context.addIssue({
code: 'custom',
path: ['knowledgeEmbeddingBaseUrl'],
message: settings.intranetCompatibilityEnabled
? '向量接口 URL 必须是完整的 HTTP(S) 端点,且不得包含凭据、查询参数或片段'
: '向量接口 URL 必须是完整的 HTTPS 端点;本机或私有网络可使用 HTTP,且不得包含凭据、查询参数或片段'
message: '向量接口 URL 必须使用 HTTP 或 HTTPS'
})
}
})
@@ -561,7 +506,6 @@ export type RuntimeSettings = {
continueMode: RuntimeSettingsInput['continueMode']
runtimeSandboxMode: RuntimeSettingsInput['runtimeSandboxMode']
subagentSmartRoutingEnabled: boolean
intranetCompatibilityEnabled: boolean
knowledgeEmbeddingEnabled: boolean
knowledgeEmbeddingBaseUrl: string
knowledgeEmbeddingModel: string
@@ -675,6 +619,11 @@ export type AgentEvent =
type: 'text'
delta: string
}
| {
requestId: string
type: 'reasoning'
delta: string
}
| {
requestId: string
type: 'tool'
+4 -8
View File
@@ -7,14 +7,10 @@ const safeEndpointSchema = z
.url()
.trim()
.max(2_048)
.refine((value) => {
const url = new URL(value)
return (
['http:', 'https:'].includes(url.protocol) &&
!url.username &&
!url.password
)
}, 'endpoint must be an HTTP URL without credentials')
.refine(
(value) => ['http:', 'https:'].includes(new URL(value).protocol),
'endpoint must be an HTTP or HTTPS URL'
)
export const embeddingErrorCodeSchema = z.enum([
'model_not_found',
-30
View File
@@ -1,30 +0,0 @@
import { describe, expect, it } from 'vitest'
import { isIntranetHostname } from './intranet-hostname'
describe('isIntranetHostname', () => {
it.each([
'localhost',
'printer',
'models.internal',
'models.corp.local',
'10.7.0.23',
'127.0.0.2',
'100.64.0.1',
'172.16.4.2',
'192.168.1.20',
'[fd12:3456::1]'
])('accepts the intranet host %s', (hostname) => {
expect(isIntranetHostname(hostname)).toBe(true)
})
it.each([
'models.example.com',
'8.8.8.8',
'169.254.169.254',
'100.100.100.200',
'[fd00:ec2::254]',
'metadata.google.internal'
])('rejects the public or metadata host %s', (hostname) => {
expect(isIntranetHostname(hostname)).toBe(false)
})
})
-95
View File
@@ -1,95 +0,0 @@
const INTRANET_HOST_SUFFIXES = [
'.home',
'.internal',
'.intranet',
'.lan',
'.local',
'.localdomain',
'.localhost'
] as const
const BLOCKED_HOSTNAMES = new Set([
'100.100.100.200',
'fd00:ec2::254',
'instance-data',
'instance-data.ec2.internal',
'metadata',
'metadata.aws.internal',
'metadata.google.internal'
])
function normalizeHostname(hostname: string): string {
const normalized = hostname.trim().toLowerCase().replace(/\.$/u, '')
return normalized.startsWith('[') && normalized.endsWith(']')
? normalized.slice(1, -1)
: normalized
}
function parseIpv4(hostname: string): readonly number[] | undefined {
const octets = hostname.split('.')
if (
octets.length !== 4 ||
octets.some(
(octet) =>
!/^(?:0|[1-9]\d{0,2})$/u.test(octet) ||
Number(octet) > 255
)
) {
return undefined
}
return octets.map(Number)
}
function isIntranetIpv4(hostname: string): boolean {
const octets = parseIpv4(hostname)
if (!octets) {
return false
}
const [first = -1, second = -1] = octets
return (
first === 10 ||
first === 127 ||
(first === 100 && second >= 64 && second <= 127) ||
(first === 172 && second >= 16 && second <= 31) ||
(first === 192 && second === 168)
)
}
function isIntranetIpv6(hostname: string): boolean {
const withoutZone = hostname.split('%', 1)[0] ?? ''
return (
withoutZone === '::1' ||
/^f[cd][0-9a-f]{2}(?::|$)/u.test(withoutZone)
)
}
export function isLoopbackHostname(hostname: string): boolean {
const normalized = normalizeHostname(hostname)
const ipv4 = parseIpv4(normalized)
return (
normalized === 'localhost' ||
normalized === '::1' ||
ipv4?.[0] === 127
)
}
export function isIntranetHostname(hostname: string): boolean {
const normalized = normalizeHostname(hostname)
if (!normalized || BLOCKED_HOSTNAMES.has(normalized)) {
return false
}
if (parseIpv4(normalized)) {
return isIntranetIpv4(normalized)
}
if (normalized.includes(':')) {
return isIntranetIpv6(normalized)
}
return (
isLoopbackHostname(normalized) ||
!normalized.includes('.') ||
INTRANET_HOST_SUFFIXES.some(
(suffix) =>
normalized === suffix.slice(1) || normalized.endsWith(suffix)
)
)
}