// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { BrowserWindow, app } from 'electron'; import fs from 'fs'; import http from 'http'; import https from 'https'; import mammoth from 'mammoth'; import Papa from 'papaparse'; import path from 'path'; import * as unzipper from 'unzipper'; import { URL } from 'url'; import { parseStringPromise } from 'xml2js'; interface FileInfo { path: string; name: string; type: string; isFolder: boolean; relativePath: string; task_id?: string; project_id?: string; source?: 'project_output' | 'camel_log'; } export class FileReader { private win: BrowserWindow | null = null; constructor(window: BrowserWindow) { this.win = window; } // Remove automatic IPC handler registration from constructor // IPC handlers should be registered once in the main process private async parseDocx(filePath: string): Promise { try { const result = await mammoth.convertToHtml({ path: filePath }); return result.value; // The generated HTML } catch (error) { console.error('DOCX parsing error:', error); throw error; } } private async parseDoc(filePath: string): Promise { try { const result = await mammoth.convertToHtml({ path: filePath }); return result.value; // The generated HTML } catch (error) { console.error('DOC parsing error:', error); throw error; } } private async parseXlsx(filePath: string): Promise { try { const directory = await unzipper.Open.file(filePath); // Find the shared strings file and worksheets const sharedStringsFile = directory.files.find( (f: any) => f.path === 'xl/sharedStrings.xml' ); const worksheetFiles = directory.files.filter((f: any) => f.path.match(/^xl\/worksheets\/sheet\d+\.xml$/) ); // Parse shared strings if exists let sharedStrings: string[] = []; if (sharedStringsFile) { const sharedStringsBuffer = await sharedStringsFile.buffer(); const sharedStringsContent = sharedStringsBuffer.toString('utf-8'); const parsedSharedStrings = await parseStringPromise(sharedStringsContent); if (parsedSharedStrings.sst && parsedSharedStrings.sst.si) { sharedStrings = parsedSharedStrings.sst.si.map((si: any) => { // Handle simple text nodes if (si.t && si.t[0]) { return typeof si.t[0] === 'string' ? si.t[0] : String(si.t[0]); } // Handle rich text nodes if (si.r) { return si.r .map((r: any) => { if (r.t && r.t[0]) { return typeof r.t[0] === 'string' ? r.t[0] : String(r.t[0]); } return ''; }) .join(''); } // Handle direct string values if (typeof si === 'string') { return si; } return ''; }); console.log(`Parsed ${sharedStrings.length} shared strings`); } } let html = `
`; // Process each worksheet for (let i = 0; i < worksheetFiles.length && i < 5; i++) { // Limit to first 5 sheets const file = worksheetFiles[i]; const contentBuffer = await file.buffer(); const content = contentBuffer.toString('utf-8'); const parsed = await parseStringPromise(content); if (worksheetFiles.length > 1) { html += `

Sheet ${i + 1}

`; } // Create table html += ''; // Get all rows const rows = parsed.worksheet?.sheetData?.[0]?.row || []; // Find the maximum column to create column headers let maxCol = 0; for (const row of rows) { const cells = row.c || []; for (const cell of cells) { if (cell.$ && cell.$.r) { const colMatch = cell.$.r.match(/^([A-Z]+)/); if (colMatch) { const colIndex = this.columnToNumber(colMatch[1]); maxCol = Math.max(maxCol, colIndex); } } } } // Add column headers row (A, B, C, ...) html += ''; html += ''; // Empty cell for row numbers for (let i = 0; i < maxCol; i++) { html += ``; } html += ''; // Add data rows html += ''; for (const row of rows) { html += ''; // Add row number const rowNum = row.$ && row.$.r ? row.$.r : ''; html += ``; // Create cells array with proper indexing const cells = row.c || []; const cellMap = new Map(); // Map cells by column index for (const cell of cells) { if (cell.$ && cell.$.r) { const colMatch = cell.$.r.match(/^([A-Z]+)/); if (colMatch) { const colIndex = this.columnToNumber(colMatch[1]); cellMap.set(colIndex, cell); } } } // Add cells in order, including empty cells for (let i = 1; i <= maxCol; i++) { const cell = cellMap.get(i); const cellValue = cell ? this.getCellValue(cell, sharedStrings) : ''; html += ``; } html += ''; } html += ''; html += '
${this.numberToColumn(i + 1)}
${rowNum}${cellValue}
'; } html += '
'; return html; } catch (error) { console.error('XLSX parsing error:', error); throw error; } } private columnToNumber(column: string): number { let result = 0; for (let i = 0; i < column.length; i++) { result = result * 26 + (column.charCodeAt(i) - 'A'.charCodeAt(0) + 1); } return result; } private numberToColumn(num: number): string { let column = ''; while (num > 0) { num--; column = String.fromCharCode((num % 26) + 'A'.charCodeAt(0)) + column; num = Math.floor(num / 26); } return column; } private getCellValue(cell: any, sharedStrings: string[]): string { try { // If cell has a value if (cell.v && cell.v[0] !== undefined) { const value = cell.v[0]; // Check cell type if (cell.$ && cell.$.t === 's') { // Shared string const index = parseInt(value); if (!isNaN(index) && index >= 0 && index < sharedStrings.length) { return sharedStrings[index] || ''; } console.warn( `Shared string index ${index} out of bounds (array length: ${sharedStrings.length})` ); return value; // Return raw value as fallback } else if (cell.$ && cell.$.t === 'inlineStr') { // Inline string return cell.is?.[0]?.t?.[0] || ''; } else if (cell.$ && cell.$.t === 'str') { // Formula result string return value; } else { // Number or other value // Format numbers to avoid long decimals const numValue = parseFloat(value); if (!isNaN(numValue) && numValue % 1 !== 0) { return numValue.toFixed(2); } return value; } } // Check for inline string without type attribute if (cell.is && cell.is[0] && cell.is[0].t && cell.is[0].t[0]) { return cell.is[0].t[0]; } // Check for formula cells if (cell.f && cell.f[0] && cell.v && cell.v[0]) { return cell.v[0]; } return ''; } catch (error) { console.error( 'Error getting cell value:', error, 'Cell:', JSON.stringify(cell) ); return ''; } } private async parsePptx(filePath: string): Promise { try { const directory = await unzipper.Open.file(filePath); const slideFiles = directory.files.filter((f: any) => f.path.match(/^ppt\/slides\/slide\d+\.xml$/) ); let html = '
'; for (let i = 0; i < slideFiles.length; i++) { const file = slideFiles[i]; const contentBuffer = await file.buffer(); const content = contentBuffer.toString('utf-8'); const parsed = await parseStringPromise(content); html += `

Slide ${i + 1}

    `; const texts = parsed['p:sld']['p:cSld'][0]['p:spTree'][0]['p:sp'] || []; for (const textNode of texts) { const paras = textNode?.['p:txBody']?.[0]?.['a:p'] || []; for (const para of paras) { const runs = para?.['a:r'] || []; for (const run of runs) { const text = run?.['a:t']?.[0]; if (text) { html += `
  • ${text}
  • `; } } } } html += '

'; } html += '
'; return html; } catch (error) { console.error('PPTX unzip parse error:', error); throw error; } } private async parseCsv(filePath: string): Promise { try { const fileContent = fs.readFileSync(filePath, 'utf-8'); const result = Papa.parse(fileContent, { header: true, skipEmptyLines: true, delimiter: ',', }); // Convert to HTML table if (result.data && result.data.length > 0) { const headers = Object.keys(result.data[0] as string[]); let html = ''; // Header row html += ''; headers.forEach((header) => { html += ``; }); html += ''; // Data rows html += ''; result.data.forEach((row: any) => { html += ''; headers.forEach((header) => { html += ``; }); html += ''; }); html += '
${header}
${row[header] || ''}
'; return html; } return '

Empty CSV file

'; } catch (error) { console.error('CSV parsing error:', error); throw error; } } // add download file method private async downloadFile(url: string, localPath: string): Promise { return new Promise((resolve, reject) => { const urlObj = new URL(url); const protocol = urlObj.protocol === 'https:' ? https : http; const request = protocol.get(url, (response) => { if (response.statusCode !== 200) { reject( new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`) ); return; } const fileStream = fs.createWriteStream(localPath); response.pipe(fileStream); fileStream.on('finish', () => { fileStream.close(); resolve(); }); fileStream.on('error', (err) => { fs.unlink(localPath, (unlinkErr) => { if (unlinkErr) console.error('Failed to delete incomplete file:', unlinkErr); }); // delete incomplete file reject(err); }); }); request.on('error', (err) => { reject(err); }); request.setTimeout(30000, () => { request.destroy(); reject(new Error('Download timeout')); }); }); } // check if it is a local file path private isLocalFile(filePath: string): boolean { return ( filePath.startsWith('localfile://') || filePath.startsWith('file://') || (!filePath.startsWith('http://') && !filePath.startsWith('https://') && !filePath.includes('://')) ); } // get temporary file path private getTempFilePath(originalPath: string, type: string): string { const userData = app.getPath('userData'); const tempDir = path.join(userData, 'temp'); // ensure temporary directory exists if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } const fileName = path.basename(originalPath) || `temp_${Date.now()}.${type}`; return path.join(tempDir, fileName); } public openFile(type: string, filePath: string, _isShowSourceCode: boolean) { return new Promise(async (resolve, reject) => { try { // check if it is a remote file if (!this.isLocalFile(filePath)) { console.log('detect remote file, start downloading:', filePath); // download file to temporary directory const tempPath = this.getTempFilePath(filePath, type); try { await this.downloadFile(filePath, tempPath); console.log('file download completed:', tempPath); // use temporary file path to continue processing filePath = tempPath; } catch (downloadError) { console.error('file download failed:', downloadError); reject(downloadError); return; } } // original file processing logic if (type === 'md') { const content = fs.readFileSync(filePath, 'utf-8'); resolve(content); } else if (type === 'html') { const content = fs.readFileSync(filePath, 'utf-8'); resolve(content); } else if (['pdf'].includes(type)) { resolve(filePath); } else if (type === 'csv') { try { const htmlContent = await this.parseCsv(filePath); resolve(htmlContent); } catch (error) { console.warn('CSV parsing failed, reading as text:', error); const content = fs.readFileSync(filePath, 'utf-8'); resolve(content); } } else if (type === 'docx') { try { const htmlContent = await this.parseDocx(filePath); resolve(htmlContent); } catch (error) { console.warn('DOCX parsing failed, reading as text:', error); const content = fs.readFileSync(filePath, 'utf-8'); resolve(content); } } else if (type === 'doc') { try { const htmlContent = await this.parseDoc(filePath); resolve(htmlContent); } catch (error) { console.warn('DOC parsing failed, reading as text:', error); const content = fs.readFileSync(filePath, 'utf-8'); resolve(content); } } else if (type === 'pptx') { try { const htmlContent = await this.parsePptx(filePath); resolve(htmlContent); } catch (error) { console.warn( 'PPTX parsing failed, reading as binary string:', error ); const content = fs.readFileSync(filePath, 'base64'); // backup processing resolve(`
${content}
`); } } else if (type === 'xlsx') { try { const htmlContent = await this.parseXlsx(filePath); resolve(htmlContent); } catch (error) { console.warn('XLSX parsing failed, reading as text:', error); const content = fs.readFileSync(filePath, 'utf-8'); resolve(content); } } else { const content = fs.readFileSync(filePath, 'utf-8'); resolve(content); } } catch (error) { reject(error); } }); } // Folders to hide in the Agent Folder view private readonly hiddenFolders = [ 'browser_agent', 'developer_agent', 'document_agent', 'multi_modal_agent', 'terminal_logs', ]; private getFilesRecursive( dirPath: string, basePath: string, baseReal?: string ): FileInfo[] { try { const resolvedBase = baseReal ?? fs.realpathSync(basePath); const files = fs.readdirSync(dirPath); const result: FileInfo[] = []; for (const file of files) { if (file.startsWith('.')) continue; // Skip hidden folders if (this.hiddenFolders.includes(file)) continue; const filePath = path.join(dirPath, file); try { const stats = fs.lstatSync(filePath); if (stats.isSymbolicLink()) continue; const realPath = fs.realpathSync(filePath); const relativeToBase = path.relative(resolvedBase, realPath); if ( relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase) ) { continue; } const isFolder = stats.isDirectory(); const relativePath = path.relative(basePath, dirPath); const fileInfo: FileInfo = { path: filePath, name: file, type: isFolder ? 'folder' : file.split('.').pop()?.toLowerCase() || '', isFolder: isFolder, relativePath: relativePath === '' ? '' : relativePath, }; result.push(fileInfo); if (isFolder) { const subFiles = this.getFilesRecursive( filePath, basePath, resolvedBase ); result.push(...subFiles); } } catch (fileErr) { console.warn('Skipping inaccessible file:', filePath, fileErr); continue; } } return result; } catch (err) { console.error('Error reading directory:', dirPath, err); return []; } } private findTaskInProjects(userDir: string, taskId: string): string | null { try { if (!fs.existsSync(userDir)) { return null; } const entries = fs.readdirSync(userDir); // Look for project directories for (const entry of entries) { if (entry.startsWith('project_')) { const projectDir = path.join(userDir, entry); const taskDir = path.join(projectDir, `task_${taskId}`); if (fs.existsSync(taskDir)) { return taskDir; } } } return null; } catch (err) { console.error('Error finding task in projects:', err); return null; } } private resolveTaskPaths( email: string, taskId: string, projectId?: string ): { dirPath: string; logPath: string; } { const safeEmail = email .split('@')[0] .replace(/[\\/*?:"<>|\s]/g, '_') .replace(/^\.+|\.+$/g, ''); const userHome = app.getPath('home'); let dirPath: string; let logPath: string; if (projectId) { dirPath = path.join( userHome, 'eigent', safeEmail, `project_${projectId}`, `task_${taskId}` ); logPath = path.join( userHome, '.eigent', safeEmail, `project_${projectId}`, `task_${taskId}` ); return { dirPath, logPath }; } const userDir = path.join(userHome, 'eigent', safeEmail); const projectBasedPath = this.findTaskInProjects(userDir, taskId); if (projectBasedPath) { dirPath = projectBasedPath; const projectMatch = projectBasedPath.match(/project_([^\\\/]+)/); if (projectMatch) { logPath = path.join( userHome, '.eigent', safeEmail, projectMatch[0], `task_${taskId}` ); } else { logPath = path.join(userHome, '.eigent', safeEmail, `task_${taskId}`); } return { dirPath, logPath }; } dirPath = path.join(userHome, 'eigent', safeEmail, `task_${taskId}`); logPath = path.join(userHome, '.eigent', safeEmail, `task_${taskId}`); return { dirPath, logPath }; } public getFileList( email: string, taskId: string, projectId?: string ): FileInfo[] { const { dirPath, logPath } = this.resolveTaskPaths( email, taskId, projectId ); const camelLogPath = path.join(logPath, 'camel_logs'); try { const projectFiles = fs.existsSync(dirPath) ? this.getFilesRecursive(dirPath, dirPath).map((file) => ({ ...file, source: 'project_output' as const, })) : []; const camelLogFiles = fs.existsSync(camelLogPath) ? this.getFilesRecursive(camelLogPath, camelLogPath).map((file) => ({ ...file, source: 'camel_log' as const, })) : []; if (projectFiles.length === 0 && camelLogFiles.length === 0) { return []; } return [...projectFiles, ...camelLogFiles]; } catch (err) { console.error('Load file failed:', err); return []; } } public deleteTaskFiles( email: string, taskId: string, projectId?: string ): { success: boolean; path: { dirPath: string; logPath: string }; } { const { dirPath, logPath } = this.resolveTaskPaths( email, taskId, projectId ); try { let success = false; if (fs.existsSync(dirPath)) { fs.rmSync(dirPath, { recursive: true, force: true }); success = true; } if (fs.existsSync(logPath)) { fs.rmSync(logPath, { recursive: true, force: true }); success = true; } return { success, path: { dirPath, logPath } }; } catch (err) { console.error('Delete task files failed:', dirPath, err); return { success: false, path: { dirPath, logPath } }; } } public getLogFolder(email: string): string { const safeEmail = email .split('@')[0] .replace(/[\\/*?:"<>|\s]/g, '_') .replace(/^\.+|\.+$/g, ''); const userHome = app.getPath('home'); const dirPath = path.join(userHome, 'eigent', safeEmail); try { if (!fs.existsSync(dirPath)) { return ''; } return dirPath; } catch (err) { console.error('Load file failed:', err); return ''; } } public createProjectStructure( email: string, projectId: string ): { success: boolean; path: string } { const safeEmail = email .split('@')[0] .replace(/[\\/*?:"<>|\s]/g, '_') .replace(/^\.+|\.+$/g, ''); const userHome = app.getPath('home'); const projectPath = path.join( userHome, 'eigent', safeEmail, `project_${projectId}` ); try { if (!fs.existsSync(projectPath)) { fs.mkdirSync(projectPath, { recursive: true }); } return { success: true, path: projectPath }; } catch (err) { console.error('Create project structure failed:', err); return { success: false, path: projectPath }; } } public getProjectList(email: string): Array<{ id: string; name: string; path: string; taskCount: number; createdAt: Date; }> { const safeEmail = email .split('@')[0] .replace(/[\\/*?:"<>|\s]/g, '_') .replace(/^\.+|\.+$/g, ''); const userHome = app.getPath('home'); const userDir = path.join(userHome, 'eigent', safeEmail); try { if (!fs.existsSync(userDir)) { return []; } const entries = fs.readdirSync(userDir); const projects: Array<{ id: string; name: string; path: string; taskCount: number; createdAt: Date; }> = []; for (const entry of entries) { if (entry.startsWith('project_')) { const projectPath = path.join(userDir, entry); const stats = fs.statSync(projectPath); if (stats.isDirectory()) { const projectId = entry.replace('project_', ''); // Count tasks in this project const taskCount = this.countTasksInProject(projectPath); projects.push({ id: projectId, name: `Project ${projectId}`, path: projectPath, taskCount, createdAt: stats.birthtime, }); } } } return projects.sort( (a, b) => b.createdAt.getTime() - a.createdAt.getTime() ); } catch (err) { console.error('Get project list failed:', err); return []; } } public getTasksInProject( email: string, projectId: string ): Array<{ id: string; name: string; path: string; createdAt: Date }> { const safeEmail = email .split('@')[0] .replace(/[\\/*?:"<>|\s]/g, '_') .replace(/^\.+|\.+$/g, ''); const userHome = app.getPath('home'); const projectPath = path.join( userHome, 'eigent', safeEmail, `project_${projectId}` ); try { if (!fs.existsSync(projectPath)) { return []; } const entries = fs.readdirSync(projectPath); const tasks: Array<{ id: string; name: string; path: string; createdAt: Date; }> = []; for (const entry of entries) { if (entry.startsWith('task_')) { const taskPath = path.join(projectPath, entry); const stats = fs.statSync(taskPath); if (stats.isDirectory()) { const taskId = entry.replace('task_', ''); tasks.push({ id: taskId, name: `Task ${taskId}`, path: taskPath, createdAt: stats.birthtime, }); } } } return tasks.sort( (a, b) => b.createdAt.getTime() - a.createdAt.getTime() ); } catch (err) { console.error('Get tasks in project failed:', err); return []; } } public moveTaskToProject( email: string, taskId: string, projectId: string ): { success: boolean; message: string } { const safeEmail = email .split('@')[0] .replace(/[\\/*?:"<>|\s]/g, '_') .replace(/^\.+|\.+$/g, ''); const userHome = app.getPath('home'); // Source path (legacy structure) const sourcePath = path.join( userHome, 'eigent', safeEmail, `task_${taskId}` ); const sourceLogPath = path.join( userHome, '.eigent', safeEmail, `task_${taskId}` ); // Destination paths (project structure) const projectPath = path.join( userHome, 'eigent', safeEmail, `project_${projectId}` ); const destPath = path.join(projectPath, `task_${taskId}`); const destLogPath = path.join( userHome, '.eigent', safeEmail, `project_${projectId}`, `task_${taskId}` ); try { // Create project structure if it doesn't exist if (!fs.existsSync(projectPath)) { fs.mkdirSync(projectPath, { recursive: true }); } // Create destination log directory const destLogDir = path.dirname(destLogPath); if (!fs.existsSync(destLogDir)) { fs.mkdirSync(destLogDir, { recursive: true }); } // Move task files if (fs.existsSync(sourcePath)) { fs.renameSync(sourcePath, destPath); } // Move log files if (fs.existsSync(sourceLogPath)) { fs.renameSync(sourceLogPath, destLogPath); } return { success: true, message: `Task ${taskId} moved to project ${projectId}`, }; } catch (err) { console.error('Move task to project failed:', err); return { success: false, message: `Failed to move task: ${err}` }; } } public getProjectFileList(email: string, projectId: string): FileInfo[] { const safeEmail = email .split('@')[0] .replace(/[\\/*?:"<>|\s]/g, '_') .replace(/^\.+|\.+$/g, ''); const userHome = app.getPath('home'); const projectPath = path.join( userHome, 'eigent', safeEmail, `project_${projectId}` ); try { if (!fs.existsSync(projectPath)) { return []; } const allFiles: FileInfo[] = []; const taskDirs = fs.readdirSync(projectPath); for (const taskDir of taskDirs) { if (!taskDir.startsWith('task_')) continue; const taskPath = path.join(projectPath, taskDir); const stats = fs.statSync(taskPath); if (stats.isDirectory()) { const taskId = taskDir.replace('task_', ''); const taskFiles = this.getFilesRecursive(taskPath, taskPath); const enrichedFiles = taskFiles.map((file) => { const fileDir = path.dirname(file.path); const relativeParentPath = path.relative(projectPath, fileDir); return { ...file, task_id: taskId, project_id: projectId, relativePath: relativeParentPath === '.' ? '' : relativeParentPath, }; }); allFiles.push(...enrichedFiles); } } return allFiles.sort((a, b) => { // Sort by task_id first, then by file path if (a.task_id !== b.task_id) { return a.task_id!.localeCompare(b.task_id!); } return a.path.localeCompare(b.path); }); } catch (err) { console.error('Get project file list failed:', err); return []; } } private countTasksInProject(projectPath: string): number { try { const entries = fs.readdirSync(projectPath); return entries.filter((entry) => entry.startsWith('task_')).length; } catch (err) { console.error('Count tasks in project failed:', err); return 0; } } }