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
+43 -24
View File
@@ -109,6 +109,7 @@ import { applicationSettingsUpdateSchema } from '../shared/application-settings-
import { releaseNotesAcknowledgeSchema } from '../shared/release-notes-contracts'
import {
speechModelActionInputSchema,
speechModelInstallInputSchema,
speechModelSelectionInputSchema
} from '../shared/speech-model-contracts'
import {
@@ -119,6 +120,7 @@ import {
} from '../shared/embedding-contracts'
import {
documentOcrModelActionInputSchema,
documentOcrModelInstallInputSchema,
documentOcrFailureSchema,
documentOcrResultSchema,
documentParsingSettingsUpdateSchema,
@@ -3513,16 +3515,25 @@ export function registerIpcHandlers(
registerHandler(
ipcChannels.documentOcrModelsInstall,
(event, input: unknown) => {
async (event, input: unknown) => {
assertTrustedSender(event, window)
if (!documentOcrModelManager || !documentParsingService) {
if (
!documentOcrModelManager ||
!documentParsingService ||
!applicationSettingsStore
) {
throw new Error('本地 OCR 模型服务不可用')
}
const { modelId } =
documentOcrModelActionInputSchema.parse(input)
const { modelId, expectedDownloadSource } =
documentOcrModelInstallInputSchema.parse(input)
const { modelDownloadSource: selectedDownloadSource } =
await applicationSettingsStore.get()
if (selectedDownloadSource !== expectedDownloadSource) {
throw new Error('模型下载源已变化,请刷新后重试')
}
return trackExecution(
documentOcrModelManager
.install(modelId)
.install(modelId, selectedDownloadSource)
.then(() => documentParsingService.snapshot())
)
}
@@ -3611,19 +3622,19 @@ export function registerIpcHandlers(
ipcChannels.documentOcrModelsOpenRepository,
async (event, input: unknown) => {
assertTrustedSender(event, window)
if (!documentOcrModelManager) {
if (!documentOcrModelManager || !applicationSettingsStore) {
throw new Error('本地 OCR 模型服务不可用')
}
const { modelId } =
documentOcrModelActionInputSchema.parse(input)
const snapshot = await documentOcrModelManager.getSnapshot()
const entry = snapshot.catalog.find(
(candidate) => candidate.id === modelId
const { modelDownloadSource: selectedDownloadSource } =
await applicationSettingsStore.get()
await shell.openExternal(
documentOcrModelManager.getRepositoryUrl(
modelId,
selectedDownloadSource
)
)
if (!entry) {
throw new Error('未知的 OCR 模型')
}
await shell.openExternal(entry.repositoryUrl)
}
)
@@ -3759,15 +3770,21 @@ export function registerIpcHandlers(
registerHandler(
ipcChannels.speechModelsInstall,
(event, input: unknown) => {
async (event, input: unknown) => {
assertTrustedSender(event, window)
if (!speechModelManager) {
if (!speechModelManager || !applicationSettingsStore) {
throw new Error('语音模型服务不可用')
}
const { modelId } = speechModelActionInputSchema.parse(input)
const { modelId, expectedDownloadSource } =
speechModelInstallInputSchema.parse(input)
const { modelDownloadSource: selectedDownloadSource } =
await applicationSettingsStore.get()
if (selectedDownloadSource !== expectedDownloadSource) {
throw new Error('模型下载源已变化,请刷新后重试')
}
return trackExecution(
speechModelManager
.install(modelId)
.install(modelId, selectedDownloadSource)
.then(() => speechModelManager.getSnapshot())
)
}
@@ -3862,16 +3879,18 @@ export function registerIpcHandlers(
ipcChannels.speechModelsOpenRepository,
async (event, input: unknown) => {
assertTrustedSender(event, window)
if (!speechModelManager) {
if (!speechModelManager || !applicationSettingsStore) {
throw new Error('语音模型服务不可用')
}
const { modelId } = speechModelActionInputSchema.parse(input)
const snapshot = await speechModelManager.getSnapshot()
const entry = snapshot.catalog.find((item) => item.id === modelId)
if (!entry) {
throw new Error('未知的语音模型')
}
await shell.openExternal(entry.repositoryUrl)
const { modelDownloadSource: selectedDownloadSource } =
await applicationSettingsStore.get()
await shell.openExternal(
speechModelManager.getRepositoryUrl(
modelId,
selectedDownloadSource
)
)
}
)