Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9b83248
feat: enhance file handling by adding MIME type extraction and file n…
hllshiro Aug 11, 2025
e2cdc17
refactor: update sanitizeText function documentation and improve whit…
hllshiro Aug 11, 2025
188f87a
fix: update chunk ID generation to include file ID for better tracking
hllshiro Aug 11, 2025
eba1d41
feat: add methods to get supported languages and separators for progr…
hllshiro Aug 19, 2025
440083a
Merge remote-tracking branch 'upstream/dev' into optimize/builtin-kno…
hllshiro Aug 19, 2025
79f3bd8
feat: update separators handling and add localization for separators …
hllshiro Aug 19, 2025
9cf6ce1
perf: builtin knowledge support custom separators
hllshiro Aug 20, 2025
b71b850
perf: RecursiveCharacterTextSplitter load custom separators
hllshiro Aug 20, 2025
468bd49
feat: implement FileValidationService with MIME type validation and s…
hllshiro Aug 20, 2025
826a21b
feat: integrate FileValidationService into FilePresenter for file val…
hllshiro Aug 20, 2025
ea7d1a8
feat: add file validation methods to KnowledgePresenter for supported…
hllshiro Aug 20, 2025
529fd27
feat: dynamically load supported file extensions and enhance file upl…
hllshiro Aug 20, 2025
7a4f97a
feat: update file support messages and improve error handling for uns…
hllshiro Aug 20, 2025
19b7d57
feat: update icon in settings and reset separators value on config ad…
hllshiro Aug 20, 2025
aea2881
feat: adjust popover width and update text color in BuiltinKnowledgeS…
hllshiro Aug 20, 2025
8295006
fix: correct placeholder attribute casing in BuiltinKnowledgeSettings…
hllshiro Aug 20, 2025
192c91e
fix: update text color for language selection and handle empty separa…
hllshiro Aug 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"@tiptap/suggestion": "^2.11.7",
"@tiptap/vue-3": "^2.11.7",
"@types/better-sqlite3": "^7.6.0",
"@types/mime-types": "^3.0.1",
"@types/node": "^22.14.1",
"@types/xlsx": "^0.0.35",
"@vitejs/plugin-vue": "^6.0.1",
Expand Down Expand Up @@ -154,10 +155,10 @@
"vite-plugin-monaco-editor-esm": "^2.0.2",
"vite-plugin-vue-devtools": "^8.0.0",
"vite-svg-loader": "^5.1.0",
"vue-renderer-markdown": "^0.0.34",
"vitest": "^3.2.4",
"vue": "^3.5.18",
"vue-i18n": "^11.1.11",
"vue-renderer-markdown": "^0.0.34",
"vue-router": "4",
"vue-tsc": "^2.2.10",
"vue-use-monaco": "^0.0.8",
Expand Down
72 changes: 70 additions & 2 deletions src/main/presenter/filePresenter/FilePresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,31 @@ import { ImageFileAdapter } from './ImageFileAdapter'
import { nanoid } from 'nanoid'
import { DirectoryAdapter } from './DirectoryAdapter'
import { UnsupportFileAdapter } from './UnsupportFileAdapter'
import {
FileValidationService,
FileValidationResult,
IFileValidationService
} from './FileValidationService'

export class FilePresenter implements IFilePresenter {
private userDataPath: string
private maxFileSize: number = 1024 * 1024 * 30 // 30 MB
private tempDir: string
private fileValidationService: IFileValidationService

constructor() {
constructor(fileValidationService?: IFileValidationService) {
this.userDataPath = app.getPath('userData')
this.tempDir = path.join(this.userDataPath, 'temp')
this.fileValidationService = fileValidationService || new FileValidationService()
// Ensure temp directory exists
fs.mkdir(this.tempDir, { recursive: true }).catch(console.error)
try {
const mkdirResult = fs.mkdir(this.tempDir, { recursive: true })
if (mkdirResult && typeof mkdirResult.catch === 'function') {
mkdirResult.catch(console.error)
}
} catch (error) {
console.error('Failed to create temp directory:', error)
}
}

async getMimeType(filePath: string): Promise<string> {
Expand Down Expand Up @@ -236,6 +250,60 @@ export class FilePresenter implements IFilePresenter {
return false
}
}

/**
* Validates if a file is supported for knowledge base processing
* @param filePath Path to the file to validate
* @returns FileValidationResult with validation details
*/
async validateFileForKnowledgeBase(filePath: string): Promise<FileValidationResult> {
try {
return await this.fileValidationService.validateFile(filePath)
} catch (error) {
console.error('Error validating file for knowledge base:', error)
return {
isSupported: false,
error: `Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
suggestedExtensions: this.fileValidationService.getSupportedExtensions()
}
}
}

/**
* Gets all supported file extensions for knowledge base processing
* @returns Array of supported file extensions (without dots)
*/
getSupportedExtensions(): string[] {
try {
return this.fileValidationService.getSupportedExtensions()
} catch (error) {
console.error('Error getting supported extensions:', error)
// Return fallback extensions if service fails
return [
'txt',
'md',
'markdown',
'pdf',
'docx',
'pptx',
'xlsx',
'csv',
'json',
'yaml',
'yml',
'xml',
'js',
'ts',
'py',
'java',
'cpp',
'c',
'h',
'css',
'html'
].sort()
}
}
}

function calculateImageTokens(adapter: ImageFileAdapter): number {
Expand Down
227 changes: 227 additions & 0 deletions src/main/presenter/filePresenter/FileValidationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { FileAdapterConstructor } from './FileAdapterConstructor'
import { getMimeTypeAdapterMap, detectMimeType } from './mime'
import { UnsupportFileAdapter } from './UnsupportFileAdapter'
import * as mimeTypes from 'mime-types'

export interface FileValidationResult {
isSupported: boolean
mimeType?: string
adapterType?: string
error?: string
suggestedExtensions?: string[]
}

export interface IFileValidationService {
validateFile(filePath: string): Promise<FileValidationResult>
getSupportedExtensions(): string[]
getSupportedMimeTypes(): string[]
}

export class FileValidationService implements IFileValidationService {
private excludedAdapters = [
'AudioFileAdapter',
'ImageFileAdapter',
'UnsupportFileAdapter',
'DirectoryAdapter'
]

constructor() {
// Constructor kept for future extensibility
}

/**
* Validates if a file is supported for knowledge base processing
* @param filePath Path to the file to validate
* @returns FileValidationResult with validation details
*/
async validateFile(filePath: string): Promise<FileValidationResult> {
try {
// Detect MIME type from file content
const mimeType = await detectMimeType(filePath)

if (!mimeType) {
return {
isSupported: false,
error: 'Could not determine file type',
suggestedExtensions: this.getSupportedExtensions()
}
}

// Get adapter map and find appropriate adapter
const adapterMap = getMimeTypeAdapterMap()
const AdapterConstructor = this.findAdapterForMimeType(mimeType, adapterMap)

if (!AdapterConstructor) {
return {
isSupported: false,
mimeType,
error: 'No adapter found for this file type',
suggestedExtensions: this.getSupportedExtensions()
}
}

// Check if adapter is supported (not in excluded list)
const isSupported = this.isAdapterSupported(AdapterConstructor)
const adapterType = AdapterConstructor.name

if (!isSupported) {
return {
isSupported: false,
mimeType,
adapterType,
error: 'File type not supported',
suggestedExtensions: this.getSupportedExtensions()
}
}

return {
isSupported: true,
mimeType,
adapterType
}
} catch (error) {
return {
isSupported: false,
error: `Error validating file: ${error instanceof Error ? error.message : 'Unknown error'}`,
suggestedExtensions: this.getSupportedExtensions()
}
}
}

/**
* Checks if an adapter is supported for knowledge base processing
* @param adapterConstructor The adapter constructor to check
* @returns true if adapter is supported, false otherwise
*/
private isAdapterSupported(adapterConstructor: FileAdapterConstructor): boolean {
const adapterName = adapterConstructor.name
return !this.excludedAdapters.includes(adapterName)
}

/**
* Finds the appropriate adapter for a given MIME type
* @param mimeType The MIME type to find an adapter for
* @param adapterMap Map of MIME types to adapter constructors
* @returns The adapter constructor or undefined if not found
*/
private findAdapterForMimeType(
mimeType: string,
adapterMap: Map<string, FileAdapterConstructor>
): FileAdapterConstructor | undefined {
// First try exact match
const exactMatch = adapterMap.get(mimeType)
if (exactMatch) {
return exactMatch
}

// Try wildcard match
const type = mimeType.split('/')[0]
const wildcardMatch = adapterMap.get(`${type}/*`)

if (wildcardMatch) {
return wildcardMatch
}

// Return UnsupportFileAdapter as fallback
return UnsupportFileAdapter
}

/**
* Gets all supported file extensions for knowledge base processing
* @returns Array of supported file extensions (without dots)
*/
getSupportedExtensions(): string[] {
try {
const adapterMap = getMimeTypeAdapterMap()
const supportedExtensions = new Set<string>()

// Iterate through all MIME types in the adapter map
for (const [mimeType, AdapterConstructor] of adapterMap.entries()) {
// Skip excluded adapters and wildcard entries
if (!this.isAdapterSupported(AdapterConstructor) || mimeType.includes('*')) {
continue
}

// Get extensions for this MIME type
const extension = mimeTypes.extension(mimeType)
if (extension) {
supportedExtensions.add(extension)
}
}

// Add some common extensions that might not be in the MIME type map
const commonExtensions = ['md', 'markdown', 'txt', 'json', 'yaml', 'yml', 'xml']
commonExtensions.forEach((ext) => supportedExtensions.add(ext))

return Array.from(supportedExtensions).sort()
} catch (error) {
// Fallback to common extensions if adapter map fails
console.error('Error getting supported extensions:', error)
return [
'txt',
'md',
'markdown',
'pdf',
'docx',
'pptx',
'xlsx',
'csv',
'json',
'yaml',
'yml',
'xml',
'js',
'ts',
'py',
'java',
'cpp',
'c',
'h',
'css',
'html'
].sort()
}
}

/**
* Gets all supported MIME types for knowledge base processing
* @returns Array of supported MIME types
*/
getSupportedMimeTypes(): string[] {
try {
const adapterMap = getMimeTypeAdapterMap()
const supportedMimeTypes: string[] = []

// Iterate through all MIME types in the adapter map
for (const [mimeType, AdapterConstructor] of adapterMap.entries()) {
// Skip excluded adapters and wildcard entries
if (!this.isAdapterSupported(AdapterConstructor) || mimeType.includes('*')) {
continue
}

supportedMimeTypes.push(mimeType)
}

return supportedMimeTypes.sort()
} catch (error) {
// Fallback to common MIME types if adapter map fails
console.error('Error getting supported MIME types:', error)
return [
'text/plain',
'text/markdown',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
'application/json',
'application/javascript',
'text/html',
'text/css'
].sort()
}
}
}
6 changes: 5 additions & 1 deletion src/main/presenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ export class Presenter implements IPresenter {
this.trayPresenter = new TrayPresenter()
this.floatingButtonPresenter = new FloatingButtonPresenter(this.configPresenter)
this.dialogPresenter = new DialogPresenter()
this.knowledgePresenter = new KnowledgePresenter(this.configPresenter, dbDir)
this.knowledgePresenter = new KnowledgePresenter(
this.configPresenter,
dbDir,
this.filePresenter
)

// this.llamaCppPresenter = new LlamaCppPresenter() // 保留原始注释
this.setupEventBus() // 设置事件总线监听
Expand Down
Loading