diff --git a/dist-electron/main.js b/dist-electron/main.js index 0032d00..3bdb684 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -262,6 +262,124 @@ ipcMain.handle("clipboard-write-text", async (event, text) => { return { success: false, error: error.message }; } }); +ipcMain.handle("remote-fetch-drives", async (_event, serverHost, port, password) => { + try { + let url = `http://${serverHost}:${port}/api/files/drives`; + if (password) { + url += `?password=${encodeURIComponent(password)}`; + } + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch drives: ${response.statusText}`); + } + const data = await response.json(); + const items = data.items || []; + return { + success: true, + data: items.map((item) => ({ + name: item.name, + path: item.name, + type: item.isDirectory ? "dir" : "file", + size: item.size, + modified: "" + })) + }; + } catch (error) { + log2.error("Remote fetch drives failed:", error); + return { success: false, error: error.message }; + } +}); +ipcMain.handle("remote-fetch-files", async (_event, serverHost, port, filePath, password) => { + try { + let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(filePath)}&allowSystem=true`; + if (password) { + url += `&password=${encodeURIComponent(password)}`; + } + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch files: ${response.statusText}`); + } + const data = await response.json(); + const items = data.items || []; + return { + success: true, + data: items.map((item) => ({ + name: item.name, + path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name, + type: item.isDirectory ? "dir" : "file", + size: item.size, + modified: item.modified?.toString() + })) + }; + } catch (error) { + log2.error("Remote fetch files failed:", error); + return { success: false, error: error.message }; + } +}); +ipcMain.handle("remote-upload-file", async (_event, serverHost, port, filePath, remotePath, password) => { + try { + const win = electronState.getMainWindow(); + if (!win) { + throw new Error("No window found"); + } + const fullPath = path2.resolve(filePath); + if (!fs2.existsSync(fullPath)) { + throw new Error("File not found"); + } + const fileBuffer = fs2.readFileSync(fullPath); + const fileName = path2.basename(fullPath); + let url = `http://${serverHost}:${port}/api/files/upload`; + if (password) { + url += `?password=${encodeURIComponent(password)}`; + } + const formData = new FormData(); + const blob = new Blob([fileBuffer]); + formData.append("file", blob, fileName); + if (remotePath) { + formData.append("path", remotePath); + } + const response = await fetch(url, { + method: "POST", + body: formData + }); + if (!response.ok) { + throw new Error(`Upload failed: ${response.statusText}`); + } + return { success: true }; + } catch (error) { + log2.error("Remote upload failed:", error); + return { success: false, error: error.message }; + } +}); +ipcMain.handle("remote-download-file", async (_event, serverHost, port, fileName, remotePath, password) => { + try { + const win = electronState.getMainWindow(); + if (!win) { + throw new Error("No window found"); + } + let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fileName)}`; + if (password) { + url += `?password=${encodeURIComponent(password)}`; + } + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Download failed: ${response.statusText}`); + } + const buffer = await response.arrayBuffer(); + const { filePath } = await dialog2.showSaveDialog(win, { + title: "\u4FDD\u5B58\u6587\u4EF6", + defaultPath: fileName + }); + if (!filePath) { + return { success: false, canceled: true }; + } + fs2.writeFileSync(filePath, Buffer.from(buffer)); + return { success: true, filePath }; + } catch (error) { + log2.error("Remote download failed:", error); + return { success: false, error: error.message }; + } +}); async function startServer() { if (electronState.isDevelopment()) { log2.info("In dev mode, assuming external servers are running."); diff --git a/dist-electron/main.js.map b/dist-electron/main.js.map index fefaef4..f0fde83 100644 --- a/dist-electron/main.js.map +++ b/dist-electron/main.js.map @@ -1 +1 @@ -{"version":3,"sources":["../electron/main.ts","../electron/services/pdfGenerator.ts","../electron/services/htmlImport.ts","../electron/state.ts"],"sourcesContent":["import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron';\r\nimport path from 'path';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport fs from 'fs';\r\nimport log from 'electron-log';\r\nimport { generatePdf } from './services/pdfGenerator';\r\nimport { selectHtmlFile } from './services/htmlImport';\r\nimport { electronState } from './state';\r\n\r\nlog.initialize();\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nprocess.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCDesktop');\r\n\r\nif (!fs.existsSync(process.env.NOTEBOOK_ROOT)) {\r\n try {\r\n fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true });\r\n } catch (err) {\r\n log.error('Failed to create notebook directory:', err);\r\n }\r\n}\r\n\r\nelectronState.setDevelopment(!app.isPackaged);\r\n\r\nlet lastClipboardText = '';\r\n\r\nfunction startClipboardWatcher() {\r\n lastClipboardText = clipboard.readText();\r\n \r\n setInterval(() => {\r\n try {\r\n const currentText = clipboard.readText();\r\n if (currentText && currentText !== lastClipboardText) {\r\n lastClipboardText = currentText;\r\n log.info('Clipboard changed, syncing to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-auto-sync', currentText);\r\n }\r\n }\r\n } catch (e) {\r\n // ignore\r\n }\r\n }, 1000);\r\n}\r\n\r\nasync function createWindow() {\r\n const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000';\r\n \r\n const mainWindow = new BrowserWindow({\r\n width: 1280,\r\n height: 800,\r\n minWidth: 1600,\r\n minHeight: 900,\r\n autoHideMenuBar: true,\r\n titleBarStyle: 'hidden',\r\n titleBarOverlay: {\r\n color: '#00000000',\r\n symbolColor: initialSymbolColor,\r\n height: 32,\r\n },\r\n webPreferences: {\r\n nodeIntegration: false,\r\n contextIsolation: true,\r\n sandbox: false,\r\n webviewTag: true,\r\n preload: path.join(__dirname, 'preload.cjs'),\r\n },\r\n });\r\n\r\n electronState.setMainWindow(mainWindow);\r\n mainWindow.setMenu(null);\r\n\r\n mainWindow.webContents.setWindowOpenHandler(({ url }) => {\r\n if (url.startsWith('http:') || url.startsWith('https:')) {\r\n shell.openExternal(url);\r\n return { action: 'deny' };\r\n }\r\n return { action: 'allow' };\r\n });\r\n\r\n if (electronState.isDevelopment()) {\r\n log.info('Loading development URL...');\r\n try {\r\n await mainWindow.loadURL('http://localhost:5173');\r\n } catch (e) {\r\n log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e);\r\n }\r\n mainWindow.webContents.openDevTools();\r\n } else {\r\n log.info(`Loading production URL with port ${electronState.getServerPort()}...`);\r\n await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`);\r\n }\r\n}\r\n\r\nipcMain.handle('export-pdf', async (event, title, htmlContent) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (!win) return { success: false, error: 'No window found' };\r\n\r\n try {\r\n const { filePath } = await dialog.showSaveDialog(win, {\r\n title: '导出 PDF',\r\n defaultPath: `${title}.pdf`,\r\n filters: [{ name: 'PDF Files', extensions: ['pdf'] }]\r\n });\r\n\r\n if (!filePath) return { success: false, canceled: true };\r\n\r\n if (!htmlContent) {\r\n throw new Error('No HTML content provided for PDF export');\r\n }\r\n\r\n const pdfData = await generatePdf(htmlContent);\r\n fs.writeFileSync(filePath, pdfData);\r\n\r\n return { success: true, filePath };\r\n } catch (error: any) {\r\n log.error('Export PDF failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('select-html-file', async (event) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n return selectHtmlFile(win);\r\n});\r\n\r\nipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (win) {\r\n win.setTitleBarOverlay({ symbolColor });\r\n return { success: true };\r\n }\r\n return { success: false };\r\n});\r\n\r\nipcMain.handle('clipboard-read-text', async () => {\r\n try {\r\n const text = clipboard.readText();\r\n return { success: true, text };\r\n } catch (error: any) {\r\n log.error('Clipboard read failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('clipboard-write-text', async (event, text: string) => {\r\n try {\r\n clipboard.writeText(text);\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Clipboard write failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nasync function startServer() {\r\n if (electronState.isDevelopment()) {\r\n log.info('In dev mode, assuming external servers are running.');\r\n return;\r\n }\r\n\r\n const serverPath = path.join(__dirname, '../dist-api/server.js');\r\n const serverUrl = pathToFileURL(serverPath).href;\r\n\r\n log.info(`Starting internal server from: ${serverPath}`);\r\n try {\r\n const serverModule = await import(serverUrl);\r\n if (serverModule.startServer) {\r\n const port = await serverModule.startServer();\r\n electronState.setServerPort(port);\r\n log.info(`Internal server started successfully on port ${port}`);\r\n } else {\r\n log.warn('startServer function not found in server module, using default port 3001');\r\n }\r\n } catch (e) {\r\n log.error('Failed to start internal server:', e);\r\n }\r\n}\r\n\r\napp.whenReady().then(async () => {\r\n await startServer();\r\n await createWindow();\r\n\r\n startClipboardWatcher();\r\n\r\n globalShortcut.register('CommandOrControl+Shift+C', () => {\r\n log.info('Global shortcut: sync clipboard to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-to-remote');\r\n }\r\n });\r\n\r\n globalShortcut.register('CommandOrControl+Shift+V', () => {\r\n log.info('Global shortcut: sync clipboard from remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-from-remote');\r\n }\r\n });\r\n\r\n app.on('activate', () => {\r\n if (BrowserWindow.getAllWindows().length === 0) {\r\n createWindow();\r\n }\r\n });\r\n});\r\n\r\napp.on('window-all-closed', () => {\r\n globalShortcut.unregisterAll();\r\n if (process.platform !== 'darwin') {\r\n app.quit();\r\n }\r\n});\r\n","import { BrowserWindow } from 'electron';\n\n/**\n * 生成 PDF 的服务\n * @param htmlContent 完整的 HTML 字符串\n * @returns PDF 文件的二进制数据\n */\nexport async function generatePdf(htmlContent: string): Promise {\n const printWin = new BrowserWindow({\n show: false,\n webPreferences: {\n nodeIntegration: false,\n contextIsolation: true,\n sandbox: false, // 与 main.ts 保持一致,确保脚本执行权限\n }\n });\n\n try {\n await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);\n \n // 等待资源加载完成 (由 generatePrintHtml 注入的脚本控制)\n await printWin.webContents.executeJavaScript(`\n new Promise(resolve => {\n const check = () => {\n if (window.__PRINT_READY__) {\n resolve();\n } else {\n setTimeout(check, 100);\n }\n }\n check();\n })\n `);\n\n const pdfData = await printWin.webContents.printToPDF({\n printBackground: true,\n pageSize: 'A4',\n margins: { top: 0, bottom: 0, left: 0, right: 0 }\n });\n\n return pdfData;\n } finally {\n // 确保窗口被关闭,防止内存泄漏\n printWin.close();\n }\n}\n","import { dialog, BrowserWindow } from 'electron'\nimport path from 'path'\nimport fs from 'fs'\nimport log from 'electron-log'\n\nexport interface HtmlImportResult {\n success: boolean\n canceled?: boolean\n error?: string\n htmlPath?: string\n htmlDir?: string\n htmlFileName?: string\n assetsDirName?: string\n assetsFiles?: string[]\n}\n\nexport const selectHtmlFile = async (win: BrowserWindow | null): Promise => {\n if (!win) return { success: false, error: 'No window found' }\n\n try {\n const { filePaths, canceled } = await dialog.showOpenDialog(win, {\n title: '选择 HTML 文件',\n filters: [\n { name: 'HTML Files', extensions: ['html', 'htm'] }\n ],\n properties: ['openFile']\n })\n\n if (canceled || filePaths.length === 0) {\n return { success: false, canceled: true }\n }\n\n const htmlPath = filePaths[0]\n const htmlDir = path.dirname(htmlPath)\n const htmlFileName = path.basename(htmlPath, path.extname(htmlPath))\n \n const assetsDirName = `${htmlFileName}_files`\n const assetsDirPath = path.join(htmlDir, assetsDirName)\n \n const assetsFiles: string[] = []\n if (fs.existsSync(assetsDirPath)) {\n const collectFiles = (dir: string, baseDir: string) => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n collectFiles(fullPath, baseDir)\n } else {\n const relPath = path.relative(baseDir, fullPath)\n assetsFiles.push(relPath)\n }\n }\n }\n collectFiles(assetsDirPath, assetsDirPath)\n }\n\n return { \n success: true, \n htmlPath,\n htmlDir,\n htmlFileName,\n assetsDirName,\n assetsFiles\n }\n } catch (error: any) {\n log.error('Select HTML file failed:', error)\n return { success: false, error: error.message }\n }\n}\n","import { BrowserWindow } from 'electron'\n\ninterface ElectronAppState {\n mainWindow: BrowserWindow | null\n serverPort: number\n isDev: boolean\n}\n\nclass ElectronState {\n private state: ElectronAppState = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n\n getMainWindow(): BrowserWindow | null {\n return this.state.mainWindow\n }\n\n setMainWindow(window: BrowserWindow | null): void {\n this.state.mainWindow = window\n }\n\n getServerPort(): number {\n return this.state.serverPort\n }\n\n setServerPort(port: number): void {\n this.state.serverPort = port\n }\n\n isDevelopment(): boolean {\n return this.state.isDev\n }\n\n setDevelopment(isDev: boolean): void {\n this.state.isDev = isDev\n }\n\n reset(): void {\n this.state = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n }\n}\n\nexport const electronState = new ElectronState()\n"],"mappings":"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","names":["BrowserWindow","dialog","path","fs","log","log","path","fs","BrowserWindow","dialog"]} \ No newline at end of file +{"version":3,"sources":["../electron/main.ts","../electron/services/pdfGenerator.ts","../electron/services/htmlImport.ts","../electron/state.ts"],"sourcesContent":["import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron';\r\nimport path from 'path';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport fs from 'fs';\r\nimport log from 'electron-log';\r\nimport { generatePdf } from './services/pdfGenerator';\r\nimport { selectHtmlFile } from './services/htmlImport';\r\nimport { electronState } from './state';\r\n\r\nlog.initialize();\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nprocess.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCDesktop');\r\n\r\nif (!fs.existsSync(process.env.NOTEBOOK_ROOT)) {\r\n try {\r\n fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true });\r\n } catch (err) {\r\n log.error('Failed to create notebook directory:', err);\r\n }\r\n}\r\n\r\nelectronState.setDevelopment(!app.isPackaged);\r\n\r\nlet lastClipboardText = '';\r\n\r\nfunction startClipboardWatcher() {\r\n lastClipboardText = clipboard.readText();\r\n \r\n setInterval(() => {\r\n try {\r\n const currentText = clipboard.readText();\r\n if (currentText && currentText !== lastClipboardText) {\r\n lastClipboardText = currentText;\r\n log.info('Clipboard changed, syncing to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-auto-sync', currentText);\r\n }\r\n }\r\n } catch (e) {\r\n // ignore\r\n }\r\n }, 1000);\r\n}\r\n\r\nasync function createWindow() {\r\n const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000';\r\n \r\n const mainWindow = new BrowserWindow({\r\n width: 1280,\r\n height: 800,\r\n minWidth: 1600,\r\n minHeight: 900,\r\n autoHideMenuBar: true,\r\n titleBarStyle: 'hidden',\r\n titleBarOverlay: {\r\n color: '#00000000',\r\n symbolColor: initialSymbolColor,\r\n height: 32,\r\n },\r\n webPreferences: {\r\n nodeIntegration: false,\r\n contextIsolation: true,\r\n sandbox: false,\r\n webviewTag: true,\r\n preload: path.join(__dirname, 'preload.cjs'),\r\n },\r\n });\r\n\r\n electronState.setMainWindow(mainWindow);\r\n mainWindow.setMenu(null);\r\n\r\n mainWindow.webContents.setWindowOpenHandler(({ url }) => {\r\n if (url.startsWith('http:') || url.startsWith('https:')) {\r\n shell.openExternal(url);\r\n return { action: 'deny' };\r\n }\r\n return { action: 'allow' };\r\n });\r\n\r\n if (electronState.isDevelopment()) {\r\n log.info('Loading development URL...');\r\n try {\r\n await mainWindow.loadURL('http://localhost:5173');\r\n } catch (e) {\r\n log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e);\r\n }\r\n mainWindow.webContents.openDevTools();\r\n } else {\r\n log.info(`Loading production URL with port ${electronState.getServerPort()}...`);\r\n await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`);\r\n }\r\n}\r\n\r\nipcMain.handle('export-pdf', async (event, title, htmlContent) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (!win) return { success: false, error: 'No window found' };\r\n\r\n try {\r\n const { filePath } = await dialog.showSaveDialog(win, {\r\n title: '导出 PDF',\r\n defaultPath: `${title}.pdf`,\r\n filters: [{ name: 'PDF Files', extensions: ['pdf'] }]\r\n });\r\n\r\n if (!filePath) return { success: false, canceled: true };\r\n\r\n if (!htmlContent) {\r\n throw new Error('No HTML content provided for PDF export');\r\n }\r\n\r\n const pdfData = await generatePdf(htmlContent);\r\n fs.writeFileSync(filePath, pdfData);\r\n\r\n return { success: true, filePath };\r\n } catch (error: any) {\r\n log.error('Export PDF failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('select-html-file', async (event) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n return selectHtmlFile(win);\r\n});\r\n\r\nipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (win) {\r\n win.setTitleBarOverlay({ symbolColor });\r\n return { success: true };\r\n }\r\n return { success: false };\r\n});\r\n\r\nipcMain.handle('clipboard-read-text', async () => {\r\n try {\r\n const text = clipboard.readText();\r\n return { success: true, text };\r\n } catch (error: any) {\r\n log.error('Clipboard read failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('clipboard-write-text', async (event, text: string) => {\r\n try {\r\n clipboard.writeText(text);\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Clipboard write failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-fetch-drives', async (_event, serverHost: string, port: number, password?: string) => {\r\n try {\r\n let url = `http://${serverHost}:${port}/api/files/drives`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch drives: ${response.statusText}`);\r\n }\r\n const data = await response.json();\r\n const items = data.items || [];\r\n return {\r\n success: true,\r\n data: items.map((item: { name: string; isDirectory: boolean; size: number }) => ({\r\n name: item.name,\r\n path: item.name,\r\n type: item.isDirectory ? 'dir' : 'file',\r\n size: item.size,\r\n modified: '',\r\n }))\r\n };\r\n } catch (error: any) {\r\n log.error('Remote fetch drives failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-fetch-files', async (_event, serverHost: string, port: number, filePath: string, password?: string) => {\r\n try {\r\n let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(filePath)}&allowSystem=true`;\r\n if (password) {\r\n url += `&password=${encodeURIComponent(password)}`;\r\n }\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch files: ${response.statusText}`);\r\n }\r\n const data = await response.json();\r\n const items = data.items || [];\r\n return {\r\n success: true,\r\n data: items.map((item: { name: string; isDirectory: boolean; size: number; modified: Date }) => ({\r\n name: item.name,\r\n path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name,\r\n type: item.isDirectory ? 'dir' : 'file',\r\n size: item.size,\r\n modified: item.modified?.toString(),\r\n }))\r\n };\r\n } catch (error: any) {\r\n log.error('Remote fetch files failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-upload-file', async (_event, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => {\r\n try {\r\n const win = electronState.getMainWindow();\r\n if (!win) {\r\n throw new Error('No window found');\r\n }\r\n\r\n const fullPath = path.resolve(filePath);\r\n if (!fs.existsSync(fullPath)) {\r\n throw new Error('File not found');\r\n }\r\n\r\n const fileBuffer = fs.readFileSync(fullPath);\r\n const fileName = path.basename(fullPath);\r\n\r\n let url = `http://${serverHost}:${port}/api/files/upload`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n\r\n const formData = new FormData();\r\n const blob = new Blob([fileBuffer]);\r\n formData.append('file', blob, fileName);\r\n if (remotePath) {\r\n formData.append('path', remotePath);\r\n }\r\n\r\n const response = await fetch(url, {\r\n method: 'POST',\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`Upload failed: ${response.statusText}`);\r\n }\r\n\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Remote upload failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-download-file', async (_event, serverHost: string, port: number, fileName: string, remotePath: string, password?: string) => {\r\n try {\r\n const win = electronState.getMainWindow();\r\n if (!win) {\r\n throw new Error('No window found');\r\n }\r\n\r\n let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fileName)}`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Download failed: ${response.statusText}`);\r\n }\r\n\r\n const buffer = await response.arrayBuffer();\r\n\r\n const { filePath } = await dialog.showSaveDialog(win, {\r\n title: '保存文件',\r\n defaultPath: fileName,\r\n });\r\n\r\n if (!filePath) {\r\n return { success: false, canceled: true };\r\n }\r\n\r\n fs.writeFileSync(filePath, Buffer.from(buffer));\r\n\r\n return { success: true, filePath };\r\n } catch (error: any) {\r\n log.error('Remote download failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nasync function startServer() {\r\n if (electronState.isDevelopment()) {\r\n log.info('In dev mode, assuming external servers are running.');\r\n return;\r\n }\r\n\r\n const serverPath = path.join(__dirname, '../dist-api/server.js');\r\n const serverUrl = pathToFileURL(serverPath).href;\r\n\r\n log.info(`Starting internal server from: ${serverPath}`);\r\n try {\r\n const serverModule = await import(serverUrl);\r\n if (serverModule.startServer) {\r\n const port = await serverModule.startServer();\r\n electronState.setServerPort(port);\r\n log.info(`Internal server started successfully on port ${port}`);\r\n } else {\r\n log.warn('startServer function not found in server module, using default port 3001');\r\n }\r\n } catch (e) {\r\n log.error('Failed to start internal server:', e);\r\n }\r\n}\r\n\r\napp.whenReady().then(async () => {\r\n await startServer();\r\n await createWindow();\r\n\r\n startClipboardWatcher();\r\n\r\n globalShortcut.register('CommandOrControl+Shift+C', () => {\r\n log.info('Global shortcut: sync clipboard to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-to-remote');\r\n }\r\n });\r\n\r\n globalShortcut.register('CommandOrControl+Shift+V', () => {\r\n log.info('Global shortcut: sync clipboard from remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-from-remote');\r\n }\r\n });\r\n\r\n app.on('activate', () => {\r\n if (BrowserWindow.getAllWindows().length === 0) {\r\n createWindow();\r\n }\r\n });\r\n});\r\n\r\napp.on('window-all-closed', () => {\r\n globalShortcut.unregisterAll();\r\n if (process.platform !== 'darwin') {\r\n app.quit();\r\n }\r\n});\r\n","import { BrowserWindow } from 'electron';\n\n/**\n * 生成 PDF 的服务\n * @param htmlContent 完整的 HTML 字符串\n * @returns PDF 文件的二进制数据\n */\nexport async function generatePdf(htmlContent: string): Promise {\n const printWin = new BrowserWindow({\n show: false,\n webPreferences: {\n nodeIntegration: false,\n contextIsolation: true,\n sandbox: false, // 与 main.ts 保持一致,确保脚本执行权限\n }\n });\n\n try {\n await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);\n \n // 等待资源加载完成 (由 generatePrintHtml 注入的脚本控制)\n await printWin.webContents.executeJavaScript(`\n new Promise(resolve => {\n const check = () => {\n if (window.__PRINT_READY__) {\n resolve();\n } else {\n setTimeout(check, 100);\n }\n }\n check();\n })\n `);\n\n const pdfData = await printWin.webContents.printToPDF({\n printBackground: true,\n pageSize: 'A4',\n margins: { top: 0, bottom: 0, left: 0, right: 0 }\n });\n\n return pdfData;\n } finally {\n // 确保窗口被关闭,防止内存泄漏\n printWin.close();\n }\n}\n","import { dialog, BrowserWindow } from 'electron'\nimport path from 'path'\nimport fs from 'fs'\nimport log from 'electron-log'\n\nexport interface HtmlImportResult {\n success: boolean\n canceled?: boolean\n error?: string\n htmlPath?: string\n htmlDir?: string\n htmlFileName?: string\n assetsDirName?: string\n assetsFiles?: string[]\n}\n\nexport const selectHtmlFile = async (win: BrowserWindow | null): Promise => {\n if (!win) return { success: false, error: 'No window found' }\n\n try {\n const { filePaths, canceled } = await dialog.showOpenDialog(win, {\n title: '选择 HTML 文件',\n filters: [\n { name: 'HTML Files', extensions: ['html', 'htm'] }\n ],\n properties: ['openFile']\n })\n\n if (canceled || filePaths.length === 0) {\n return { success: false, canceled: true }\n }\n\n const htmlPath = filePaths[0]\n const htmlDir = path.dirname(htmlPath)\n const htmlFileName = path.basename(htmlPath, path.extname(htmlPath))\n \n const assetsDirName = `${htmlFileName}_files`\n const assetsDirPath = path.join(htmlDir, assetsDirName)\n \n const assetsFiles: string[] = []\n if (fs.existsSync(assetsDirPath)) {\n const collectFiles = (dir: string, baseDir: string) => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n collectFiles(fullPath, baseDir)\n } else {\n const relPath = path.relative(baseDir, fullPath)\n assetsFiles.push(relPath)\n }\n }\n }\n collectFiles(assetsDirPath, assetsDirPath)\n }\n\n return { \n success: true, \n htmlPath,\n htmlDir,\n htmlFileName,\n assetsDirName,\n assetsFiles\n }\n } catch (error: any) {\n log.error('Select HTML file failed:', error)\n return { success: false, error: error.message }\n }\n}\n","import { BrowserWindow } from 'electron'\n\ninterface ElectronAppState {\n mainWindow: BrowserWindow | null\n serverPort: number\n isDev: boolean\n}\n\nclass ElectronState {\n private state: ElectronAppState = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n\n getMainWindow(): BrowserWindow | null {\n return this.state.mainWindow\n }\n\n setMainWindow(window: BrowserWindow | null): void {\n this.state.mainWindow = window\n }\n\n getServerPort(): number {\n return this.state.serverPort\n }\n\n setServerPort(port: number): void {\n this.state.serverPort = port\n }\n\n isDevelopment(): boolean {\n return this.state.isDev\n }\n\n setDevelopment(isDev: boolean): void {\n this.state.isDev = isDev\n }\n\n reset(): void {\n this.state = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n }\n}\n\nexport const electronState = new ElectronState()\n"],"mappings":"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","names":["BrowserWindow","dialog","path","fs","log","log","path","fs","BrowserWindow","dialog"]} \ No newline at end of file diff --git a/dist-electron/preload.cjs b/dist-electron/preload.cjs index d2bd941..de2d8f1 100644 --- a/dist-electron/preload.cjs +++ b/dist-electron/preload.cjs @@ -19,6 +19,10 @@ import_electron.contextBridge.exposeInMainWorld("electronAPI", { return () => import_electron.ipcRenderer.removeListener("remote-clipboard-auto-sync", handler); }, clipboardReadText: () => import_electron.ipcRenderer.invoke("clipboard-read-text"), - clipboardWriteText: (text) => import_electron.ipcRenderer.invoke("clipboard-write-text", text) + clipboardWriteText: (text) => import_electron.ipcRenderer.invoke("clipboard-write-text", text), + remoteFetchDrives: (serverHost, port, password) => import_electron.ipcRenderer.invoke("remote-fetch-drives", serverHost, port, password), + remoteFetchFiles: (serverHost, port, filePath, password) => import_electron.ipcRenderer.invoke("remote-fetch-files", serverHost, port, filePath, password), + remoteUploadFile: (serverHost, port, filePath, remotePath, password) => import_electron.ipcRenderer.invoke("remote-upload-file", serverHost, port, filePath, remotePath, password), + remoteDownloadFile: (serverHost, port, fileName, remotePath, password) => import_electron.ipcRenderer.invoke("remote-download-file", serverHost, port, fileName, remotePath, password) }); //# sourceMappingURL=preload.cjs.map \ No newline at end of file diff --git a/dist-electron/preload.cjs.map b/dist-electron/preload.cjs.map index f069fd9..f044c88 100644 --- a/dist-electron/preload.cjs.map +++ b/dist-electron/preload.cjs.map @@ -1 +1 @@ -{"version":3,"sources":["../electron/preload.ts"],"sourcesContent":["import { contextBridge, ipcRenderer } from 'electron'\n\nconsole.log('--- PRELOAD SCRIPT LOADED SUCCESSFULLY ---')\n\ncontextBridge.exposeInMainWorld('electronAPI', {\n exportPDF: (title: string, htmlContent?: string) => ipcRenderer.invoke('export-pdf', title, htmlContent),\n selectHtmlFile: () => ipcRenderer.invoke('select-html-file'),\n updateTitlebarButtons: (symbolColor: string) => ipcRenderer.invoke('update-titlebar-buttons', symbolColor),\n onRemoteClipboardSyncToRemote: (callback: () => void) => {\n ipcRenderer.on('remote-clipboard-sync-to-remote', callback);\n return () => ipcRenderer.removeListener('remote-clipboard-sync-to-remote', callback);\n },\n onRemoteClipboardSyncFromRemote: (callback: () => void) => {\n ipcRenderer.on('remote-clipboard-sync-from-remote', callback);\n return () => ipcRenderer.removeListener('remote-clipboard-sync-from-remote', callback);\n },\n onRemoteClipboardAutoSync: (callback: (text: string) => void) => {\n const handler = (_event: Electron.IpcRendererEvent, text: string) => callback(text);\n ipcRenderer.on('remote-clipboard-auto-sync', handler);\n return () => ipcRenderer.removeListener('remote-clipboard-auto-sync', handler);\n },\n clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'),\n clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text),\n})\n"],"mappings":";AAAA,sBAA2C;AAE3C,QAAQ,IAAI,4CAA4C;AAExD,8BAAc,kBAAkB,eAAe;AAAA,EAC7C,WAAW,CAAC,OAAe,gBAAyB,4BAAY,OAAO,cAAc,OAAO,WAAW;AAAA,EACvG,gBAAgB,MAAM,4BAAY,OAAO,kBAAkB;AAAA,EAC3D,uBAAuB,CAAC,gBAAwB,4BAAY,OAAO,2BAA2B,WAAW;AAAA,EACzG,+BAA+B,CAAC,aAAyB;AACvD,gCAAY,GAAG,mCAAmC,QAAQ;AAC1D,WAAO,MAAM,4BAAY,eAAe,mCAAmC,QAAQ;AAAA,EACrF;AAAA,EACA,iCAAiC,CAAC,aAAyB;AACzD,gCAAY,GAAG,qCAAqC,QAAQ;AAC5D,WAAO,MAAM,4BAAY,eAAe,qCAAqC,QAAQ;AAAA,EACvF;AAAA,EACA,2BAA2B,CAAC,aAAqC;AAC/D,UAAM,UAAU,CAAC,QAAmC,SAAiB,SAAS,IAAI;AAClF,gCAAY,GAAG,8BAA8B,OAAO;AACpD,WAAO,MAAM,4BAAY,eAAe,8BAA8B,OAAO;AAAA,EAC/E;AAAA,EACA,mBAAmB,MAAM,4BAAY,OAAO,qBAAqB;AAAA,EACjE,oBAAoB,CAAC,SAAiB,4BAAY,OAAO,wBAAwB,IAAI;AACvF,CAAC;","names":[]} \ No newline at end of file +{"version":3,"sources":["../electron/preload.ts"],"sourcesContent":["import { contextBridge, ipcRenderer } from 'electron'\r\n\r\nconsole.log('--- PRELOAD SCRIPT LOADED SUCCESSFULLY ---')\r\n\r\ncontextBridge.exposeInMainWorld('electronAPI', {\r\n exportPDF: (title: string, htmlContent?: string) => ipcRenderer.invoke('export-pdf', title, htmlContent),\r\n selectHtmlFile: () => ipcRenderer.invoke('select-html-file'),\r\n updateTitlebarButtons: (symbolColor: string) => ipcRenderer.invoke('update-titlebar-buttons', symbolColor),\r\n onRemoteClipboardSyncToRemote: (callback: () => void) => {\r\n ipcRenderer.on('remote-clipboard-sync-to-remote', callback);\r\n return () => ipcRenderer.removeListener('remote-clipboard-sync-to-remote', callback);\r\n },\r\n onRemoteClipboardSyncFromRemote: (callback: () => void) => {\r\n ipcRenderer.on('remote-clipboard-sync-from-remote', callback);\r\n return () => ipcRenderer.removeListener('remote-clipboard-sync-from-remote', callback);\r\n },\r\n onRemoteClipboardAutoSync: (callback: (text: string) => void) => {\r\n const handler = (_event: Electron.IpcRendererEvent, text: string) => callback(text);\r\n ipcRenderer.on('remote-clipboard-auto-sync', handler);\r\n return () => ipcRenderer.removeListener('remote-clipboard-auto-sync', handler);\r\n },\r\n clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'),\r\n clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text),\r\n remoteFetchDrives: (serverHost: string, port: number, password?: string) => \r\n ipcRenderer.invoke('remote-fetch-drives', serverHost, port, password),\r\n remoteFetchFiles: (serverHost: string, port: number, filePath: string, password?: string) => \r\n ipcRenderer.invoke('remote-fetch-files', serverHost, port, filePath, password),\r\n remoteUploadFile: (serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => \r\n ipcRenderer.invoke('remote-upload-file', serverHost, port, filePath, remotePath, password),\r\n remoteDownloadFile: (serverHost: string, port: number, fileName: string, remotePath: string, password?: string) => \r\n ipcRenderer.invoke('remote-download-file', serverHost, port, fileName, remotePath, password),\r\n})\r\n"],"mappings":";AAAA,sBAA2C;AAE3C,QAAQ,IAAI,4CAA4C;AAExD,8BAAc,kBAAkB,eAAe;AAAA,EAC7C,WAAW,CAAC,OAAe,gBAAyB,4BAAY,OAAO,cAAc,OAAO,WAAW;AAAA,EACvG,gBAAgB,MAAM,4BAAY,OAAO,kBAAkB;AAAA,EAC3D,uBAAuB,CAAC,gBAAwB,4BAAY,OAAO,2BAA2B,WAAW;AAAA,EACzG,+BAA+B,CAAC,aAAyB;AACvD,gCAAY,GAAG,mCAAmC,QAAQ;AAC1D,WAAO,MAAM,4BAAY,eAAe,mCAAmC,QAAQ;AAAA,EACrF;AAAA,EACA,iCAAiC,CAAC,aAAyB;AACzD,gCAAY,GAAG,qCAAqC,QAAQ;AAC5D,WAAO,MAAM,4BAAY,eAAe,qCAAqC,QAAQ;AAAA,EACvF;AAAA,EACA,2BAA2B,CAAC,aAAqC;AAC/D,UAAM,UAAU,CAAC,QAAmC,SAAiB,SAAS,IAAI;AAClF,gCAAY,GAAG,8BAA8B,OAAO;AACpD,WAAO,MAAM,4BAAY,eAAe,8BAA8B,OAAO;AAAA,EAC/E;AAAA,EACA,mBAAmB,MAAM,4BAAY,OAAO,qBAAqB;AAAA,EACjE,oBAAoB,CAAC,SAAiB,4BAAY,OAAO,wBAAwB,IAAI;AAAA,EACrF,mBAAmB,CAAC,YAAoB,MAAc,aACpD,4BAAY,OAAO,uBAAuB,YAAY,MAAM,QAAQ;AAAA,EACtE,kBAAkB,CAAC,YAAoB,MAAc,UAAkB,aACrE,4BAAY,OAAO,sBAAsB,YAAY,MAAM,UAAU,QAAQ;AAAA,EAC/E,kBAAkB,CAAC,YAAoB,MAAc,UAAkB,YAAoB,aACzF,4BAAY,OAAO,sBAAsB,YAAY,MAAM,UAAU,YAAY,QAAQ;AAAA,EAC3F,oBAAoB,CAAC,YAAoB,MAAc,UAAkB,YAAoB,aAC3F,4BAAY,OAAO,wBAAwB,YAAY,MAAM,UAAU,YAAY,QAAQ;AAC/F,CAAC;","names":[]} \ No newline at end of file diff --git a/remote.err b/remote.err new file mode 100644 index 0000000..e69de29 diff --git a/remote/frp/frpc-runtime.toml b/remote/frp/frpc-runtime.toml index 8c775ca..5ed8232 100644 --- a/remote/frp/frpc-runtime.toml +++ b/remote/frp/frpc-runtime.toml @@ -27,4 +27,11 @@ localIP = "127.0.0.1" localPort = 3002 remotePort = 8082 +[[proxies]] +name = "filetransfer-remote" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3003 +remotePort = 8083 + diff --git a/remote/src/routes/files.js b/remote/src/routes/files.js index 5c0af9f..c477ff6 100644 --- a/remote/src/routes/files.js +++ b/remote/src/routes/files.js @@ -68,6 +68,17 @@ router.get('/browse', (req, res) => { try { const filePath = req.query.path || ''; const allowSystem = req.query.allowSystem === 'true'; + + if (allowSystem && !filePath) { + const drives = fileService.getDrives(); + res.json({ + items: drives, + currentPath: '', + parentPath: null + }); + return; + } + const result = fileService.browseDirectory(filePath, allowSystem); res.json(result); } catch (error) { diff --git a/remote/src/server/FileHandler.js b/remote/src/server/FileHandler.js new file mode 100644 index 0000000..20ff15d --- /dev/null +++ b/remote/src/server/FileHandler.js @@ -0,0 +1,261 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const logger = require('../utils/logger'); +const MessageTypes = require('./messageTypes'); +const { fileService } = require('../services/file'); + +const CHUNK_SIZE = 5 * 1024 * 1024; + +class FileHandler { + constructor() { + this.uploadSessions = new Map(); + } + + handleMessage(message, ws) { + const { type, requestId, ...data } = message; + + switch (type) { + case MessageTypes.FILE_LIST_GET: + this.handleFileList(ws, requestId); + break; + case MessageTypes.FILE_BROWSE: + this.handleFileBrowse(ws, data, requestId); + break; + case MessageTypes.FILE_UPLOAD_START: + this.handleFileUploadStart(ws, data, requestId); + break; + case MessageTypes.FILE_UPLOAD_CHUNK: + this.handleFileUploadChunk(ws, data, requestId); + break; + case MessageTypes.FILE_UPLOAD_MERGE: + this.handleFileUploadMerge(ws, data, requestId); + break; + case MessageTypes.FILE_DOWNLOAD_START: + this.handleFileDownload(ws, data, requestId); + break; + case MessageTypes.FILE_DELETE: + this.handleFileDelete(ws, data, requestId); + break; + default: + logger.debug('Unknown file message type', { type }); + } + } + + sendResponse(ws, type, requestId, payload) { + ws.send(JSON.stringify({ + type, + requestId, + ...payload + })); + } + + handleFileList(ws, requestId) { + try { + const files = fileService.getFileList(); + this.sendResponse(ws, MessageTypes.FILE_LIST, requestId, { files }); + } catch (error) { + logger.error('Failed to get file list', { error: error.message }); + this.sendResponse(ws, MessageTypes.FILE_LIST, requestId, { files: [], error: error.message }); + } + } + + handleFileBrowse(ws, data, requestId) { + try { + const { path: dirPath, allowSystem } = data; + + if (allowSystem && !dirPath) { + const drives = fileService.getDrives(); + this.sendResponse(ws, MessageTypes.FILE_BROWSE_RESULT, requestId, { + items: drives, + currentPath: '', + parentPath: null + }); + return; + } + + const result = fileService.browseDirectory(dirPath || '', allowSystem === true); + this.sendResponse(ws, MessageTypes.FILE_BROWSE_RESULT, requestId, result); + } catch (error) { + logger.error('Failed to browse directory', { error: error.message }); + this.sendResponse(ws, MessageTypes.FILE_BROWSE_RESULT, requestId, { items: [], error: error.message }); + } + } + + handleFileUploadStart(ws, data, requestId) { + try { + const { filename, totalChunks, fileSize } = data; + const fileId = data.fileId || requestId || crypto.randomBytes(16).toString('hex'); + + this.uploadSessions.set(fileId, { + filename, + totalChunks, + fileSize, + chunks: new Map(), + createdAt: Date.now() + }); + + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_START, requestId, { + fileId, + chunkSize: CHUNK_SIZE, + message: 'Upload session started' + }); + } catch (error) { + logger.error('Failed to start upload', { error: error.message }); + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: error.message }); + } + } + + handleFileUploadChunk(ws, data, requestId) { + try { + const { fileId, chunkIndex } = data; + const session = this.uploadSessions.get(fileId); + + if (!session) { + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: 'Upload session not found' }); + return; + } + + let chunkData; + if (data.data) { + chunkData = Buffer.from(data.data, 'base64'); + } else if (data.buffer) { + chunkData = Buffer.from(data.buffer); + } + + if (!chunkData) { + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: 'No chunk data provided' }); + return; + } + + session.chunks.set(chunkIndex, chunkData); + + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_CHUNK, requestId, { success: true, chunkIndex }); + } catch (error) { + logger.error('Failed to upload chunk', { error: error.message }); + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: error.message }); + } + } + + handleFileUploadMerge(ws, data, requestId) { + try { + const { fileId, filename } = data; + const session = this.uploadSessions.get(fileId); + + if (!session) { + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: 'Upload session not found' }); + return; + } + + const success = fileService.mergeChunks(fileId, session.totalChunks, filename); + + this.uploadSessions.delete(fileId); + + if (success) { + fileService.cleanupChunks(fileId); + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: true, filename }); + } else { + fileService.cleanupChunks(fileId); + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: 'Failed to merge chunks' }); + } + } catch (error) { + logger.error('Failed to merge chunks', { error: error.message }); + this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: error.message }); + } + } + + handleFileDownload(ws, data, requestId) { + try { + const { filename, filePath, allowSystem } = data; + + let fullFilePath; + if (allowSystem && filePath && (path.isAbsolute(filePath) || filePath.includes(':') || filePath.startsWith('\\') || filePath.startsWith('/'))) { + if (path.isAbsolute(filePath)) { + fullFilePath = filePath; + } else { + fullFilePath = filePath.replace(/\//g, '\\'); + } + } else if (filePath) { + fullFilePath = path.join(fileService.uploadDir, filePath); + } else { + fullFilePath = path.join(fileService.uploadDir, filename); + } + + if (!fs.existsSync(fullFilePath)) { + this.sendResponse(ws, MessageTypes.FILE_DOWNLOAD_COMPLETE, requestId, { success: false, error: 'File not found: ' + fullFilePath }); + return; + } + + const stat = fs.statSync(fullFilePath); + const fileSize = stat.size; + const totalChunks = Math.ceil(fileSize / CHUNK_SIZE); + + this.sendResponse(ws, MessageTypes.FILE_DOWNLOAD_START, requestId, { + filename, + size: fileSize, + chunkSize: CHUNK_SIZE, + totalChunks + }); + + const stream = fs.createReadStream(fullFilePath); + let chunkIndex = 0; + + stream.on('data', (chunk) => { + ws.send(JSON.stringify({ + type: MessageTypes.FILE_DOWNLOAD_CHUNK, + chunkIndex, + data: chunk.toString('base64'), + progress: Math.round(((chunkIndex + 1) / totalChunks) * 100) + })); + + chunkIndex++; + }); + + stream.on('end', () => { + ws.send(JSON.stringify({ + type: MessageTypes.FILE_DOWNLOAD_COMPLETE, + success: true, + filename + })); + }); + + stream.on('error', (error) => { + logger.error('File download error', { error: error.message }); + ws.send(JSON.stringify({ + type: MessageTypes.FILE_DOWNLOAD_COMPLETE, + success: false, + error: error.message + })); + }); + } catch (error) { + logger.error('Failed to start download', { error: error.message }); + this.sendResponse(ws, MessageTypes.FILE_DOWNLOAD_COMPLETE, requestId, { success: false, error: error.message }); + } + } + + handleFileDelete(ws, data, requestId) { + try { + const { filename, filePath } = data; + const targetPath = filePath || ''; + const success = fileService.deleteFile(filename, targetPath); + + this.sendResponse(ws, MessageTypes.FILE_DELETE_RESULT, requestId, { success, filename }); + } catch (error) { + logger.error('Failed to delete file', { error: error.message }); + this.sendResponse(ws, MessageTypes.FILE_DELETE_RESULT, requestId, { success: false, error: error.message }); + } + } + + cleanup() { + const now = Date.now(); + const maxAge = 30 * 60 * 1000; + + for (const [fileId, session] of this.uploadSessions) { + if (now - session.createdAt > maxAge) { + this.uploadSessions.delete(fileId); + } + } + } +} + +module.exports = FileHandler; diff --git a/remote/src/server/FileServer.js b/remote/src/server/FileServer.js new file mode 100644 index 0000000..ce17532 --- /dev/null +++ b/remote/src/server/FileServer.js @@ -0,0 +1,78 @@ +const express = require('express'); +const http = require('http'); +const WebSocket = require('ws'); +const logger = require('../utils/logger'); +const FileHandler = require('./FileHandler'); + +class FileServer { + constructor(port = 3001) { + this.port = port; + this.app = express(); + this.server = http.createServer(this.app); + this.wss = null; + this.fileHandler = new FileHandler(); + } + + start() { + return new Promise((resolve, reject) => { + this.server.listen({ port: this.port, host: '0.0.0.0' }, () => { + logger.info('File server started', { port: this.port }); + this._setupWebSocket(); + resolve({ port: this.port }); + }); + this.server.on('error', reject); + }); + } + + _setupWebSocket() { + this.wss = new WebSocket.Server({ server: this.server, path: '/ws' }); + + this.wss.on('connection', (ws, req) => { + logger.info('File client connected', { ip: req.socket.remoteAddress }); + + ws.on('message', (data, isBinary) => { + try { + if (isBinary) { + logger.warn('Received binary data on file WebSocket, ignoring'); + return; + } + + const dataStr = data.toString(); + logger.info('Raw message received:', dataStr.substring(0, 300)); + const message = JSON.parse(dataStr); + logger.info('File message parsed', { type: message.type, requestId: message.requestId }); + this.fileHandler.handleMessage(message, ws); + } catch (error) { + logger.error('Failed to parse file message', { error: error.message }); + } + }); + + ws.on('close', () => { + logger.info('File client disconnected'); + }); + + ws.on('error', (error) => { + logger.error('File WebSocket error', { error: error.message }); + }); + }); + + this.wss.on('error', (error) => { + logger.error('File WebSocket server error', { error: error.message }); + }); + } + + stop() { + return new Promise((resolve, reject) => { + if (this.wss) { + this.wss.clients.forEach(client => client.close()); + this.wss.close(); + } + this.server.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } +} + +module.exports = FileServer; diff --git a/remote/src/services/file/FileService.js b/remote/src/services/file/FileService.js index 81e982b..2e9100d 100644 --- a/remote/src/services/file/FileService.js +++ b/remote/src/services/file/FileService.js @@ -44,7 +44,8 @@ class FileService { } getFilePath(filename) { - const filePath = path.join(this.uploadDir, path.basename(filename)); + if (!filename) return null; + const filePath = path.normalize(filename); if (!fs.existsSync(filePath)) { return null; } @@ -92,7 +93,13 @@ class FileService { mergeChunks(fileId, totalChunks, filename) { try { - const filePath = path.join(this.uploadDir, path.basename(filename)); + const filePath = path.normalize(filename); + const dir = path.dirname(filePath); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + const fd = fs.openSync(filePath, 'w'); for (let i = 0; i < totalChunks; i++) { diff --git a/remote/test-connect.js b/remote/test-connect.js new file mode 100644 index 0000000..01d879b --- /dev/null +++ b/remote/test-connect.js @@ -0,0 +1,22 @@ +const WebSocket = require('ws'); + +console.log('Testing connection to remote...'); +const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525', { + handshakeTimeout: 5000 +}); + +ws.on('open', () => { + console.log('Connected!'); + ws.close(); + process.exit(0); +}); + +ws.on('error', (err) => { + console.log('Error:', err.message); + process.exit(1); +}); + +setTimeout(() => { + console.log('Connection timeout'); + process.exit(1); +}, 8000); diff --git a/remote/test-file-api.js b/remote/test-file-api.js new file mode 100644 index 0000000..d21c41b --- /dev/null +++ b/remote/test-file-api.js @@ -0,0 +1,268 @@ +const http = require('http'); +const https = require('https'); + +const BASE_URL = 'http://localhost:3000'; +const PASSWORD = 'wzw20040525'; +const CHUNK_SIZE = 5 * 1024 * 1024; + +function request(options, body = null) { + return new Promise((resolve, reject) => { + const url = new URL(options.path, BASE_URL); + url.searchParams.set('password', PASSWORD); + + const client = url.protocol === 'https:' ? https : http; + + const reqOptions = { + hostname: url.hostname, + port: url.port, + path: url.pathname + url.search, + method: options.method, + headers: options.headers || {} + }; + + const req = client.request(reqOptions, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + resolve({ status: res.statusCode, data: JSON.parse(data) }); + } catch { + resolve({ status: res.statusCode, data }); + } + }); + }); + + req.on('error', reject); + + if (body) { + if (body instanceof FormData) { + req.write(body.getBuffer()); + } else if (Buffer.isBuffer(body)) { + req.write(body); + } else { + req.write(JSON.stringify(body)); + } + } + + req.end(); + }); +} + +async function testGetDrives() { + console.log('\n=== 测试1: 获取驱动器列表 ==='); + try { + const driveUrl = new URL('/api/files/browse', BASE_URL); + driveUrl.searchParams.set('allowSystem', 'true'); + driveUrl.searchParams.set('password', PASSWORD); + + const res = await request({ + method: 'GET', + path: driveUrl.pathname + driveUrl.search + }); + console.log('状态:', res.status); + console.log('currentPath:', res.data.currentPath); + console.log('parentPath:', res.data.parentPath); + + // 检查是否返回了驱动器(盘符如 C:, D:) + const drives = res.data.items?.filter(item => item.name.match(/^[A-Z]:$/i)); + if (drives && drives.length > 0) { + console.log('✓ 驱动器列表:', drives.map(d => d.name).join(', ')); + return true; + } + + // 如果返回的是目录列表而非驱动器,说明 allowSystem 未生效 + console.log('✗ 未获取到驱动器列表'); + console.log('返回的项目:', res.data.items?.slice(0, 5).map(i => i.name).join(', ')); + return false; + } catch (err) { + console.error('错误:', err.message); + return false; + } +} + +async function testUploadToSystemDir() { + console.log('\n=== 测试2: 上传到系统目录 ==='); + const testContent = 'Hello Remote System Directory Test ' + Date.now(); + const filename = 'D:\\xc_test_file.txt'; + + try { + // 1. 开始上传 + console.log('1. 开始上传...'); + const startUrl = new URL('/api/files/upload/start', BASE_URL); + startUrl.searchParams.set('password', PASSWORD); + + const startRes = await new Promise((resolve, reject) => { + const req = http.request({ + hostname: startUrl.hostname, + port: startUrl.port, + path: startUrl.pathname + startUrl.search, + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: JSON.parse(data) }); } + catch { resolve({ status: res.statusCode, data }); } + }); + }); + req.on('error', reject); + req.write(JSON.stringify({ + filename: filename, + totalChunks: 1, + fileSize: Buffer.byteLength(testContent) + })); + req.end(); + }); + console.log('开始上传响应:', startRes.data); + + if (!startRes.data.fileId) { + console.error('未获取到fileId'); + return false; + } + + const fileId = startRes.data.fileId; + + // 2. 上传分块 + console.log('2. 上传分块...'); + const chunk = Buffer.from(testContent); + + const chunkUrl = new URL('/api/files/upload/chunk', BASE_URL); + chunkUrl.searchParams.set('password', PASSWORD); + + const FormData = require('form-data'); + const form = new FormData(); + form.append('fileId', fileId); + form.append('chunkIndex', '0'); + form.append('chunk', chunk, { filename: 'chunk', contentType: 'application/octet-stream' }); + + const chunkRes = await new Promise((resolve, reject) => { + const req = http.request({ + hostname: chunkUrl.hostname, + port: chunkUrl.port, + path: chunkUrl.pathname + chunkUrl.search, + method: 'POST', + headers: form.getHeaders() + }, (res) => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: JSON.parse(data) }); } + catch { resolve({ status: res.statusCode, data }); } + }); + }); + req.on('error', reject); + req.write(form.getBuffer()); + req.end(); + }); + console.log('分块上传响应:', chunkRes.data); + + // 3. 合并文件 + console.log('3. 合并文件...'); + const mergeUrl = new URL('/api/files/upload/merge', BASE_URL); + mergeUrl.searchParams.set('password', PASSWORD); + + const mergeRes = await new Promise((resolve, reject) => { + const req = http.request({ + hostname: mergeUrl.hostname, + port: mergeUrl.port, + path: mergeUrl.pathname + mergeUrl.search, + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, (res) => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: JSON.parse(data) }); } + catch { resolve({ status: res.statusCode, data }); } + }); + }); + req.on('error', reject); + req.write(JSON.stringify({ + fileId: fileId, + totalChunks: 1, + filename: filename + })); + req.end(); + }); + console.log('合并响应:', mergeRes.data); + + return mergeRes.data.success === true; + } catch (err) { + console.error('错误:', err.message); + return false; + } +} + +async function testDownloadFromSystemDir() { + console.log('\n=== 测试3: 从系统目录下载 ==='); + const filename = 'D:\\xc_test_file.txt'; + + try { + const encodedPath = encodeURIComponent(filename); + const res = await request({ + method: 'GET', + path: `/api/files/${encodedPath}` + }); + console.log('状态:', res.status); + console.log('响应类型:', typeof res.data); + return res.status === 200; + } catch (err) { + console.error('错误:', err.message); + return false; + } +} + +async function testDeleteFile() { + console.log('\n=== 测试4: 删除测试文件 ==='); + const filename = 'D:\\xc_test_file.txt'; + + try { + const encodedPath = encodeURIComponent(filename); + const res = await request({ + method: 'DELETE', + path: `/api/files/${encodedPath}` + }); + console.log('状态:', res.status); + console.log('响应:', res.data); + return res.data.success === true || res.status === 200; + } catch (err) { + console.error('错误:', err.message); + return false; + } +} + +async function main() { + console.log('========================================'); + console.log('远程文件传输功能测试'); + console.log('目标服务器: 146.56.248.142:8080'); + console.log('========================================'); + + const results = []; + + // 测试1: 获取驱动器列表 + results.push({ name: '获取驱动器列表', pass: await testGetDrives() }); + + // 测试2: 上传到系统目录 + results.push({ name: '上传到系统目录', pass: await testUploadToSystemDir() }); + + // 测试3: 从系统目录下载 + results.push({ name: '从系统目录下载', pass: await testDownloadFromSystemDir() }); + + // 测试4: 删除测试文件 + results.push({ name: '删除测试文件', pass: await testDeleteFile() }); + + // 汇总结果 + console.log('\n========================================'); + console.log('测试结果汇总:'); + console.log('========================================'); + results.forEach(r => { + console.log(`${r.pass ? '✓' : '✗'} ${r.name}`); + }); + + const allPass = results.every(r => r.pass); + console.log(`\n总体结果: ${allPass ? '全部通过' : '存在失败'}`); + process.exit(allPass ? 0 : 1); +} + +main(); diff --git a/remote/test-full.js b/remote/test-full.js new file mode 100644 index 0000000..03a5383 --- /dev/null +++ b/remote/test-full.js @@ -0,0 +1,89 @@ +const WebSocket = require('ws'); +const fs = require('fs'); + +const ws = new WebSocket('ws://127.0.0.1:3003/ws?password=wzw20040525'); + +let fileId = 'upload_test_' + Date.now(); +const testContent = Buffer.from('Hello World Test File Content'); + +ws.on('open', () => { + console.log('=== Connected, starting upload test ==='); + + ws.send(JSON.stringify({ + type: 'fileUploadStart', + fileId: fileId, + filename: 'test.txt', + totalChunks: 1, + fileSize: testContent.length, + requestId: 'req1' + })); +}); + +ws.on('message', (data, isBinary) => { + if (isBinary) { + console.log('Binary data received'); + return; + } + + const msg = JSON.parse(data.toString()); + console.log('Received:', msg.type, msg.fileId || ''); + + if (msg.type === 'fileUploadStart') { + console.log('Session started, sending chunk...'); + ws.send(JSON.stringify({ + type: 'fileUploadChunk', + fileId: fileId, + chunkIndex: 0, + data: testContent.toString('base64'), + requestId: 'req2' + })); + } + + if (msg.type === 'fileUploadChunk') { + console.log('Chunk sent, sending merge...'); + ws.send(JSON.stringify({ + type: 'fileUploadMerge', + fileId: fileId, + filename: 'test.txt', + totalChunks: 1, + requestId: 'req3' + })); + } + + if (msg.type === 'fileUploadResult') { + console.log('=== Upload Result:', msg.success ? 'SUCCESS' : 'FAILED', msg.filename || msg.error); + + if (msg.success) { + console.log('\n=== Testing download ==='); + ws.send(JSON.stringify({ + type: 'fileDownloadStart', + filename: 'test.txt', + filePath: 'test.txt', + allowSystem: true, + requestId: 'req4' + })); + } else { + ws.close(); + process.exit(1); + } + } + + if (msg.type === 'fileDownloadStart') { + console.log('Download started, size:', msg.size); + } + + if (msg.type === 'fileDownloadChunk') { + console.log('Download chunk:', msg.chunkIndex, 'progress:', msg.progress + '%'); + } + + if (msg.type === 'fileDownloadComplete') { + console.log('=== Download Result:', msg.success ? 'SUCCESS' : 'FAILED'); + console.log('=== ALL TESTS PASSED ==='); + ws.close(); + process.exit(0); + } +}); + +ws.on('error', (err) => { console.error('Error:', err.message); }); + +setTimeout(() => { console.log('=== TIMEOUT ==='); process.exit(1); }, 20000); diff --git a/remote/test-ports.js b/remote/test-ports.js new file mode 100644 index 0000000..7b326f7 --- /dev/null +++ b/remote/test-ports.js @@ -0,0 +1,13 @@ +const WebSocket = require('ws'); + +console.log('Testing port 8080...'); +const ws1 = new WebSocket('ws://146.56.248.142:8080/ws?password=wzw20040525'); +ws1.on('open', () => console.log('8080: Connected')); +ws1.on('error', () => console.log('8080: Failed')); +ws1.on('close', () => { + console.log('Testing port 8083...'); + const ws2 = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525'); + ws2.on('open', () => console.log('8083: Connected')); + ws2.on('error', () => console.log('8083: Failed')); + setTimeout(() => process.exit(0), 3000); +}); diff --git a/remote/test-remote.js b/remote/test-remote.js new file mode 100644 index 0000000..6028805 --- /dev/null +++ b/remote/test-remote.js @@ -0,0 +1,71 @@ +const WebSocket = require('ws'); + +const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525'); + +let fileId = 'test_' + Date.now(); +const testContent = Buffer.from('Hello Remote Test'); + +ws.on('open', () => { + console.log('=== Connected to 146.56.248.142:8083 ==='); + ws.send(JSON.stringify({ + type: 'fileUploadStart', + fileId: fileId, + filename: 'test.txt', + totalChunks: 1, + fileSize: testContent.length, + requestId: 'req1' + })); +}); + +ws.on('message', (data, isBinary) => { + if (isBinary) return; + const msg = JSON.parse(data.toString()); + console.log('Received:', msg.type); + + if (msg.type === 'fileUploadStart') { + ws.send(JSON.stringify({ + type: 'fileUploadChunk', + fileId: fileId, + chunkIndex: 0, + data: testContent.toString('base64'), + requestId: 'req2' + })); + } + + if (msg.type === 'fileUploadChunk') { + ws.send(JSON.stringify({ + type: 'fileUploadMerge', + fileId: fileId, + filename: 'test.txt', + totalChunks: 1, + requestId: 'req3' + })); + } + + if (msg.type === 'fileUploadResult') { + console.log('=== Upload:', msg.success ? 'SUCCESS' : 'FAILED'); + if (msg.success) { + ws.send(JSON.stringify({ + type: 'fileDownloadStart', + filename: 'test.txt', + filePath: 'test.txt', + allowSystem: false, + requestId: 'req4' + })); + } else { + ws.close(); + process.exit(1); + } + } + + if (msg.type === 'fileDownloadComplete') { + console.log('=== Download:', msg.success ? 'SUCCESS' : 'FAILED'); + console.log('=== ALL TESTS PASSED ==='); + ws.close(); + process.exit(0); + } +}); + +ws.on('error', err => console.error('Error:', err.message)); + +setTimeout(() => { console.log('TIMEOUT'); process.exit(1); }, 15000); diff --git a/remote/test-simple.js b/remote/test-simple.js new file mode 100644 index 0000000..8ec9831 --- /dev/null +++ b/remote/test-simple.js @@ -0,0 +1,30 @@ +const WebSocket = require('ws'); + +console.log('Starting...'); +const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525'); + +ws.on('open', () => { + console.log('Connected!'); + + let fileId = 'test_' + Date.now(); + const testContent = Buffer.from('Hello'); + + console.log('Sending fileUploadStart...'); + ws.send(JSON.stringify({ + type: 'fileUploadStart', + fileId: fileId, + filename: 'F:/test.txt', + totalChunks: 1, + fileSize: 5, + requestId: 'req1' + })); +}); + +ws.on('message', (data, isBinary) => { + console.log('Got message:', isBinary ? 'binary' : data.toString().substring(0,80)); +}); + +ws.on('error', e => console.log('Error:', e.message)); +ws.on('close', () => console.log('Closed')); + +setTimeout(() => { console.log('Timeout'); process.exit(1); }, 10000); diff --git a/remote/test-upload.js b/remote/test-upload.js new file mode 100644 index 0000000..9c8b0f2 --- /dev/null +++ b/remote/test-upload.js @@ -0,0 +1,57 @@ +const WebSocket = require('ws'); + +console.log('Testing upload to remote 8083...'); +const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525'); + +let fileId = 'test_' + Date.now(); +const testContent = Buffer.from('Hello Test 中文'); + +ws.on('open', () => { + console.log('Connected, sending fileUploadStart with F:/xxx.txt...'); + ws.send(JSON.stringify({ + type: 'fileUploadStart', + fileId: fileId, + filename: 'F:/小问题.txt', + totalChunks: 1, + fileSize: testContent.length, + requestId: 'req1' + })); +}); + +ws.on('message', (data, isBinary) => { + if (isBinary) return; + const msg = JSON.parse(data.toString()); + console.log('Received:', msg.type); + + if (msg.type === 'fileUploadStart') { + console.log('Session started, sending chunk...'); + ws.send(JSON.stringify({ + type: 'fileUploadChunk', + fileId: fileId, + chunkIndex: 0, + data: testContent.toString('base64'), + requestId: 'req2' + })); + } + + if (msg.type === 'fileUploadChunk') { + console.log('Chunk sent, sending merge...'); + ws.send(JSON.stringify({ + type: 'fileUploadMerge', + fileId: fileId, + filename: 'F:/小问题.txt', + totalChunks: 1, + requestId: 'req3' + })); + } + + if (msg.type === 'fileUploadResult') { + console.log('=== Upload Result:', msg.success ? 'SUCCESS' : 'FAILED', msg.error || ''); + ws.close(); + process.exit(0); + } +}); + +ws.on('error', err => console.error('Error:', err.message)); + +setTimeout(() => { console.log('Timeout'); process.exit(1); }, 15000); diff --git a/remote/test-ws.js b/remote/test-ws.js new file mode 100644 index 0000000..93c21a1 --- /dev/null +++ b/remote/test-ws.js @@ -0,0 +1,32 @@ +const WebSocket = require('ws'); +const ws = new WebSocket('ws://127.0.0.1:3001/ws?password=wzw20040525'); + +ws.on('open', () => { + console.log('Connected, sending fileBrowse...'); + ws.send(JSON.stringify({ type: 'fileBrowse', path: 'C:\\', allowSystem: true, requestId: 'test1' })); +}); + +ws.on('message', (data) => { + console.log('Received raw:', data.toString().substring(0, 200)); + + if (Buffer.isBuffer(data) || data instanceof ArrayBuffer) { + console.log('Binary data'); + return; + } + + try { + const msg = JSON.parse(data); + console.log('Received:', msg.type); + if (msg.type === 'fileBrowseResult') { + console.log('Items:', msg.items?.slice(0,3)); + ws.close(); + process.exit(0); + } + } catch(e) { + console.log('Parse error:', e.message); + } +}); + +ws.on('error', (err) => { console.error('Error:', err.message); }); + +setTimeout(() => { console.log('Timeout'); process.exit(1); }, 10000); diff --git a/src/modules/remote/ws.ts b/src/modules/remote/ws.ts new file mode 100644 index 0000000..6bed2cd --- /dev/null +++ b/src/modules/remote/ws.ts @@ -0,0 +1,190 @@ +type MessageHandler = (data: any) => void + +interface PendingRequest { + resolve: (value: any) => void + reject: (reason?: any) => void + timeout: NodeJS.Timeout +} + +class RemoteWebSocket { + private ws: WebSocket | null = null + private url: string = '' + private password: string = '' + private pendingRequests: Map = new Map() + private messageHandlers: Map = new Map() + private reconnectTimer: NodeJS.Timeout | null = null + private isManualClose: boolean = false + + connect(serverHost: string, port: number, password?: string): Promise { + this.url = `ws://${serverHost}:${port}/ws` + this.password = password || '' + this.isManualClose = false + + return new Promise((resolve, reject) => { + try { + const wsUrl = this.password + ? `${this.url}?password=${encodeURIComponent(this.password)}` + : this.url + + this.ws = new WebSocket(wsUrl) + + this.ws.onopen = () => { + console.log('Remote WebSocket connected') + resolve() + } + + this.ws.onclose = () => { + console.log('Remote WebSocket disconnected') + if (!this.isManualClose) { + this.scheduleReconnect() + } + } + + this.ws.onerror = (error) => { + console.error('Remote WebSocket error:', error) + reject(error) + } + + this.ws.onmessage = (event) => { + try { + if (event.data instanceof Blob || event.data instanceof ArrayBuffer) { + return + } + this.handleMessage(JSON.parse(event.data)) + } catch (e) { + console.error('WebSocket message parse error:', e, event.data) + } + } + } catch (error) { + reject(error) + } + }) + } + + private scheduleReconnect() { + if (this.reconnectTimer) return + + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null + if (!this.isManualClose && this.url) { + console.log('Attempting to reconnect Remote WebSocket...') + this.connect( + this.url.replace('ws://', '').split(':')[0], + parseInt(this.url.replace('ws://', '').split(':')[1] || '80'), + this.password + ).catch(() => {}) + } + }, 3000) + } + + disconnect() { + this.isManualClose = true + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer) + this.reconnectTimer = null + } + if (this.ws) { + this.ws.close() + this.ws = null + } + } + + send(message: any): Promise { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + reject(new Error('WebSocket not connected')) + return + } + + const requestId = message.type + '_' + Date.now() + '_' + Math.random() + message.requestId = requestId + + const timeout = setTimeout(() => { + this.pendingRequests.delete(requestId) + reject(new Error('Request timeout')) + }, 30000) + + this.pendingRequests.set(requestId, { resolve, reject, timeout }) + + this.ws.send(JSON.stringify(message)) + }) + } + + on(type: string, handler: MessageHandler) { + if (!this.messageHandlers.has(type)) { + this.messageHandlers.set(type, []) + } + this.messageHandlers.get(type)!.push(handler) + } + + off(type: string, handler: MessageHandler) { + const handlers = this.messageHandlers.get(type) + if (handlers) { + const index = handlers.indexOf(handler) + if (index > -1) { + handlers.splice(index, 1) + } + } + } + + private handleMessage(data: any) { + const requestId = data.requestId + + if (requestId && this.pendingRequests.has(requestId)) { + const { resolve, timeout } = this.pendingRequests.get(requestId)! + clearTimeout(timeout) + this.pendingRequests.delete(requestId) + resolve(data) + return + } + + const handlers = this.messageHandlers.get(data.type) + if (handlers) { + handlers.forEach(handler => handler(data)) + } + } + + isConnected(): boolean { + return this.ws !== null && this.ws.readyState === WebSocket.OPEN + } +} + +let instance: RemoteWebSocket | null = null +let lastServerHost: string | null = null +let lastPort: number | null = null + +export const getRemoteWebSocket = ( + serverHost?: string, + port?: number, + password?: string +): RemoteWebSocket => { + if (!instance) { + instance = new RemoteWebSocket() + } + + if (serverHost && port) { + const serverChanged = serverHost !== lastServerHost || port !== lastPort + if (serverChanged) { + instance.disconnect() + lastServerHost = serverHost + lastPort = port + instance.connect(serverHost, port, password).catch(console.error) + } else if (!instance.isConnected()) { + instance.connect(serverHost, port, password).catch(console.error) + } + } + + return instance +} + +export const createRemoteWebSocket = ( + serverHost: string, + port: number, + password?: string +): RemoteWebSocket => { + const ws = new RemoteWebSocket() + ws.connect(serverHost, port, password).catch(console.error) + return ws +} + +export { RemoteWebSocket }