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.
This commit is contained in:
+2
-1
@@ -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",
|
||||
|
||||
+23
-9
@@ -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())
|
||||
|
||||
@@ -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 }>
|
||||
/**
|
||||
* 辅助方法:延迟执行
|
||||
*/
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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<PreuploadResponse> {
|
||||
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<ApiResponse<PreuploadResponse>>(endpoint, null, config)
|
||||
@@ -162,7 +162,7 @@ export default class Doc2xOcrProvider extends BaseOcrProvider {
|
||||
|
||||
private async getStatus(uid: string): Promise<StatusResponse> {
|
||||
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<ApiResponse<StatusResponse>>(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<ApiResponse<any>>(endpoint, payload, config)
|
||||
@@ -211,7 +211,7 @@ export default class Doc2xOcrProvider extends BaseOcrProvider {
|
||||
|
||||
private async getParsedFile(uid: string): Promise<ParsedFileResponse> {
|
||||
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<ApiResponse<ParsedFileResponse>>(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}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FileMetadataResponse | undefined> {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
|
||||
const cachedResponse = CacheService.get<any>(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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<FileUploadResponse>
|
||||
abstract deleteFile(fileId: string): Promise<void>
|
||||
abstract listFiles(): Promise<FileListResponse>
|
||||
abstract retrieveFile(fileId: string): Promise<FileUploadResponse>
|
||||
}
|
||||
@@ -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<string, BaseFileService> = 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
|
||||
}
|
||||
}
|
||||
@@ -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<FileUploadResponse> {
|
||||
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<FileUploadResponse> {
|
||||
const cachedResponse = CacheService.get<any>(`${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<FileListResponse> {
|
||||
const cachedList = CacheService.get<FileListResponse>(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<void> {
|
||||
await this.fileManager.deleteFile(fileId)
|
||||
}
|
||||
}
|
||||
@@ -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<FileUploadResponse> {
|
||||
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<FileListResponse> {
|
||||
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<void> {
|
||||
try {
|
||||
await this.client.files.delete({
|
||||
fileId
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async retrieveFile(fileId: string): Promise<FileUploadResponse> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+5
-6
@@ -117,12 +117,11 @@ declare global {
|
||||
setMinimumSize: (width: number, height: number) => Promise<void>
|
||||
resetMinimumSize: () => Promise<void>
|
||||
}
|
||||
gemini: {
|
||||
uploadFile: (file: FileType, apiKey: string) => Promise<UploadFileResponse>
|
||||
retrieveFile: (file: FileType, apiKey: string) => Promise<FileMetadataResponse | undefined>
|
||||
base64File: (file: FileType) => Promise<{ data: string; mimeType: string }>
|
||||
listFiles: (apiKey: string) => Promise<ListFilesResponse>
|
||||
deleteFile: (apiKey: string, fileId: string) => Promise<void>
|
||||
fileService: {
|
||||
upload: (type: string, apiKey: string, file: FileType) => Promise<UploadFileResponse>
|
||||
retrieve: (type: string, apiKey: string, fileId: string) => Promise<FileMetadataResponse | undefined>
|
||||
list: (type: string, apiKey: string) => Promise<ListFilesResponse>
|
||||
delete: (type: string, apiKey: string, fileId: string) => Promise<void>
|
||||
}
|
||||
selectionMenu: {
|
||||
action: (action: string) => Promise<void>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -19,7 +19,7 @@ const GeminiFiles: FC<GeminiFilesProps> = ({ 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<GeminiFilesProps> = ({ 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])
|
||||
})
|
||||
|
||||
@@ -107,7 +107,8 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ 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)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user