From 64838cb3fb8bbad334c47fe5bc0ce0d7087e0d2f Mon Sep 17 00:00:00 2001 From: suyao Date: Mon, 24 Mar 2025 01:47:57 +0800 Subject: [PATCH] refactor(fileService): integrate Mistral and Gemini file services - Added Mistral and Gemini file services to handle file uploads, retrieval, listing, and deletion. - Refactored IPC handlers to support new file service architecture. - Updated preload API to accommodate new file service methods. - Removed deprecated Gemini service implementation. - Enhanced type definitions for file handling to support local and remote sources. --- package.json | 3 +- src/main/ipc.ts | 32 ++-- src/main/ocr/BaseOcrProvider.ts | 17 +-- src/main/ocr/DefaultOcrProvider.ts | 6 +- src/main/ocr/Doc2xOcrProvider.ts | 16 +- src/main/ocr/OcrProviderFactory.ts | 14 +- src/main/services/GeminiService.ts | 63 -------- src/main/services/MistralClientManager.ts | 31 ++++ src/main/services/file/BaseFileService.ts | 13 ++ src/main/services/file/FileServiceManager.ts | 37 +++++ src/main/services/file/GeminiService.ts | 141 ++++++++++++++++++ src/main/services/file/MistralService.ts | 84 +++++++++++ src/preload/index.d.ts | 11 +- src/preload/index.ts | 14 +- src/renderer/src/pages/files/GeminiFiles.tsx | 4 +- src/renderer/src/pages/files/MistralFiles.tsx | 0 .../src/pages/knowledge/KnowledgeContent.tsx | 3 +- src/renderer/src/providers/GeminiProvider.ts | 7 +- src/renderer/src/types/index.ts | 57 ++++++- src/renderer/src/utils/file.ts | 9 ++ yarn.lock | 12 ++ 21 files changed, 450 insertions(+), 124 deletions(-) delete mode 100644 src/main/services/GeminiService.ts create mode 100644 src/main/services/MistralClientManager.ts create mode 100644 src/main/services/file/BaseFileService.ts create mode 100644 src/main/services/file/FileServiceManager.ts create mode 100644 src/main/services/file/GeminiService.ts create mode 100644 src/main/services/file/MistralService.ts create mode 100644 src/renderer/src/pages/files/MistralFiles.tsx create mode 100644 src/renderer/src/utils/file.ts diff --git a/package.json b/package.json index bd308d83f..82dbf4f7d 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@llm-tools/embedjs-loader-web": "^0.1.28", "@llm-tools/embedjs-loader-xml": "^0.1.28", "@llm-tools/embedjs-openai": "^0.1.28", + "@mistralai/mistralai": "^1.5.2", "@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch", "adm-zip": "^0.5.16", "docx": "^9.0.2", @@ -109,11 +110,11 @@ "@types/md5": "^2.3.5", "@types/node": "^18.19.9", "@types/pako": "^1.0.2", + "@types/pdf-parse": "^1.1.4", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "@types/react-infinite-scroll-component": "^5.0.0", "@types/tinycolor2": "^1", - "@types/pdf-parse": "^1.1.4", "@vitejs/plugin-react": "^4.2.1", "antd": "^5.22.5", "applescript": "^1.0.0", diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 224f91d3a..3c214e1aa 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -2,7 +2,7 @@ import fs from 'node:fs' import { isMac, isWin } from '@main/constant' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' -import { MCPServer, Shortcut, ThemeMode } from '@types' +import { LocalFileSource, MCPServer, Shortcut, ThemeMode } from '@types' import { BrowserWindow, ipcMain, session, shell } from 'electron' import log from 'electron-log' @@ -12,9 +12,9 @@ import BackupManager from './services/BackupManager' import { configManager } from './services/ConfigManager' import CopilotService from './services/CopilotService' import { ExportService } from './services/ExportService' +import { FileServiceManager } from './services/file/FileServiceManager' import FileService from './services/FileService' import FileStorage from './services/FileStorage' -import { GeminiService } from './services/GeminiService' import KnowledgeService from './services/KnowledgeService' import MCPService from './services/MCPService' import { ProxyConfig, proxyManager } from './services/ProxyManager' @@ -185,6 +185,27 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle('file:copy', fileManager.copyFile) ipcMain.handle('file:binaryFile', fileManager.binaryFile) + // file service + ipcMain.handle('file-service:upload', async (_, type: string, apiKey: string, file: LocalFileSource) => { + const service = FileServiceManager.getInstance().getService(type, apiKey) + return await service.uploadFile(file) + }) + + ipcMain.handle('file-service:list', async (_, type: string, apiKey: string) => { + const service = FileServiceManager.getInstance().getService(type, apiKey) + return await service.listFiles() + }) + + ipcMain.handle('file-service:delete', async (_, type: string, apiKey: string, fileId: string) => { + const service = FileServiceManager.getInstance().getService(type, apiKey) + return await service.deleteFile(fileId) + }) + + ipcMain.handle('file-service:retrieve', async (_, type: string, apiKey: string, fileId: string) => { + const service = FileServiceManager.getInstance().getService(type, apiKey) + return await service.retrieveFile(fileId) + }) + // fs ipcMain.handle('fs:read', FileService.readFile) @@ -240,13 +261,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { } }) - // gemini - ipcMain.handle('gemini:upload-file', GeminiService.uploadFile) - ipcMain.handle('gemini:base64-file', GeminiService.base64File) - ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile) - ipcMain.handle('gemini:list-files', GeminiService.listFiles) - ipcMain.handle('gemini:delete-file', GeminiService.deleteFile) - // mini window ipcMain.handle('miniwindow:show', () => windowService.showMiniWindow()) ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow()) diff --git a/src/main/ocr/BaseOcrProvider.ts b/src/main/ocr/BaseOcrProvider.ts index 7c5837e23..b165f9ff2 100644 --- a/src/main/ocr/BaseOcrProvider.ts +++ b/src/main/ocr/BaseOcrProvider.ts @@ -1,21 +1,18 @@ import fs from 'node:fs' import { windowService } from '@main/services/WindowService' -import { FileType, KnowledgeBaseParams } from '@types' +import { FileSource, OcrProvider } from '@types' import Logger from 'electron-log' import pdfParse from 'pdf-parse' export default abstract class BaseOcrProvider { - protected base: KnowledgeBaseParams - constructor(base: KnowledgeBaseParams) { - if (!base) { - throw new Error('KnowledgeBaseParams is required') + protected provider: OcrProvider + constructor(provider: OcrProvider) { + if (!provider) { + throw new Error('Ocr provider is not set') } - if (!base.ocrProvider || base.ocrProvider?.apiKey === '') { - throw new Error('Ocr provider is not set or apiKey is empty') - } - this.base = base + this.provider = provider } - abstract parseFile(sourceId: string, file: FileType): Promise<{ processedFile: FileType }> + abstract parseFile(sourceId: string, file: FileSource): Promise<{ processedFile: FileSource }> /** * 辅助方法:延迟执行 */ diff --git a/src/main/ocr/DefaultOcrProvider.ts b/src/main/ocr/DefaultOcrProvider.ts index 9f5e892cb..c35390246 100644 --- a/src/main/ocr/DefaultOcrProvider.ts +++ b/src/main/ocr/DefaultOcrProvider.ts @@ -1,10 +1,10 @@ -import { FileType, KnowledgeBaseParams } from '@types' +import { FileType, OcrProvider } from '@types' import BaseOcrProvider from './BaseOcrProvider' export default class DefaultOcrProvider extends BaseOcrProvider { - constructor(base: KnowledgeBaseParams) { - super(base) + constructor(provider: OcrProvider) { + super(provider) } public parseFile(): Promise<{ processedFile: FileType }> { throw new Error('Method not implemented.') diff --git a/src/main/ocr/Doc2xOcrProvider.ts b/src/main/ocr/Doc2xOcrProvider.ts index 90f820aa9..d4df2c11d 100644 --- a/src/main/ocr/Doc2xOcrProvider.ts +++ b/src/main/ocr/Doc2xOcrProvider.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import path from 'node:path' -import { FileType, KnowledgeBaseParams } from '@types' +import { FileType, OcrProvider } from '@types' import AdmZip from 'adm-zip' import axios, { AxiosRequestConfig } from 'axios' import Logger from 'electron-log' @@ -30,8 +30,8 @@ type ParsedFileResponse = { } export default class Doc2xOcrProvider extends BaseOcrProvider { - constructor(base: KnowledgeBaseParams) { - super(base) + constructor(provider: OcrProvider) { + super(provider) } public async parseFile(sourceId: string, file: FileType): Promise<{ processedFile: FileType }> { @@ -130,7 +130,7 @@ export default class Doc2xOcrProvider extends BaseOcrProvider { private async preupload(): Promise { const config = this.createAuthConfig() - const endpoint = `${this.base.ocrProvider?.apiHost}/api/v2/parse/preupload` + const endpoint = `${this.provider.apiHost}/api/v2/parse/preupload` try { const { data } = await axios.post>(endpoint, null, config) @@ -162,7 +162,7 @@ export default class Doc2xOcrProvider extends BaseOcrProvider { private async getStatus(uid: string): Promise { const config = this.createAuthConfig() - const endpoint = `${this.base.ocrProvider?.apiHost}/api/v2/parse/status?uid=${uid}` + const endpoint = `${this.provider.apiHost}/api/v2/parse/status?uid=${uid}` try { const response = await axios.get>(endpoint, config) @@ -195,7 +195,7 @@ export default class Doc2xOcrProvider extends BaseOcrProvider { filename: fileName } - const endpoint = `${this.base.ocrProvider?.apiHost}/api/v2/convert/parse` + const endpoint = `${this.provider.apiHost}/api/v2/convert/parse` try { const response = await axios.post>(endpoint, payload, config) @@ -211,7 +211,7 @@ export default class Doc2xOcrProvider extends BaseOcrProvider { private async getParsedFile(uid: string): Promise { const config = this.createAuthConfig() - const endpoint = `${this.base.ocrProvider?.apiHost}/api/v2/convert/parse/result?uid=${uid}` + const endpoint = `${this.provider.apiHost}/api/v2/convert/parse/result?uid=${uid}` try { const response = await axios.get>(endpoint, config) @@ -259,7 +259,7 @@ export default class Doc2xOcrProvider extends BaseOcrProvider { private createAuthConfig(): AxiosRequestConfig { return { headers: { - Authorization: `Bearer ${this.base.ocrProvider?.apiKey}` + Authorization: `Bearer ${this.provider.apiKey}` } } } diff --git a/src/main/ocr/OcrProviderFactory.ts b/src/main/ocr/OcrProviderFactory.ts index 2384dce28..cf8df1675 100644 --- a/src/main/ocr/OcrProviderFactory.ts +++ b/src/main/ocr/OcrProviderFactory.ts @@ -1,14 +1,16 @@ -import { KnowledgeBaseParams } from '@types' +import { OcrProvider } from '@types' import BaseOcrProvider from './BaseOcrProvider' import DefaultOcrProvider from './DefaultOcrProvider' import Doc2xOcrProvider from './Doc2xOcrProvider' - +import MistralOcrProvider from './MistralOcrProvider' export default class OcrProviderFactory { - static create(base: KnowledgeBaseParams): BaseOcrProvider { - if (base.ocrProvider?.id === 'doc2x') { - return new Doc2xOcrProvider(base) + static create(provider: OcrProvider): BaseOcrProvider { + if (provider.id === 'doc2x') { + return new Doc2xOcrProvider(provider) + } else if (provider.id === 'mistral') { + return new MistralOcrProvider(provider) } - return new DefaultOcrProvider(base) + return new DefaultOcrProvider(provider) } } diff --git a/src/main/services/GeminiService.ts b/src/main/services/GeminiService.ts deleted file mode 100644 index b79193ff0..000000000 --- a/src/main/services/GeminiService.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { FileMetadataResponse, FileState, GoogleAIFileManager } from '@google/generative-ai/server' -import { FileType } from '@types' -import fs from 'fs' - -import { CacheService } from './CacheService' - -export class GeminiService { - private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list' - private static readonly CACHE_DURATION = 3000 - - static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) { - const fileManager = new GoogleAIFileManager(apiKey) - const uploadResult = await fileManager.uploadFile(file.path, { - mimeType: 'application/pdf', - displayName: file.origin_name - }) - return uploadResult - } - - static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) { - return { - data: Buffer.from(fs.readFileSync(file.path)).toString('base64'), - mimeType: 'application/pdf' - } - } - - static async retrieveFile( - _: Electron.IpcMainInvokeEvent, - file: FileType, - apiKey: string - ): Promise { - const fileManager = new GoogleAIFileManager(apiKey) - - const cachedResponse = CacheService.get(GeminiService.FILE_LIST_CACHE_KEY) - if (cachedResponse) { - return GeminiService.processResponse(cachedResponse, file) - } - - const response = await fileManager.listFiles() - CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION) - - return GeminiService.processResponse(response, file) - } - - private static processResponse(response: any, file: FileType) { - if (response.files) { - return response.files - .filter((file) => file.state === FileState.ACTIVE) - .find((i) => i.displayName === file.origin_name && Number(i.sizeBytes) === file.size) - } - return undefined - } - - static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) { - const fileManager = new GoogleAIFileManager(apiKey) - return await fileManager.listFiles() - } - - static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) { - const fileManager = new GoogleAIFileManager(apiKey) - await fileManager.deleteFile(fileId) - } -} diff --git a/src/main/services/MistralClientManager.ts b/src/main/services/MistralClientManager.ts new file mode 100644 index 000000000..2b132927d --- /dev/null +++ b/src/main/services/MistralClientManager.ts @@ -0,0 +1,31 @@ +import { Mistral } from '@mistralai/mistralai' + +export class MistralClientManager { + private static instance: MistralClientManager + private client: Mistral | null = null + + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + + public static getInstance(): MistralClientManager { + if (!MistralClientManager.instance) { + MistralClientManager.instance = new MistralClientManager() + } + return MistralClientManager.instance + } + + public initializeClient(apiKey: string): void { + if (!this.client) { + this.client = new Mistral({ + apiKey + }) + } + } + + public getClient(): Mistral { + if (!this.client) { + throw new Error('Mistral client not initialized. Call initializeClient first.') + } + return this.client + } +} diff --git a/src/main/services/file/BaseFileService.ts b/src/main/services/file/BaseFileService.ts new file mode 100644 index 000000000..8a39aa95a --- /dev/null +++ b/src/main/services/file/BaseFileService.ts @@ -0,0 +1,13 @@ +import { FileListResponse, FileUploadResponse, LocalFileSource } from '@types' + +export abstract class BaseFileService { + protected readonly apiKey: string + protected constructor(apiKey: string) { + this.apiKey = apiKey + } + + abstract uploadFile(file: LocalFileSource): Promise + abstract deleteFile(fileId: string): Promise + abstract listFiles(): Promise + abstract retrieveFile(fileId: string): Promise +} diff --git a/src/main/services/file/FileServiceManager.ts b/src/main/services/file/FileServiceManager.ts new file mode 100644 index 000000000..7ebdbe922 --- /dev/null +++ b/src/main/services/file/FileServiceManager.ts @@ -0,0 +1,37 @@ +import { BaseFileService } from './BaseFileService' +import { GeminiService } from './GeminiService' +import { MistralService } from './MistralService' + +export class FileServiceManager { + private static instance: FileServiceManager + private services: Map = new Map() + + private constructor() {} + + static getInstance(): FileServiceManager { + if (!this.instance) { + this.instance = new FileServiceManager() + } + return this.instance + } + + getService(type: string, apiKey: string): BaseFileService { + let service = this.services.get(type) + + if (!service) { + switch (type) { + case 'gemini': + service = new GeminiService(apiKey) + break + case 'mistral': + service = new MistralService(apiKey) + break + default: + throw new Error(`Unsupported service type: ${type}`) + } + this.services.set(type, service) + } + + return service + } +} diff --git a/src/main/services/file/GeminiService.ts b/src/main/services/file/GeminiService.ts new file mode 100644 index 000000000..b832f51e6 --- /dev/null +++ b/src/main/services/file/GeminiService.ts @@ -0,0 +1,141 @@ +import { FileState, GoogleAIFileManager } from '@google/generative-ai/server' +import { FileListResponse, FileUploadResponse, LocalFileSource } from '@types' + +import { CacheService } from '../CacheService' +import { BaseFileService } from './BaseFileService' + +export class GeminiService extends BaseFileService { + private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list' + private static readonly FILE_CACHE_DURATION = 48 * 60 * 60 * 1000 + private static readonly LIST_CACHE_DURATION = 3000 + + protected readonly fileManager: GoogleAIFileManager + + constructor(apiKey: string) { + super(apiKey) + this.fileManager = new GoogleAIFileManager(apiKey) + } + + async uploadFile(file: LocalFileSource): Promise { + const uploadResult = await this.fileManager.uploadFile(file.path, { + mimeType: 'application/pdf', + displayName: file.origin_name + }) + + // 根据文件状态设置响应状态 + let status: 'success' | 'processing' | 'failed' | 'unknown' + switch (uploadResult.file.state) { + case FileState.ACTIVE: + status = 'success' + break + case FileState.PROCESSING: + status = 'processing' + break + case FileState.FAILED: + status = 'failed' + break + default: + status = 'unknown' + } + + const response: FileUploadResponse = { + fileId: uploadResult.file.name, + displayName: file.origin_name, + status, + originalFile: uploadResult + } + + // 只缓存成功的文件 + if (status === 'success') { + const cacheKey = `${GeminiService.FILE_LIST_CACHE_KEY}_${response.fileId}` + CacheService.set(cacheKey, response, GeminiService.FILE_CACHE_DURATION) + } + + return response + } + + async retrieveFile(fileId: string): Promise { + const cachedResponse = CacheService.get(`${GeminiService.FILE_LIST_CACHE_KEY}_${fileId}`) + if (cachedResponse) { + return cachedResponse + } + + const response = await this.fileManager.getFile(fileId) + + // 根据文件状态设置响应状态 + let status: 'success' | 'processing' | 'failed' | 'unknown' + switch (response.state) { + case FileState.ACTIVE: + status = 'success' + break + case FileState.PROCESSING: + status = 'processing' + break + case FileState.FAILED: + status = 'failed' + break + default: + status = 'unknown' + } + + const fileResponse: FileUploadResponse = { + fileId, + displayName: response.displayName || '', + status, + originalFile: response + } + + // 只缓存成功的文件 + if (status === 'success') { + CacheService.set( + `${GeminiService.FILE_LIST_CACHE_KEY}_${fileId}`, + fileResponse, + GeminiService.FILE_CACHE_DURATION + ) + } + + return fileResponse + } + + async listFiles(): Promise { + const cachedList = CacheService.get(GeminiService.FILE_LIST_CACHE_KEY) + if (cachedList) { + return cachedList + } + const response = await this.fileManager.listFiles() + const fileList: FileListResponse = { + files: response.files + .filter((file) => file.state === FileState.ACTIVE) + .map((file) => { + // 更新单个文件的缓存 + const fileResponse: FileUploadResponse = { + fileId: file.name, + displayName: file.displayName || '', + status: 'success', + originalFile: file + } + CacheService.set( + `${GeminiService.FILE_LIST_CACHE_KEY}_${file.name}`, + fileResponse, + GeminiService.FILE_CACHE_DURATION + ) + + return { + id: file.name, + displayName: file.displayName || '', + size: Number(file.sizeBytes), + status: 'success', + originalFile: file + } + }) + } + + // 更新文件列表缓存 + CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, fileList, GeminiService.LIST_CACHE_DURATION) + return fileList + } + + async deleteFile(fileId: string): Promise { + await this.fileManager.deleteFile(fileId) + } +} diff --git a/src/main/services/file/MistralService.ts b/src/main/services/file/MistralService.ts new file mode 100644 index 000000000..88df2c918 --- /dev/null +++ b/src/main/services/file/MistralService.ts @@ -0,0 +1,84 @@ +import { FileListResponse, FileUploadResponse, LocalFileSource } from '@types' +import { fileFrom } from 'fetch-blob/from.js' + +import { MistralClientManager } from '../MistralClientManager' +import { BaseFileService } from './BaseFileService' + +export class MistralService extends BaseFileService { + private readonly client + + constructor(apiKey: string) { + super(apiKey) + const clientManager = MistralClientManager.getInstance() + clientManager.initializeClient(apiKey) + this.client = clientManager.getClient() + } + + async uploadFile(file: LocalFileSource): Promise { + try { + const blob = await fileFrom(file.path) + const response = await this.client.files.upload({ + file: blob, + purpose: 'ocr' + }) + + return { + fileId: response.id, + displayName: file.origin_name, + status: 'success' + } + } catch (error) { + console.error('Error uploading file:', error) + return { + fileId: '', + displayName: file.origin_name, + status: 'failed' + } + } + } + + async listFiles(): Promise { + try { + const response = await this.client.files.list({}) + return { + 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 + })) + } + } catch (error) { + console.error('Error listing files:', error) + return { files: [] } + } + } + + async deleteFile(fileId: string): Promise { + try { + await this.client.files.delete({ + fileId + }) + } catch (error) { + console.error('Error deleting file:', error) + throw error + } + } + + async retrieveFile(fileId: string): Promise { + try { + const response = await this.client.files.retrieve({ + fileId + }) + + return { + fileId: response.id, + displayName: response.filename || '', + status: 'success' // Retrieved files are always processed + } + } catch (error) { + console.error('Error retrieving file:', error) + throw error + } + } +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index febaf299e..bcbb211ad 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -117,12 +117,11 @@ declare global { setMinimumSize: (width: number, height: number) => Promise resetMinimumSize: () => Promise } - gemini: { - uploadFile: (file: FileType, apiKey: string) => Promise - retrieveFile: (file: FileType, apiKey: string) => Promise - base64File: (file: FileType) => Promise<{ data: string; mimeType: string }> - listFiles: (apiKey: string) => Promise - deleteFile: (apiKey: string, fileId: string) => Promise + fileService: { + upload: (type: string, apiKey: string, file: FileType) => Promise + retrieve: (type: string, apiKey: string, fileId: string) => Promise + list: (type: string, apiKey: string) => Promise + delete: (type: string, apiKey: string, fileId: string) => Promise } selectionMenu: { action: (action: string) => Promise diff --git a/src/preload/index.ts b/src/preload/index.ts index 3eeb24980..1b4dfb9da 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -91,12 +91,14 @@ const api = { setMinimumSize: (width: number, height: number) => ipcRenderer.invoke('window:set-minimum-size', width, height), resetMinimumSize: () => ipcRenderer.invoke('window:reset-minimum-size') }, - gemini: { - uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:upload-file', file, apiKey), - base64File: (file: FileType) => ipcRenderer.invoke('gemini:base64-file', file), - retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey), - listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey), - deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId) + fileService: { + upload: (type: string, apiKey: string, file: FileType) => + ipcRenderer.invoke('file-service:upload', type, apiKey, file), + list: (type: string, apiKey: string) => ipcRenderer.invoke('file-service:list', type, apiKey), + delete: (type: string, apiKey: string, fileId: string) => + ipcRenderer.invoke('file-service:delete', type, apiKey, fileId), + retrieve: (type: string, apiKey: string, fileId: string) => + ipcRenderer.invoke('file-service:retrieve', type, apiKey, fileId) }, selectionMenu: { action: (action: string) => ipcRenderer.invoke('selection-menu:action', action) diff --git a/src/renderer/src/pages/files/GeminiFiles.tsx b/src/renderer/src/pages/files/GeminiFiles.tsx index ff60c8751..07dea8fbd 100644 --- a/src/renderer/src/pages/files/GeminiFiles.tsx +++ b/src/renderer/src/pages/files/GeminiFiles.tsx @@ -19,7 +19,7 @@ const GeminiFiles: FC = ({ id }) => { const [loading, setLoading] = useState(false) const fetchFiles = useCallback(async () => { - const { files } = await window.api.gemini.listFiles(provider.apiKey) + const { files } = await window.api.fileService.list(provider.type, provider.apiKey) files && setFiles(files.filter((file) => file.state === 'ACTIVE')) }, [provider]) @@ -57,7 +57,7 @@ const GeminiFiles: FC = ({ id }) => { style={{ cursor: 'pointer', color: 'var(--color-error)' }} onClick={() => { setFiles(files.filter((file) => file.name !== record.name)) - window.api.gemini.deleteFile(provider.apiKey, record.name).catch((error) => { + window.api.fileService.delete(provider.type, provider.apiKey, record.name).catch((error) => { console.error('Failed to delete file:', error) setFiles((prev) => [...prev, record]) }) diff --git a/src/renderer/src/pages/files/MistralFiles.tsx b/src/renderer/src/pages/files/MistralFiles.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index 634730117..036cd665a 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -107,7 +107,8 @@ const KnowledgeContent: FC = ({ selectedBase }) => { count: 1, origin_name: file.name, type: file.type as FileTypes, - created_at: new Date().toISOString() + created_at: new Date().toISOString(), + source: 'local' as const })) .filter(({ ext }) => fileTypes.includes(ext)) const uploadedFiles = await FileManager.uploadFiles(_files) diff --git a/src/renderer/src/providers/GeminiProvider.ts b/src/renderer/src/providers/GeminiProvider.ts index a21385fb7..06c7d8c03 100644 --- a/src/renderer/src/providers/GeminiProvider.ts +++ b/src/renderer/src/providers/GeminiProvider.ts @@ -32,6 +32,7 @@ import { } from '@renderer/services/MessagesService' import { Assistant, FileType, FileTypes, MCPToolResponse, Message, Model, Provider, Suggestion } from '@renderer/types' import { removeSpecialCharactersForTopicName } from '@renderer/utils' +import { fileToBase64 } from '@renderer/utils/file' import { callMCPTool, geminiFunctionCallToMcpTool, @@ -74,7 +75,7 @@ export default class GeminiProvider extends BaseProvider { const isSmallFile = file.size < smallFileSize if (isSmallFile) { - const { data, mimeType } = await window.api.gemini.base64File(file) + const { data, mimeType } = await fileToBase64(file.path) return { inlineData: { data, @@ -84,7 +85,7 @@ export default class GeminiProvider extends BaseProvider { } // Retrieve file from Gemini uploaded files - const fileMetadata = await window.api.gemini.retrieveFile(file, this.apiKey) + const fileMetadata = await window.api.fileService.retrieve(this.provider.type, this.apiKey, file.id) if (fileMetadata) { return { @@ -96,7 +97,7 @@ export default class GeminiProvider extends BaseProvider { } // If file is not found, upload it to Gemini - const uploadResult = await window.api.gemini.uploadFile(file, this.apiKey) + const uploadResult = await window.api.fileService.upload(this.provider.type, this.apiKey, file) return { fileData: { diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 0177ad965..70c9b8ed4 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -128,7 +128,7 @@ export type Provider = { isNotSupportArrayContent?: boolean } -export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai' +export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai' | 'mistral' export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning' | 'function_calling' @@ -171,17 +171,62 @@ export type MinAppType = { style?: React.CSSProperties } -export interface FileType { +interface BaseFileSource { id: string name: string - origin_name: string - path: string + type: FileTypes size: number ext: string - type: FileTypes + source: 'local' | 'remote' +} + +export interface RemoteFileSource extends BaseFileSource { + source: 'remote' + url: string + status: 'pending' | 'downloading' | 'downloaded' | 'error' + downloadProgress?: number + localPath?: string // 下载后的本地路径 +} + +export interface FileUploadResponse { + fileId: string + displayName: string + status: 'success' | 'processing' | 'failed' | 'unknown' + originalFile?: any // 保留原始响应,以备需要 +} + +export interface FileListResponse { + files: Array<{ + id: string + displayName: string + size?: number + status: 'success' | 'processing' | 'failed' | 'unknown' + originalFile?: any // 保留原始文件对象 + }> +} + +export interface LocalFileSource extends BaseFileSource { + origin_name: string + path: string created_at: string count: number tokens?: number + source: 'local' +} + +// 联合类型,表示一个文件可以是本地的或远程的 +export type FileSource = LocalFileSource | RemoteFileSource + +// 为了保持向后兼容 +export type FileType = LocalFileSource + +// 类型保护函数,用于区分文件类型 +export const isLocalFile = (file: FileSource): file is LocalFileSource => { + return file.source === 'local' +} + +export const isRemoteFile = (file: FileSource): file is RemoteFileSource => { + return file.source === 'remote' } export enum FileTypes { @@ -295,7 +340,6 @@ export type KnowledgeBaseParams = { rerankModelProvider?: string topN?: number preprocessing?: boolean - ocrProvider?: OcrProvider } export interface OcrProvider { @@ -303,6 +347,7 @@ export interface OcrProvider { name: string apiKey?: string apiHost?: string + model?: string } export type GenerateImageParams = { diff --git a/src/renderer/src/utils/file.ts b/src/renderer/src/utils/file.ts new file mode 100644 index 000000000..18b1a26e9 --- /dev/null +++ b/src/renderer/src/utils/file.ts @@ -0,0 +1,9 @@ +import fs from 'fs' + +export const fileToBase64 = async (filePath: string) => { + const buffer = await fs.promises.readFile(filePath) + return { + data: buffer.toString('base64'), + mimeType: 'application/pdf' + } +} diff --git a/yarn.lock b/yarn.lock index 5fa4c561e..8622d3a6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1985,6 +1985,17 @@ __metadata: languageName: node linkType: hard +"@mistralai/mistralai@npm:^1.5.2": + version: 1.5.2 + resolution: "@mistralai/mistralai@npm:1.5.2" + dependencies: + zod-to-json-schema: "npm:^3.24.1" + peerDependencies: + zod: ">= 3" + checksum: 10c0/d33a8a71adac4d2074ea4bfa09605b1c2158b5ffeaa9a78f3d9602c822cf0885df660b09f2372f17d8a81e78fa64795f31d9fad0cc40a1fab57ca0a4df9dc009 + languageName: node + linkType: hard + "@modelcontextprotocol/sdk@npm:1.6.1": version: 1.6.1 resolution: "@modelcontextprotocol/sdk@npm:1.6.1" @@ -3345,6 +3356,7 @@ __metadata: "@llm-tools/embedjs-loader-web": "npm:^0.1.28" "@llm-tools/embedjs-loader-xml": "npm:^0.1.28" "@llm-tools/embedjs-openai": "npm:^0.1.28" + "@mistralai/mistralai": "npm:^1.5.2" "@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch" "@notionhq/client": "npm:^2.2.15" "@reduxjs/toolkit": "npm:^2.2.5"