From 931e6b72789832666bc5d338a00539a48b65efe1 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 23 Aug 2025 15:08:05 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor(ocr):=20=E9=87=8D=E6=9E=84OCR?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=92=8CAPI=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将OCR提供者配置拆分为独立类型,增加模型能力记录和API配置类型检查 添加OCR处理程序类型定义,为未来扩展提供更好的类型支持 --- src/renderer/src/types/ocr.ts | 72 ++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/types/ocr.ts b/src/renderer/src/types/ocr.ts index c7e53f24a6..e443e1597f 100644 --- a/src/renderer/src/types/ocr.ts +++ b/src/renderer/src/types/ocr.ts @@ -1,4 +1,4 @@ -import { FileMetadata, ImageFileMetadata, isImageFile, Model } from '.' +import { FileMetadata, ImageFileMetadata, isImageFile } from '.' export const BuiltinOcrProviderIds = { tesseract: 'tesseract' @@ -23,22 +23,70 @@ export const isOcrProviderCapability = (cap: string): cap is OcrProviderCapabili export type OcrProviderCapabilityRecord = Partial> +// OCR models and providers share the same type definition. +// A provider can offer capabilities to process multiple file types, +// while a model belonging to that provider may be limited to processing only one specific file type. +export type OcrModelCapabilityRecord = OcrProviderCapabilityRecord + +export interface OcrModel { + id: string + name: string + providerId: string + capabilities: OcrModelCapabilityRecord +} + +/** + * Extend this type to define provider-specefic config types. + */ +export type OcrProviderApiConfig = { + apiKey: string + apiHost: string + apiVersion?: string +} + +export const isOcrProviderApiConfig = (config: unknown): config is OcrProviderApiConfig => { + return ( + typeof config === 'object' && + config !== null && + 'apiKey' in config && + typeof config.apiKey === 'string' && + 'apiHost' in config && + typeof config.apiHost === 'string' && + (!('apiVersion' in config) || typeof config.apiVersion === 'string') + ) +} + +/** + * For future. Model based ocr, api based ocr. May different api client. + * + * Extend this type to define provider-specific config types. + */ +export type OcrProviderConfig = { + /** Not used for now. Could safely remove. */ + api?: OcrProviderApiConfig + /** Not used for now. Could safely remove. */ + models?: OcrModel[] + /** Not used for now. Could safely remove. */ + enabled?: boolean +} + export type OcrProvider = { id: string name: string capabilities: OcrProviderCapabilityRecord - config?: { - // for future. Model based ocr, api based ocr. May different api client. - api?: { - apiKey: string - apiHost: string - apiVersion?: string - } - models?: Model[] - enabled?: boolean + config?: OcrProviderConfig +} + +export type OcrApiProvider = OcrProvider & { + config: OcrProviderConfig & { + api: OcrProviderApiConfig } } +export const isOcrApiProvider = (p: OcrProvider): p is OcrApiProvider => { + return !!(p.config && p.config.api && isOcrProviderApiConfig(p.config.api)) +} + export type BuiltinOcrProvider = OcrProvider & { id: BuiltinOcrProviderId } @@ -71,3 +119,7 @@ export const isSupportedOcrFile = (file: FileMetadata): file is SupportedOcrFile export type OcrResult = { text: string } + +export type OcrHandler = (file: SupportedOcrFile) => Promise + +export type OcrImageHandler = (file: ImageFileMetadata) => Promise From be242d1308d042c37396611e28bb6191d07f8c63 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 23 Aug 2025 15:08:28 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor(OcrService):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E7=9A=84OcrHandler=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 已在@types中定义OcrHandler类型,移除重复定义以提高代码一致性 --- src/main/services/ocr/OcrService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/services/ocr/OcrService.ts b/src/main/services/ocr/OcrService.ts index 73907574b2..6ac8c311e3 100644 --- a/src/main/services/ocr/OcrService.ts +++ b/src/main/services/ocr/OcrService.ts @@ -1,10 +1,8 @@ import { loggerService } from '@logger' -import { BuiltinOcrProviderIds, OcrProvider, OcrResult, SupportedOcrFile } from '@types' +import { BuiltinOcrProviderIds, OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types' import { tesseractService } from './tesseract/TesseractService' -type OcrHandler = (file: SupportedOcrFile) => Promise - const logger = loggerService.withContext('OcrService') export class OcrService { From 4b031597ebe52d3a0257bef2082f6a589448e6e2 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 23 Aug 2025 15:10:21 +0800 Subject: [PATCH 3/5] =?UTF-8?q?refactor(ocr):=20=E5=B0=86OcrService?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E5=88=B0ocr=E7=9B=AE=E5=BD=95=E4=B8=8B?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E5=BC=95=E7=94=A8=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/hooks/useOcr.ts | 2 +- src/renderer/src/services/{ => ocr}/OcrService.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/renderer/src/services/{ => ocr}/OcrService.ts (100%) diff --git a/src/renderer/src/hooks/useOcr.ts b/src/renderer/src/hooks/useOcr.ts index 1efdc1961f..aadc291ad8 100644 --- a/src/renderer/src/hooks/useOcr.ts +++ b/src/renderer/src/hooks/useOcr.ts @@ -1,5 +1,5 @@ import { loggerService } from '@logger' -import * as OcrService from '@renderer/services/OcrService' +import * as OcrService from '@renderer/services/ocr/OcrService' import { useAppSelector } from '@renderer/store' import { ImageFileMetadata, isImageFile, SupportedOcrFile } from '@renderer/types' import { uuid } from '@renderer/utils' diff --git a/src/renderer/src/services/OcrService.ts b/src/renderer/src/services/ocr/OcrService.ts similarity index 100% rename from src/renderer/src/services/OcrService.ts rename to src/renderer/src/services/ocr/OcrService.ts From 31cf452974713b7d7eb05c64428bf025ec35ff69 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 23 Aug 2025 15:50:00 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat(ocr):=20=E6=B7=BB=E5=8A=A0OCR=20API?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=B7=A5=E5=8E=82=E5=8F=8A=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现OCR API客户端工厂模式,支持根据不同提供商创建对应的客户端 新增OcrBaseApiClient作为基础类,提供通用功能 添加OcrExampleApiClient作为示例实现 修改OcrService以使用新的客户端工厂 --- src/renderer/src/services/ocr/OcrService.ts | 11 ++++- .../ocr/clients/OcrApiClientFactory.ts | 28 ++++++++++++ .../services/ocr/clients/OcrBaseApiClient.ts | 43 +++++++++++++++++++ .../ocr/clients/OcrExampleApiClient.ts | 15 +++++++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/services/ocr/clients/OcrApiClientFactory.ts create mode 100644 src/renderer/src/services/ocr/clients/OcrBaseApiClient.ts create mode 100644 src/renderer/src/services/ocr/clients/OcrExampleApiClient.ts diff --git a/src/renderer/src/services/ocr/OcrService.ts b/src/renderer/src/services/ocr/OcrService.ts index cf06b4d79e..10ad4d5736 100644 --- a/src/renderer/src/services/ocr/OcrService.ts +++ b/src/renderer/src/services/ocr/OcrService.ts @@ -1,4 +1,6 @@ -import { OcrProvider, OcrResult, SupportedOcrFile } from '@renderer/types' +import { isOcrApiProvider, OcrProvider, OcrResult, SupportedOcrFile } from '@renderer/types' + +import { OcrApiClientFactory } from './clients/OcrApiClientFactory' // const logger = loggerService.withContext('renderer:OcrService') @@ -10,5 +12,10 @@ import { OcrProvider, OcrResult, SupportedOcrFile } from '@renderer/types' * @throws {Error} */ export const ocr = async (file: SupportedOcrFile, provider: OcrProvider): Promise => { - return window.api.ocr.ocr(file, provider) + if (isOcrApiProvider(provider)) { + const client = OcrApiClientFactory.create(provider) + return client.ocr(file) + } else { + return window.api.ocr.ocr(file, provider) + } } diff --git a/src/renderer/src/services/ocr/clients/OcrApiClientFactory.ts b/src/renderer/src/services/ocr/clients/OcrApiClientFactory.ts new file mode 100644 index 0000000000..e685c0e3f9 --- /dev/null +++ b/src/renderer/src/services/ocr/clients/OcrApiClientFactory.ts @@ -0,0 +1,28 @@ +import { loggerService } from '@logger' +import { OcrApiProvider } from '@renderer/types' + +import { OcrBaseApiClient } from './OcrBaseApiClient' +import { OcrExampleApiClient } from './OcrExampleApiClient' + +const logger = loggerService.withContext('OcrApiClientFactory') + +export class OcrApiClientFactory { + /** + * Create an ApiClient instance for the given provider + * 为给定的提供者创建ApiClient实例 + */ + static create(provider: OcrApiProvider): OcrBaseApiClient { + logger.debug(`Creating ApiClient for provider:`, { + id: provider.id, + config: provider.config + }) + + let instance: OcrBaseApiClient + + // Extend other clients here + // eslint-disable-next-line prefer-const + instance = new OcrExampleApiClient(provider) + + return instance + } +} diff --git a/src/renderer/src/services/ocr/clients/OcrBaseApiClient.ts b/src/renderer/src/services/ocr/clients/OcrBaseApiClient.ts new file mode 100644 index 0000000000..c9605671ae --- /dev/null +++ b/src/renderer/src/services/ocr/clients/OcrBaseApiClient.ts @@ -0,0 +1,43 @@ +import { OcrApiProvider, OcrHandler } from '@renderer/types' + +export abstract class OcrBaseApiClient { + public provider: OcrApiProvider + protected host: string + protected apiKey: string + + constructor(provider: OcrApiProvider) { + this.provider = provider + this.host = this.getHost() + this.apiKey = this.getApiKey() + } + + abstract ocr: OcrHandler + + // copy from BaseApiClient + public getHost(): string { + return this.provider.config.api.apiHost + } + + // copy from BaseApiClient + public getApiKey() { + const keys = this.provider.config.api.apiKey.split(',').map((key) => key.trim()) + const keyName = `ocr_provider:${this.provider.id}:last_used_key` + + if (keys.length === 1) { + return keys[0] + } + + const lastUsedKey = window.keyv.get(keyName) + if (!lastUsedKey) { + window.keyv.set(keyName, keys[0]) + return keys[0] + } + + const currentIndex = keys.indexOf(lastUsedKey) + const nextIndex = (currentIndex + 1) % keys.length + const nextKey = keys[nextIndex] + window.keyv.set(keyName, nextKey) + + return nextKey + } +} diff --git a/src/renderer/src/services/ocr/clients/OcrExampleApiClient.ts b/src/renderer/src/services/ocr/clients/OcrExampleApiClient.ts new file mode 100644 index 0000000000..34d28173bb --- /dev/null +++ b/src/renderer/src/services/ocr/clients/OcrExampleApiClient.ts @@ -0,0 +1,15 @@ +import { OcrApiProvider, SupportedOcrFile } from '@renderer/types' + +import { OcrBaseApiClient } from './OcrBaseApiClient' + +export type OcrExampleProvider = OcrApiProvider + +export class OcrExampleApiClient extends OcrBaseApiClient { + constructor(provider: OcrApiProvider) { + super(provider) + } + + public ocr = async (file: SupportedOcrFile) => { + return { text: `Example output: ${file.path}` } + } +} From 4e30d89e1c12e0719f28de0afe9a25903bfdaca7 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 23 Aug 2025 15:51:42 +0800 Subject: [PATCH 5/5] =?UTF-8?q?refactor(ocr):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E4=BB=A5=E8=B7=9F=E8=B8=AA?= =?UTF-8?q?OCR=E6=96=87=E4=BB=B6=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在OCR服务中添加日志记录功能,便于跟踪文件处理过程 --- src/renderer/src/services/ocr/OcrService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/services/ocr/OcrService.ts b/src/renderer/src/services/ocr/OcrService.ts index 10ad4d5736..3d8339f6e3 100644 --- a/src/renderer/src/services/ocr/OcrService.ts +++ b/src/renderer/src/services/ocr/OcrService.ts @@ -1,8 +1,9 @@ +import { loggerService } from '@logger' import { isOcrApiProvider, OcrProvider, OcrResult, SupportedOcrFile } from '@renderer/types' import { OcrApiClientFactory } from './clients/OcrApiClientFactory' -// const logger = loggerService.withContext('renderer:OcrService') +const logger = loggerService.withContext('renderer:OcrService') /** * ocr a file @@ -12,6 +13,7 @@ import { OcrApiClientFactory } from './clients/OcrApiClientFactory' * @throws {Error} */ export const ocr = async (file: SupportedOcrFile, provider: OcrProvider): Promise => { + logger.info(`ocr file ${file.path}`) if (isOcrApiProvider(provider)) { const client = OcrApiClientFactory.create(provider) return client.ocr(file)