| 1 | import { ipcMain, shell } from 'electron' |
| 2 | import fs from 'fs' |
| 3 | import path from 'path' |
| 4 | import { pathToFileURL } from 'url' |
| 5 | import type { IpcContext } from '../ipc/context' |
| 6 | import { ensureSessionRuntimeCompatible } from '../session/runtime-assets' |
| 7 | |
| 8 | export function registerFileHandlers(ctx: IpcContext): void { |
| 9 | const { parsePathPayload, normalizeSessionId, assertPathInAllowedRoots } = ctx |
| 10 | |
| 11 | ipcMain.handle('file:open', async (_event, payload, legacySessionId?: string) => { |
| 12 | const parsed = parsePathPayload(payload, 'path') |
| 13 | const sessionId = parsed.sessionId ?? normalizeSessionId(legacySessionId) |
| 14 | const safePath = await assertPathInAllowedRoots({ |
| 15 | filePath: parsed.filePath, |
| 16 | mode: 'read', |
| 17 | sessionId |
| 18 | }) |
| 19 | return fs.promises.readFile(safePath, 'utf-8') |
| 20 | }) |
| 21 | |
| 22 | ipcMain.handle('file:reveal', async (_event, payload, legacySessionId?: string) => { |
| 23 | const parsed = parsePathPayload(payload, 'path') |
| 24 | const sessionId = parsed.sessionId ?? normalizeSessionId(legacySessionId) |
| 25 | const safePath = await assertPathInAllowedRoots({ |
| 26 | filePath: parsed.filePath, |
| 27 | mode: 'read', |
| 28 | sessionId |
| 29 | }) |
| 30 | shell.showItemInFolder(safePath) |
| 31 | return { success: true } |
| 32 | }) |
| 33 | |
| 34 | ipcMain.handle( |
| 35 | 'file:openInBrowser', |
| 36 | async (_event, payloadOrPath, legacyHash?: string, legacySessionId?: string) => { |
| 37 | const parsed = parsePathPayload(payloadOrPath, 'path') |
| 38 | const sessionId = parsed.sessionId ?? normalizeSessionId(legacySessionId) |
| 39 | const hashRaw = typeof legacyHash === 'string' ? legacyHash : parsed.hash |
| 40 | const safePath = await assertPathInAllowedRoots({ |
| 41 | filePath: parsed.filePath, |
| 42 | mode: 'read', |
| 43 | sessionId, |
| 44 | htmlOnly: true |
| 45 | }) |
| 46 | |
| 47 | // Ensure index-runtime.js is up-to-date before opening in browser |
| 48 | try { |
| 49 | const projectDir = path.dirname(safePath) |
| 50 | await ensureSessionRuntimeCompatible(ctx, projectDir) |
| 51 | } catch { |
| 52 | // ignore |
| 53 | } |
| 54 | |
| 55 | const baseUrl = pathToFileURL(safePath).toString() |
| 56 | const hashValue = |
| 57 | typeof hashRaw === 'string' && hashRaw.trim().length > 0 |
| 58 | ? hashRaw.startsWith('#') |
| 59 | ? hashRaw |
| 60 | : `#${hashRaw}` |
| 61 | : '' |
| 62 | await shell.openExternal(`${baseUrl}${hashValue}`) |
| 63 | return { success: true } |
| 64 | } |
| 65 | ) |
| 66 | |
| 67 | ipcMain.handle('file:save', async (_event, payload) => { |
| 68 | if (!payload || typeof payload !== 'object') { |
| 69 | throw new Error('file:save 参数无效') |
| 70 | } |
| 71 | const record = payload as { path?: unknown; content?: unknown; sessionId?: unknown } |
| 72 | const filePath = typeof record.path === 'string' ? record.path : '' |
| 73 | const content = typeof record.content === 'string' ? record.content : '' |
| 74 | const sessionId = normalizeSessionId(record.sessionId) |
| 75 | const safePath = await assertPathInAllowedRoots({ |
| 76 | filePath, |
| 77 | mode: 'write', |
| 78 | sessionId |
| 79 | }) |
| 80 | await fs.promises.writeFile(safePath, content, 'utf-8') |
| 81 | return { success: true } |
| 82 | }) |
| 83 | } |
| 84 |