feat: add document OCR and offline model archives
This commit is contained in:
+45
-1
@@ -74,6 +74,15 @@ import type {
|
||||
EmbeddingIndexStatus,
|
||||
EmbeddingSettingsSnapshot
|
||||
} from './embedding-contracts'
|
||||
import type {
|
||||
DocumentOcrAssets,
|
||||
DocumentOcrFailure,
|
||||
DocumentOcrRequest,
|
||||
DocumentOcrResult,
|
||||
DocumentParsingDiagnostic,
|
||||
DocumentParsingSettings,
|
||||
DocumentParsingSnapshot
|
||||
} from './document-parsing-contracts'
|
||||
import type { WeixinBindingSnapshot } from './weixin-channel-contracts'
|
||||
import type { RemoteChannelActivity } from './remote-channel-contracts'
|
||||
import {
|
||||
@@ -1038,7 +1047,10 @@ export type DesktopApi = {
|
||||
cancel: (modelId: string) => Promise<boolean>
|
||||
remove: (modelId: string) => Promise<SpeechModelSnapshot>
|
||||
select: (modelId: string | null) => Promise<SpeechModelSnapshot>
|
||||
importLocalDirectory: (
|
||||
importArchive: (
|
||||
modelId: string
|
||||
) => Promise<SpeechModelSnapshot | undefined>
|
||||
exportArchive: (
|
||||
modelId: string
|
||||
) => Promise<SpeechModelSnapshot | undefined>
|
||||
openRepository: (modelId: string) => Promise<void>
|
||||
@@ -1059,6 +1071,38 @@ export type DesktopApi = {
|
||||
listener: (status: EmbeddingIndexStatus) => void
|
||||
) => () => void
|
||||
}
|
||||
documentParsing?: {
|
||||
getSnapshot: () => Promise<DocumentParsingSnapshot>
|
||||
update: (
|
||||
input: DocumentParsingSettings
|
||||
) => Promise<DocumentParsingSnapshot>
|
||||
test: () => Promise<DocumentParsingDiagnostic | undefined>
|
||||
installOcrModel: (
|
||||
modelId: string
|
||||
) => Promise<DocumentParsingSnapshot>
|
||||
cancelOcrModelOperation: (modelId: string) => Promise<boolean>
|
||||
removeOcrModel: (
|
||||
modelId: string
|
||||
) => Promise<DocumentParsingSnapshot>
|
||||
importOcrModelArchive: (
|
||||
modelId: string
|
||||
) => Promise<DocumentParsingSnapshot | undefined>
|
||||
exportOcrModelArchive: (
|
||||
modelId: string
|
||||
) => Promise<DocumentParsingSnapshot | undefined>
|
||||
openOcrModelRepository: (modelId: string) => Promise<void>
|
||||
openOcrModelsDirectory: () => Promise<void>
|
||||
getOcrAssets: (modelId: string) => Promise<DocumentOcrAssets>
|
||||
respondOcr: (
|
||||
response: DocumentOcrResult | DocumentOcrFailure
|
||||
) => Promise<void>
|
||||
onOcrRequest: (
|
||||
listener: (request: DocumentOcrRequest) => void
|
||||
) => () => void
|
||||
onOcrCancel: (
|
||||
listener: (requestId: string) => void
|
||||
) => () => void
|
||||
}
|
||||
projects: {
|
||||
list: (includeArchived?: boolean) => Promise<AssistantProject[]>
|
||||
create: (input: ProjectCreateInput) => Promise<AssistantProject>
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const documentParsingPurposeSchema = z.enum([
|
||||
'chat-attachment',
|
||||
'knowledge-index',
|
||||
'diagnostic'
|
||||
])
|
||||
|
||||
export const chatDocumentWorkflowSchema = z.enum([
|
||||
'auto',
|
||||
'fast-text',
|
||||
'high-fidelity'
|
||||
])
|
||||
|
||||
export const knowledgeDocumentWorkflowSchema = z.enum([
|
||||
'complete-index',
|
||||
'fast-index',
|
||||
'high-fidelity'
|
||||
])
|
||||
|
||||
export const pdfOcrModeSchema = z.enum([
|
||||
'auto',
|
||||
'always',
|
||||
'disabled'
|
||||
])
|
||||
|
||||
export const documentOcrProviderSchema = z.literal('local')
|
||||
|
||||
export const localOcrModelIdSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(96)
|
||||
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u)
|
||||
|
||||
const documentOcrSha256Schema = z
|
||||
.string()
|
||||
.regex(/^[a-f0-9]{64}$/u)
|
||||
|
||||
export const documentOcrModelFileRoleSchema = z.enum([
|
||||
'detection',
|
||||
'recognition',
|
||||
'dictionary'
|
||||
])
|
||||
|
||||
export const documentOcrModelDownloadSchema = z
|
||||
.object({
|
||||
url: z.url().max(2_048),
|
||||
size: z.number().int().positive().safe(),
|
||||
sha256: documentOcrSha256Schema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrModelFileSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.regex(/^[^/\\\0]+$/u),
|
||||
role: documentOcrModelFileRoleSchema,
|
||||
download: documentOcrModelDownloadSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrModelCatalogEntrySchema = z
|
||||
.object({
|
||||
id: localOcrModelIdSchema,
|
||||
displayName: z.string().trim().min(1).max(120),
|
||||
description: z.string().trim().min(1).max(500),
|
||||
languages: z.array(z.string().trim().min(1).max(32)).min(1).max(32),
|
||||
runtime: z.literal('onnxruntime-web-wasm'),
|
||||
quality: z.enum(['basic', 'balanced', 'high']),
|
||||
speed: z.enum(['fast', 'balanced', 'slow']),
|
||||
recommended: z.boolean(),
|
||||
repositoryUrl: z.url().max(2_048),
|
||||
license: z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
notice: z.string().trim().min(1).max(1_000),
|
||||
url: z.url().max(2_048)
|
||||
})
|
||||
.strict(),
|
||||
files: z.array(documentOcrModelFileSchema).length(3)
|
||||
})
|
||||
.strict()
|
||||
.superRefine((entry, context) => {
|
||||
if (
|
||||
new Set(entry.files.map((file) => file.name)).size !==
|
||||
entry.files.length
|
||||
) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['files'],
|
||||
message: 'OCR 模型文件名不能重复'
|
||||
})
|
||||
}
|
||||
if (
|
||||
new Set(entry.files.map((file) => file.role)).size !==
|
||||
entry.files.length
|
||||
) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['files'],
|
||||
message: 'OCR 模型文件角色不能重复'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const installedDocumentOcrModelSchema = z
|
||||
.object({
|
||||
id: localOcrModelIdSchema,
|
||||
displayName: z.string().trim().min(1).max(120),
|
||||
source: z.enum(['download', 'local']),
|
||||
installedAt: z.string().datetime(),
|
||||
files: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
name: z.string().min(1).max(255).regex(/^[^/\\\0]+$/u),
|
||||
role: documentOcrModelFileRoleSchema,
|
||||
size: z.number().int().positive().safe(),
|
||||
sha256: documentOcrSha256Schema
|
||||
})
|
||||
.strict()
|
||||
)
|
||||
.length(3)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrModelOperationSchema = z
|
||||
.object({
|
||||
modelId: localOcrModelIdSchema,
|
||||
kind: z.enum(['download', 'import']),
|
||||
phase: z.enum(['preparing', 'transferring', 'installing']),
|
||||
currentFile: z.string().min(1).max(255).nullable(),
|
||||
completedBytes: z.number().int().nonnegative().safe(),
|
||||
totalBytes: z.number().int().nonnegative().safe().nullable()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrModelSnapshotSchema = z
|
||||
.object({
|
||||
rootDirectory: z.string().min(1).max(32_768),
|
||||
catalog: z.array(documentOcrModelCatalogEntrySchema).max(16),
|
||||
installed: z.array(installedDocumentOcrModelSchema).max(16),
|
||||
operations: z.array(documentOcrModelOperationSchema).max(8)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrModelActionInputSchema = z
|
||||
.object({
|
||||
modelId: localOcrModelIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentParsingSettingsSchema = z
|
||||
.object({
|
||||
chatWorkflow: chatDocumentWorkflowSchema,
|
||||
knowledgeWorkflow: knowledgeDocumentWorkflowSchema,
|
||||
pdfOcrMode: pdfOcrModeSchema,
|
||||
ocrProvider: documentOcrProviderSchema,
|
||||
localOcrEnabled: z.boolean(),
|
||||
localOcrModelId: localOcrModelIdSchema,
|
||||
maximumPages: z.number().int().min(1).max(500),
|
||||
ocrConcurrency: z.number().int().min(1).max(4),
|
||||
pageTimeoutSeconds: z.number().int().min(10).max(300)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentParsingSettingsUpdateSchema =
|
||||
documentParsingSettingsSchema
|
||||
|
||||
export const documentParsingModelStatusSchema = z
|
||||
.object({
|
||||
id: localOcrModelIdSchema,
|
||||
displayName: z.string().trim().min(1).max(120),
|
||||
available: z.boolean(),
|
||||
verified: z.boolean(),
|
||||
runtime: z.literal('onnxruntime-web-wasm'),
|
||||
detail: z.string().trim().min(1).max(500)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentParsingStatusSchema = z
|
||||
.object({
|
||||
nativeParsingAvailable: z.literal(true),
|
||||
conversionAvailable: z.boolean(),
|
||||
localOcr: documentParsingModelStatusSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentParsingSnapshotSchema = z
|
||||
.object({
|
||||
settings: documentParsingSettingsSchema,
|
||||
status: documentParsingStatusSchema,
|
||||
ocrModels: documentOcrModelSnapshotSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentParsingTestInputSchema = z
|
||||
.object({
|
||||
purpose: documentParsingPurposeSchema.default('diagnostic')
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentParsingDiagnosticSchema = z
|
||||
.object({
|
||||
fileName: z.string().trim().min(1).max(500),
|
||||
sourceFormat: z.string().trim().min(1).max(32),
|
||||
pageCount: z.number().int().nonnegative().max(10_000),
|
||||
ocrPageCount: z.number().int().nonnegative().max(10_000),
|
||||
characterCount: z.number().int().nonnegative().safe(),
|
||||
method: z.enum(['native', 'ocr', 'mixed']),
|
||||
durationMs: z.number().int().nonnegative().safe(),
|
||||
preview: z.string().max(2_000),
|
||||
warnings: z.array(z.string().trim().min(1).max(500)).max(20)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrAssetsSchema = z
|
||||
.object({
|
||||
modelId: localOcrModelIdSchema,
|
||||
detection: z.instanceof(ArrayBuffer),
|
||||
recognition: z.instanceof(ArrayBuffer),
|
||||
dictionary: z.instanceof(ArrayBuffer)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrRequestSchema = z
|
||||
.object({
|
||||
requestId: z.string().uuid(),
|
||||
modelId: localOcrModelIdSchema,
|
||||
fileName: z.string().trim().min(1).max(500),
|
||||
mimeType: z.enum([
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp'
|
||||
]),
|
||||
data: z
|
||||
.instanceof(ArrayBuffer)
|
||||
.refine(
|
||||
(value) => value.byteLength > 0 && value.byteLength <= 20 * 1024 * 1024,
|
||||
'OCR 输入必须介于 1 字节和 20MB 之间'
|
||||
),
|
||||
maximumPages: z.number().int().min(1).max(500),
|
||||
pageNumbers: z
|
||||
.array(z.number().int().min(1).max(10_000))
|
||||
.min(1)
|
||||
.max(500)
|
||||
.optional(),
|
||||
pageTimeoutSeconds: z.number().int().min(10).max(300)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrSectionSchema = z
|
||||
.object({
|
||||
locator: z.string().trim().min(1).max(500),
|
||||
content: z.string().trim().min(1).max(1_000_000),
|
||||
confidence: z.number().min(0).max(1)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrResultSchema = z
|
||||
.object({
|
||||
requestId: z.string().uuid(),
|
||||
sections: z.array(documentOcrSectionSchema).max(500),
|
||||
pageCount: z.number().int().nonnegative().max(10_000),
|
||||
warnings: z.array(z.string().trim().min(1).max(500)).max(20)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const documentOcrFailureSchema = z
|
||||
.object({
|
||||
requestId: z.string().uuid(),
|
||||
error: z.string().trim().min(1).max(1_000)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type DocumentParsingPurpose = z.infer<
|
||||
typeof documentParsingPurposeSchema
|
||||
>
|
||||
export type DocumentParsingSettings = z.infer<
|
||||
typeof documentParsingSettingsSchema
|
||||
>
|
||||
export type DocumentParsingSnapshot = z.infer<
|
||||
typeof documentParsingSnapshotSchema
|
||||
>
|
||||
export type DocumentOcrModelFile = z.infer<
|
||||
typeof documentOcrModelFileSchema
|
||||
>
|
||||
export type DocumentOcrModelCatalogEntry = z.infer<
|
||||
typeof documentOcrModelCatalogEntrySchema
|
||||
>
|
||||
export type InstalledDocumentOcrModel = z.infer<
|
||||
typeof installedDocumentOcrModelSchema
|
||||
>
|
||||
export type DocumentOcrModelOperation = z.infer<
|
||||
typeof documentOcrModelOperationSchema
|
||||
>
|
||||
export type DocumentOcrModelSnapshot = z.infer<
|
||||
typeof documentOcrModelSnapshotSchema
|
||||
>
|
||||
export type DocumentParsingDiagnostic = z.infer<
|
||||
typeof documentParsingDiagnosticSchema
|
||||
>
|
||||
export type DocumentOcrAssets = z.infer<typeof documentOcrAssetsSchema>
|
||||
export type DocumentOcrRequest = z.infer<typeof documentOcrRequestSchema>
|
||||
export type DocumentOcrResult = z.infer<typeof documentOcrResultSchema>
|
||||
export type DocumentOcrFailure = z.infer<typeof documentOcrFailureSchema>
|
||||
@@ -48,7 +48,8 @@ export const ipcChannels = {
|
||||
speechModelsCancel: 'settings:speech-models:cancel',
|
||||
speechModelsRemove: 'settings:speech-models:remove',
|
||||
speechModelsSelect: 'settings:speech-models:select',
|
||||
speechModelsImportLocal: 'settings:speech-models:import-local',
|
||||
speechModelsImportArchive: 'settings:speech-models:import-archive',
|
||||
speechModelsExportArchive: 'settings:speech-models:export-archive',
|
||||
speechModelsOpenRepository: 'settings:speech-models:open-repository',
|
||||
speechModelsOpenDirectory: 'settings:speech-models:open-directory',
|
||||
speechTranscribe: 'speech:transcribe',
|
||||
@@ -58,6 +59,24 @@ export const ipcChannels = {
|
||||
embeddingIndexRebuild: 'settings:embedding:index:rebuild',
|
||||
embeddingIndexCancel: 'settings:embedding:index:cancel',
|
||||
embeddingIndexStatusChanged: 'settings:embedding:index:status-changed',
|
||||
documentParsingGet: 'settings:document-parsing:get',
|
||||
documentParsingUpdate: 'settings:document-parsing:update',
|
||||
documentParsingTest: 'settings:document-parsing:test',
|
||||
documentOcrModelsInstall: 'settings:document-ocr-models:install',
|
||||
documentOcrModelsCancel: 'settings:document-ocr-models:cancel',
|
||||
documentOcrModelsRemove: 'settings:document-ocr-models:remove',
|
||||
documentOcrModelsImportArchive:
|
||||
'settings:document-ocr-models:import-archive',
|
||||
documentOcrModelsExportArchive:
|
||||
'settings:document-ocr-models:export-archive',
|
||||
documentOcrModelsOpenRepository:
|
||||
'settings:document-ocr-models:open-repository',
|
||||
documentOcrModelsOpenDirectory:
|
||||
'settings:document-ocr-models:open-directory',
|
||||
documentParsingOcrAssets: 'document-parsing:ocr:assets',
|
||||
documentParsingOcrRequest: 'document-parsing:ocr:request',
|
||||
documentParsingOcrRespond: 'document-parsing:ocr:respond',
|
||||
documentParsingOcrCancel: 'document-parsing:ocr:cancel',
|
||||
projectsList: 'projects:list',
|
||||
projectsCreate: 'projects:create',
|
||||
projectsUpdate: 'projects:update',
|
||||
|
||||
Reference in New Issue
Block a user