feat: add explicit managed model download sources

Managed speech and OCR downloads previously used catalog-specific source URLs without a global selection. Platform Features now lets users choose ModelScope by default or Hugging Face, while Main validates and freezes that source for each download.

Verified coverage remains explicit: downloads never mix artifacts or silently switch sources, and installed models plus ZIP imports stay source-independent.

Release note: 可在“设置 → 平台功能 → 通用设置”中选择 ModelScope 或 Hugging Face 作为后续语音输入与 OCR 模型下载源;缺少完整已验证文件或下载失败时不会静默换源。
This commit is contained in:
mesalogo
2026-08-19 13:43:16 +08:00
parent c41d7e8b96
commit 43e1d162dc
44 changed files with 6083 additions and 839 deletions
+71
View File
@@ -0,0 +1,71 @@
const MAX_REDIRECTS = 3
const redirectStatuses = new Set([301, 302, 303, 307, 308])
function validateDownloadUrl(value: string, modelLabel: string): URL {
const url = new URL(value)
if (
url.protocol !== 'https:' ||
(url.port !== '' && url.port !== '443') ||
url.username ||
url.password ||
url.hash
) {
throw new Error(
`${modelLabel}下载地址必须是使用标准端口、无凭据和 Fragment 的 HTTPS URL`
)
}
return url
}
export async function fetchModelDownloadResponse(options: {
transport: typeof fetch
initialUrl: string
redirectHosts: readonly string[]
signal: AbortSignal
modelLabel: string
}): Promise<Response> {
let url = validateDownloadUrl(options.initialUrl, options.modelLabel)
const initialHost = url.hostname
const allowedRedirectHosts = new Set(options.redirectHosts)
for (let redirectCount = 0; ; redirectCount += 1) {
if (options.signal.aborted) {
throw new DOMException('The operation was aborted', 'AbortError')
}
const response = await options.transport(url, {
method: 'GET',
redirect: 'manual',
credentials: 'omit',
cache: 'no-store',
signal: options.signal
})
if (!redirectStatuses.has(response.status)) {
return response
}
if (redirectCount >= MAX_REDIRECTS) {
await response.body?.cancel().catch(() => undefined)
throw new Error(
`${options.modelLabel}下载重定向次数过多`
)
}
const location = response.headers.get('location')
await response.body?.cancel().catch(() => undefined)
if (!location) {
throw new Error(
`${options.modelLabel}下载重定向缺少地址`
)
}
const nextUrl = validateDownloadUrl(
new URL(location, url).toString(),
options.modelLabel
)
if (
nextUrl.hostname !== initialHost &&
!allowedRedirectHosts.has(nextUrl.hostname)
) {
throw new Error(
`${options.modelLabel}下载重定向到未声明的主机`
)
}
url = nextUrl
}
}