diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 8c9efbe68..f5a8e7104 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ '@cherrystudio/embedjs-loader-sitemap', '@cherrystudio/embedjs-libsql', '@cherrystudio/embedjs-loader-image', + '@cherrystudio/mac-system-ocr', 'p-queue', 'webdav' ] diff --git a/src/main/ocr/MacSysOcrProvider.ts b/src/main/ocr/MacSysOcrProvider.ts index ac27d20b0..032759624 100644 --- a/src/main/ocr/MacSysOcrProvider.ts +++ b/src/main/ocr/MacSysOcrProvider.ts @@ -1,4 +1,3 @@ -import MacOCR from '@cherrystudio/mac-system-ocr' import { FileSource, isLocalFile, LocalFileSource, OcrProvider } from '@types' import Logger from 'electron-log' import * as fs from 'fs' @@ -12,9 +11,23 @@ export default class MacSysOcrProvider extends BaseOcrProvider { private readonly BATCH_SIZE = 4 private readonly CONCURRENCY = 2 private readonly MIN_TEXT_LENGTH = 1000 + private MacOCR: any + + private async initMacOCR() { + if (!this.MacOCR) { + try { + const module = await import('@cherrystudio/mac-system-ocr') + this.MacOCR = module.default + } catch (error) { + Logger.error('[OCR] Failed to load mac-system-ocr:', error) + throw error + } + } + return this.MacOCR + } private getRecognitionLevel(level?: number) { - return level === 0 ? MacOCR.RECOGNITION_LEVEL_FAST : MacOCR.RECOGNITION_LEVEL_ACCURATE + return level === 0 ? this.MacOCR.RECOGNITION_LEVEL_FAST : this.MacOCR.RECOGNITION_LEVEL_ACCURATE } constructor(provider: OcrProvider) { @@ -29,6 +42,7 @@ export default class MacSysOcrProvider extends BaseOcrProvider { ): Promise { const queue = new PQueue({ concurrency: this.CONCURRENCY }) const batches: Promise[] = [] + await this.initMacOCR() // Create ordered batches for (let startPage = 0; startPage < totalPages; startPage += this.BATCH_SIZE) { @@ -44,7 +58,7 @@ export default class MacSysOcrProvider extends BaseOcrProvider { } // Process batch - const ocrResults = await MacOCR.recognizeBatchFromBuffer(pageBuffers, { + const ocrResults = await this.MacOCR.recognizeBatchFromBuffer(pageBuffers, { maxThreads: 4, ocrOptions: { recognitionLevel: this.getRecognitionLevel(this.provider.options?.recognitionLevel), diff --git a/src/main/services/file/GeminiService.ts b/src/main/services/file/GeminiService.ts index abb8bf606..f3251a552 100644 --- a/src/main/services/file/GeminiService.ts +++ b/src/main/services/file/GeminiService.ts @@ -39,7 +39,7 @@ export class GeminiService extends BaseFileService { } const response: FileUploadResponse = { - fileId: uploadResult.file.name, + fileId: uploadResult.file.name || '', displayName: file.origin_name, status, originalFile: uploadResult @@ -89,7 +89,7 @@ export class GeminiService extends BaseFileService { } const response = await this.fileManager.listFiles() const fileList: FileListResponse = { - files: response.files + files: (response.files || []) .filter((file) => file.state === FileState.ACTIVE) .map((file) => { // 更新单个文件的缓存 diff --git a/src/main/services/file/MistralService.ts b/src/main/services/file/MistralService.ts index 863d5ec25..ab8086ddc 100644 --- a/src/main/services/file/MistralService.ts +++ b/src/main/services/file/MistralService.ts @@ -22,7 +22,7 @@ export class MistralService extends BaseFileService { const fileBuffer = await fs.readFile(file.path) const response = await this.client.files.upload({ file: { - fileName: file.path, + fileName: file.origin_name, content: new Uint8Array(fileBuffer) }, purpose: 'ocr' @@ -50,12 +50,13 @@ export class MistralService extends BaseFileService { files: response.data.map((file) => ({ id: file.id, displayName: file.filename || '', - size: 0, // Size information not available in SDK response - status: 'success' // All listed files are processed + size: file.sizeBytes, + status: 'success', // All listed files are processed, + originalFile: file })) } } catch (error) { - console.error('Error listing files:', error) + Logger.error('Error listing files:', error) return { files: [] } } } @@ -65,8 +66,9 @@ export class MistralService extends BaseFileService { await this.client.files.delete({ fileId }) + Logger.info(`File ${fileId} deleted`) } catch (error) { - console.error('Error deleting file:', error) + Logger.error('Error deleting file:', error) throw error } } @@ -83,7 +85,7 @@ export class MistralService extends BaseFileService { status: 'success' // Retrieved files are always processed } } catch (error) { - console.error('Error retrieving file:', error) + Logger.error('Error retrieving file:', error) return { fileId: fileId, displayName: '', diff --git a/src/renderer/src/pages/files/ContentView.tsx b/src/renderer/src/pages/files/ContentView.tsx index 6e3d34283..33479c7db 100644 --- a/src/renderer/src/pages/files/ContentView.tsx +++ b/src/renderer/src/pages/files/ContentView.tsx @@ -6,7 +6,7 @@ import React, { memo } from 'react' import styled from 'styled-components' import GeminiFiles from './GeminiFiles' - +import MistralFiles from './MistralFiles' interface ContentViewProps { id: FileTypes | 'all' | string files?: FileType[] @@ -49,6 +49,10 @@ const ContentView: React.FC = ({ id, files, dataSource, column return } + if (id.startsWith('mistral_')) { + return + } + return ( { const { providers } = useProviders() const geminiProviders = providers.filter((provider) => provider.type === 'gemini') - + const mistralProviders = providers.filter((provider) => provider.type === 'mistral') const tempFilesSort = (files: FileType[]) => { return files.sort((a, b) => { const aIsTemp = a.origin_name.startsWith('temp_file') @@ -187,6 +187,11 @@ const FilesPage: FC = () => { label: provider.name, icon: })), + ...mistralProviders.map((provider) => ({ + key: 'mistral_' + provider.id, + label: provider.name, + icon: + })), { key: 'all', label: t('files.all'), icon: } ].filter(Boolean) as MenuProps['items'] diff --git a/src/renderer/src/pages/files/MistralFiles.tsx b/src/renderer/src/pages/files/MistralFiles.tsx index e69de29bb..2eed190e2 100644 --- a/src/renderer/src/pages/files/MistralFiles.tsx +++ b/src/renderer/src/pages/files/MistralFiles.tsx @@ -0,0 +1,99 @@ +import { DeleteOutlined } from '@ant-design/icons' +import { type FileSchema } from '@mistralai/mistralai/src/models/components' +import { useProvider } from '@renderer/hooks/useProvider' +import { runAsyncFunction } from '@renderer/utils' +import { Table } from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { FC, useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +interface MistralFilesProps { + id: string +} + +const MistralFiles: FC = ({ id }) => { + const { provider } = useProvider(id) + const { t } = useTranslation() + const [files, setFiles] = useState([]) + const [loading, setLoading] = useState(false) + + const fetchFiles = useCallback(async () => { + const response = await window.api.fileService.list(provider.type, provider.apiKey) + const files = response.files.map((file) => file.originalFile as FileSchema) + setFiles(files) + }, [provider]) + + const columns: ColumnsType = [ + { + title: t('files.name'), + dataIndex: 'filename', + key: 'filename' + }, + { + title: t('files.type'), + dataIndex: 'object', + key: 'object' + }, + { + title: t('files.size'), + dataIndex: 'sizeBytes', + key: 'sizeBytes', + render: (size: string) => `${(parseInt(size) / 1024 / 1024).toFixed(2)} MB` + }, + { + title: t('files.created_at'), + dataIndex: 'createdAt', + key: 'createdAt', + render: (time: number) => new Date(time * 1000).toLocaleString() + }, + { + title: t('files.actions'), + dataIndex: 'actions', + key: 'actions', + align: 'center', + render: (_, record) => { + return ( + { + setFiles(files.filter((file) => file.id !== record.id)) + window.api.fileService.delete(provider.type, provider.apiKey, record.id).catch((error) => { + console.error('Failed to delete file:', error) + setFiles((prev) => [...prev, record]) + }) + }} + /> + ) + } + } + ] + + useEffect(() => { + runAsyncFunction(async () => { + try { + setLoading(true) + await fetchFiles() + setLoading(false) + } catch (error: any) { + console.error('Failed to fetch files:', error) + window.message.error(error.message) + setLoading(false) + } + }) + }, [fetchFiles]) + + useEffect(() => { + setFiles([]) + }, [id]) + + return ( + +
+ + ) +} + +const Container = styled.div`` + +export default MistralFiles diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index fdc64cec3..c1ce49abc 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -849,6 +849,12 @@ const migrateConfig = { name: 'System(Mac Only)' }) } + + state.llm.providers.forEach((provider) => { + if (provider.id === 'mistral') { + provider.type = 'mistral' + } + }) return state } } diff --git a/yarn.lock b/yarn.lock index 44f0ae0cf..6ae1847b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3891,6 +3891,9 @@ __metadata: vite: "npm:^5.0.12" webdav: "npm:^5.8.0" zipread: "npm:^1.3.3" + dependenciesMeta: + "@cherrystudio/mac-system-ocr": + optional: true languageName: unknown linkType: soft