Files
ele-HeiXiu/electron/main/ipc/canvas/index.ts
YoungestSongMo 136a17c0bf feat: 任务列表右键菜单回调 + 主进程下载 + sharp 缩略图 + IPC 精简重构
- 任务右键菜单:实现打开输出目录(inputs/outputs/.cache 结构)、
    复制资源链接、重新编辑、再次生成、拉取结果、重新下载、收藏、删除记录
  - 主进程下载:通过 Electron net 模块绕过 CORS,支持 CDN 媒体文件下载
  - sharp 缩略图:替换 nativeImage,支持 4K/8K+ 大尺寸素材的流式处理
  - IPC 精简:40+ 冗余通道 → 15 个分组通道,移除未使用的 preload 桥接方法
  - 任务存储:新增收藏字段、schema 迁移、task-store 持久化
  - 清理废弃模块:canvas-ipc / updater / request / pricing

refactor: IPC/Preload/Services 模块化拆分 — 单一职责 + 向后兼容

  主要变更:
  - shared/constants/ipc-channels.ts → ipc/ 模块 (base/storage/canvas/updater)
  - electron/preload.ts → preload/ 模块 (base/safe-storage/file-storage/canvas)
  - electron/main/ipc/canvas-ipc.ts → canvas/ 模块 (types/walk/sidecar/mime/thumbnail)
  - electron/main/ipc/storage-ipc.ts → 提取 utils/ (path/download/thumbnail)
  - electron/main/updater.ts → updater/ 模块 (provider/platform-updaters)
  - src/services/request.ts → request/ 模块 (token/interceptors/methods)
  - src/shared/utils/pricing.ts → pricing/ 模块 (types/helpers/format/calculate/fields)
  - src/modules/home/center/views/ModelInputForm.tsx → 提取 utils + DependentFormItem

  架构优化:
  - 所有拆分均保持向后兼容(旧导入路径仍可用)
  - TypeScript 编译通过 ✓
  - 每个模块单一职责,平均 ~130 行
2026-07-03 11:13:36 +08:00

353 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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 处理器已注册(完整实现)');
}