feat: add document OCR and offline model archives

This commit is contained in:
lofyer
2026-08-11 16:49:51 +08:00
parent 19a4469561
commit e0e7bc573c
46 changed files with 7831 additions and 85 deletions
+310
View File
@@ -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>