// ============================================================ // canvas/index.ts — 画布 IPC 处理器注册 // // 职责: // - 注册所有画布相关的 IPC 处理器 // - 组合使用各工具模块实现业务逻辑 // ============================================================ import { ipcMain } from 'electron'; import fs from 'node:fs'; import path from 'node:path'; import sharp from 'sharp'; import { BIDIRECTIONAL_CANVAS } from '../../../../shared/constants/ipc/canvas'; import { logger } from '../../logger'; import { walkDirectory } from './walk'; import { readSidecarFile, writeSidecarFile } from './sidecar'; import { generateCanvasThumbnail } from './thumbnail'; import { buildScanRoots, getStageExtensions } from './scan-roots'; import type { StageType, AssetTypeFilter } from './types'; /** 注册画布 IPC 处理器 */ export function registerCanvasIpcHandlers(): void { // ---- 扫描项目(主进程内部 buildScanRoots + walkDirectory) ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_SCAN_PROJECT, async ( _event, params: { projectPath: string; stage: StageType; assetTypeFilter: AssetTypeFilter; shotQuery: string; }, ) => { try { logger.info('canvas', '收到 CANVAS_SCAN_PROJECT', params); const roots = buildScanRoots(params); const extensions = getStageExtensions(params.stage); logger.info('canvas', `buildScanRoots 结果:${roots.length} 个扫描根`, { roots: roots.length <= 10 ? roots : roots.slice(0, 10).concat(`... 共 ${roots.length} 个`), extensions, stage: params.stage, }); if (roots.length === 0) { logger.warn('canvas', '扫描根目录为空', undefined, { projectPath: params.projectPath, stage: params.stage, shotQuery: params.shotQuery || '(无)', }); return { success: true, data: [] }; } const allResults: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string; }> = []; for (const root of roots) { const results = walkDirectory(root, extensions, true); if (results.length > 0) { logger.info('canvas', ` 📁 ${root} → ${results.length} 个文件`); } allResults.push(...results); } logger.info( 'canvas', `扫描项目完成:${allResults.length} 个文件(${roots.length} 个扫描根目录,阶段:${params.stage})`, ); return { success: true, data: allResults }; } catch (err) { const msg = (err as Error).message; logger.warn('canvas', '项目扫描失败', err instanceof Error ? err : undefined); return { success: false, error: msg }; } }, ); // ---- 目录扫描(保留低级 API:外部提供扫描根) ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_SCAN_DIRECTORY, async ( _event, params: { roots: string[]; extensions: string[]; recursive: boolean }, ) => { try { const allResults: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string; }> = []; for (const root of params.roots) { if (!fs.existsSync(root)) { logger.warn('canvas', `扫描根目录不存在,跳过:${root}`); continue; } const results = walkDirectory(root, params.extensions, params.recursive); allResults.push(...results); } logger.info( 'canvas', `扫描完成:${allResults.length} 个文件(${params.roots.length} 个根目录)`, ); return { success: true, data: allResults }; } catch (err) { const msg = (err as Error).message; logger.warn('canvas', '目录扫描失败', err instanceof Error ? err : undefined); return { success: false, error: msg }; } }, ); // ---- 读取 sidecar ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_READ_SIDECAR, async (_event, params: { filePath: string }) => { try { const data = readSidecarFile(params.filePath); return { success: true, data }; } catch (err) { logger.warn('canvas', '读取 sidecar 失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 写入 sidecar ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_WRITE_SIDECAR, async (_event, params: { filePath: string; metadata: object }) => { try { writeSidecarFile(params.filePath, params.metadata); return { success: true }; } catch (err) { logger.warn('canvas', '写入 sidecar 失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 生成缩略图 ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_GENERATE_THUMBNAIL, async (_event, params: { filePath: string; size: number }) => { try { const result = await generateCanvasThumbnail(params.filePath, params.size); return { success: true, data: result }; } catch (err) { logger.warn('canvas', '生成缩略图失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 复制文件 ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_COPY_FILE, async (_event, params: { src: string; destDir: string; fileName: string }) => { try { if (!fs.existsSync(params.destDir)) { fs.mkdirSync(params.destDir, { recursive: true }); } const dest = path.join(params.destDir, params.fileName); fs.copyFileSync(params.src, dest); return { success: true, data: dest }; } catch (err) { logger.warn('canvas', '复制文件失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 获取图片尺寸(使用 sharp) ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_GET_IMAGE_SIZE, async (_event, params: { filePath: string }) => { try { if (sharp) { const metadata = await sharp(params.filePath).metadata(); return { success: true, data: { width: metadata.width || 0, height: metadata.height || 0, }, }; } return { success: false, error: 'sharp 未安装,无法获取图片尺寸' }; } catch (err) { logger.warn('canvas', '获取图片尺寸失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 列出子文件夹(非递归,排除隐藏) ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_LIST_SUBDIRS, async (_event, params: { parentPath: string }) => { try { if (!fs.existsSync(params.parentPath)) { logger.info('canvas', `listSubdirs: 路径不存在,返回空数组 — ${params.parentPath}`); return { success: true, data: [] }; } const entries = fs.readdirSync(params.parentPath, { withFileTypes: true }); const dirs = entries .filter((e) => e.isDirectory() && !e.name.startsWith('.')) .map((e) => e.name); logger.info('canvas', `listSubdirs: ${dirs.length} 个子文件夹 — ${params.parentPath}`); return { success: true, data: dirs }; } catch (err) { logger.warn('canvas', 'listSubdirs 失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 扫描阶段文件 ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_SCAN_STAGE_FILES, async ( _event, params: { projectPath: string; episode: string; shot: string; stage: string; }, ) => { try { const stageDir = path.join( params.projectPath, 'shots', params.episode, params.shot, params.stage, ); logger.info('canvas', `scanStageFiles: ${stageDir}`); if (!fs.existsSync(stageDir)) { logger.info('canvas', `scanStageFiles: 阶段目录不存在 — ${stageDir}`); return { success: true, data: [] }; } const extensions = getStageExtensions(params.stage as StageType); const results = walkDirectory(stageDir, extensions, true); logger.info('canvas', `scanStageFiles: ${results.length} 个文件 — ${stageDir}`); return { success: true, data: results }; } catch (err) { logger.warn('canvas', 'scanStageFiles 失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 扫描资产文件(深度递归) ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_SCAN_ASSET_FILES, async ( _event, params: { projectPath: string; assetTypeFilter: string }, ) => { try { let assetsDir: string; switch (params.assetTypeFilter) { case '角色': assetsDir = path.join(params.projectPath, 'assets', 'chr'); break; case '道具': assetsDir = path.join(params.projectPath, 'assets', 'prp'); break; case '场景': assetsDir = path.join(params.projectPath, 'assets', 'set'); break; default: assetsDir = path.join(params.projectPath, 'assets'); } logger.info('canvas', `scanAssetFiles: ${assetsDir} (filter=${params.assetTypeFilter})`); if (!fs.existsSync(assetsDir)) { logger.info('canvas', `scanAssetFiles: 资产目录不存在 — ${assetsDir}`); return { success: true, data: [] }; } // 资产深度递归:图片 + 视频 const extensions = ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'mp4', 'webm']; const results = walkDirectory(assetsDir, extensions, true); logger.info('canvas', `scanAssetFiles: ${results.length} 个文件 — ${assetsDir}`); return { success: true, data: results }; } catch (err) { logger.warn('canvas', 'scanAssetFiles 失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 创建目录 ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_CREATE_DIRECTORY, async (_event, params: { parentPath: string; dirName: string }) => { try { // 安全校验:拒绝包含危险字符的名称 const dangerous = /[/\\:*?"<>|]/; if (!params.dirName || dangerous.test(params.dirName)) { return { success: false, error: `无效的目录名: "${params.dirName}"`, }; } const fullPath = path.join(params.parentPath, params.dirName); if (fs.existsSync(fullPath)) { return { success: false, error: `目录已存在: ${fullPath}` }; } fs.mkdirSync(fullPath); logger.info('canvas', `createDirectory: ${fullPath}`); return { success: true, data: fullPath }; } catch (err) { logger.warn('canvas', 'createDirectory 失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }, ); // ---- 启动原生文件拖拽(拖放到 Photoshop/Explorer 等外部应用) ---- ipcMain.handle( BIDIRECTIONAL_CANVAS.CANVAS_START_DRAG, async (_event, params: { filePath: string; fileName: string }) => { try { if (!fs.existsSync(params.filePath)) { logger.warn('canvas', `startDrag: 文件不存在 — ${params.filePath}`); return { success: false, error: '文件不存在' }; } _event.sender.startDrag({ file: params.filePath, icon: params.filePath, }); logger.info('canvas', `startDrag: 已启动 — ${params.fileName}`); return { success: true }; } catch (err) { const msg = (err as Error).message; logger.warn('canvas', 'startDrag 失败', err instanceof Error ? err : undefined); return { success: false, error: msg }; } }, ); logger.info('canvas', '画布 IPC 处理器已注册(完整实现)'); }