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 行
This commit is contained in:
2026-07-03 11:13:36 +08:00
parent 904edecd90
commit 136a17c0bf
69 changed files with 4139 additions and 2713 deletions

View File

@@ -0,0 +1,22 @@
// ============================================================
// preload/app-runtime.ts — 应用运行时 API
//
// 提供窗口标题、版本控制等运行时接口
// 渲染进程通过 window.appRuntime 调用
// ============================================================
import { ipcRenderer, contextBridge } from 'electron';
import { RENDERER_TO_MAIN } from '../../shared/constants/ipc';
/** 应用运行时 API */
export function exposeAppRuntime(): void {
contextBridge.exposeInMainWorld('appRuntime', {
/**
* 登录/登出后更新窗口标题中的版控后缀
* @param edition - 'personal' | 'enterprise' | nullnull 清除后缀)
*/
setWindowEdition(edition: string | null): void {
ipcRenderer.send(RENDERER_TO_MAIN.SET_WINDOW_TITLE, edition);
},
});
}

29
electron/preload/base.ts Normal file
View File

@@ -0,0 +1,29 @@
// ============================================================
// preload/base.ts — 基础 ipcRenderer 桥接
//
// 提供安全的 IPC 通信接口,暴露给渲染进程
// ============================================================
import { ipcRenderer, contextBridge } from 'electron';
/** 基础 IPC 通信 API */
export function exposeBaseIpc(): void {
contextBridge.exposeInMainWorld('ipcRenderer', {
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args;
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args));
},
off(...args: Parameters<typeof ipcRenderer.off>) {
const [channel, ...omit] = args;
return ipcRenderer.off(channel, ...omit);
},
send(...args: Parameters<typeof ipcRenderer.send>) {
const [channel, ...omit] = args;
return ipcRenderer.send(channel, ...omit);
},
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
const [channel, ...omit] = args;
return ipcRenderer.invoke(channel, ...omit);
},
});
}

124
electron/preload/canvas.ts Normal file
View File

@@ -0,0 +1,124 @@
// ============================================================
// preload/canvas.ts — 无限画布 API
//
// 提供画布窗口、文件扫描、缩略图、拖拽等接口
// 渲染进程通过 window.canvas 调用
// ============================================================
import { ipcRenderer, contextBridge } from 'electron';
import { BIDIRECTIONAL, RENDERER_TO_MAIN } from '../../shared/constants/ipc';
/** 无限画布 API */
export function exposeCanvas(): void {
contextBridge.exposeInMainWorld('canvas', {
/** 打开画布窗口(主进程创建新 BrowserWindow */
openWindow(projectPath?: string): void {
ipcRenderer.send(RENDERER_TO_MAIN.OPEN_CANVAS_WINDOW, { projectPath });
},
/** 扫描目录,返回匹配的文件列表 */
scanDirectory(params: {
roots: string[];
extensions: string[];
recursive: boolean;
}): Promise<{
success: boolean;
data: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>;
error?: string;
}> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_SCAN_DIRECTORY, params);
},
/** 扫描项目(主进程内部完成 buildScanRoots + walkDirectory推荐使用 */
scanProject(params: {
projectPath: string;
stage: string;
assetTypeFilter: string;
shotQuery: string;
}): Promise<{
success: boolean;
data: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>;
error?: string;
}> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_SCAN_PROJECT, params);
},
/** 读取旁加载 JSON 元数据 */
readSidecar(params: { filePath: string }): Promise<{ success: boolean; data: object | null }> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_READ_SIDECAR, params);
},
/** 写入旁加载 JSON 元数据 */
writeSidecar(params: { filePath: string; metadata: object }): Promise<{ success: boolean; error?: string }> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_WRITE_SIDECAR, params);
},
/** 生成缩略图JPEG base64 */
generateThumbnail(params: { filePath: string; size: number }): Promise<{
success: boolean;
data: { data: string; mimeType: string; width: number; height: number };
error?: string;
}> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_GENERATE_THUMBNAIL, params);
},
/** 复制文件到目标目录(导入操作) */
copyFile(params: { src: string; destDir: string; fileName: string }): Promise<{
success: boolean;
data: string;
error?: string;
}> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_COPY_FILE, params);
},
/** 列出目录下的子文件夹(非递归,排除隐藏) */
listSubdirs(params: { parentPath: string }): Promise<{ success: boolean; data: string[]; error?: string }> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_LIST_SUBDIRS, params);
},
/** 扫描阶段文件(单个 episode/shot/stage 组合) */
scanStageFiles(params: { projectPath: string; episode: string; shot: string; stage: string }): Promise<{
success: boolean;
data: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>;
error?: string;
}> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_SCAN_STAGE_FILES, params);
},
/** 扫描资产文件(深度递归扫描 assets/ 目录) */
scanAssetFiles(params: { projectPath: string; assetTypeFilter: string }): Promise<{
success: boolean;
data: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>;
error?: string;
}> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_SCAN_ASSET_FILES, params);
},
/** 创建目录 */
createDirectory(params: { parentPath: string; dirName: string }): Promise<{
success: boolean;
data: string;
error?: string;
}> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_CREATE_DIRECTORY, params);
},
/** 将本地文件路径转换为可通过 local-media:// 协议加载的 URL */
getMediaUrl(filePath: string): string {
const normalized = filePath.replace(/\\/g, '/');
// UNC 路径(//server/share/…)开头 // 会被 URL 解析器视为 authority
// 必须用 local-media://<host>/<path> 格式而非 local-media:/// 前缀
if (normalized.startsWith('//')) {
// normalized = "//192.168.110.250/share/path"
return 'local-media://' + normalized.slice(2);
}
// 本地路径C:/path → local-media:///C:/path
return 'local-media:///' + normalized;
},
/** 启动原生文件拖拽(从缩略图拖放媒体文件到外部应用) */
startDrag(params: { filePath: string; fileName: string }): Promise<{ success: boolean; error?: string }> {
return ipcRenderer.invoke(BIDIRECTIONAL.CANVAS_START_DRAG, params);
},
});
}

View File

@@ -0,0 +1,94 @@
// ============================================================
// preload/file-storage.ts — 本地存储文件 API主进程 fs 桥接)
//
// 提供文件读写、目录管理、下载等接口
// 渲染进程通过 window.fileStorage 调用
// ============================================================
import { ipcRenderer, contextBridge } from 'electron';
import { BIDIRECTIONAL } from '../../shared/constants/ipc';
/** 本地存储文件 API */
export function exposeFileStorage(): void {
contextBridge.exposeInMainWorld('fileStorage', {
/** 获取 heixiu-data 根目录路径 */
async getDataDir(): Promise<string | null> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.GET_DATA_DIR);
return r?.success ? r.data : null;
},
/** 写入文件Base64 → 磁盘),返回写入的绝对路径 */
async writeFile(relativePath: string, data: string, encoding?: 'base64' | 'utf8'): Promise<string | null> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.FILE_WRITE, { relativePath, data, encoding });
return r?.success ? r.data : null;
},
/** 读取文件(磁盘 → Base64 */
async readFile(relativePath: string): Promise<string | null> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.FILE_READ, { relativePath });
return r?.success ? r.data : null;
},
/** 删除文件 */
async deleteFile(relativePath: string): Promise<boolean> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.FILE_DELETE, { relativePath });
return r?.success ?? false;
},
/** 递归创建目录 */
async makeDir(relativePath: string): Promise<string | null> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.MAKE_DIR, { relativePath });
return r?.success ? r.data : null;
},
/** 在文件管理器中显示文件(绝对路径) */
async showItemInFolder(filePath: string): Promise<boolean> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.SHOW_ITEM_IN_FOLDER, { filePath });
return r?.success ?? false;
},
/** 获取文件大小(字节) */
async fileSize(relativePath: string): Promise<number | null> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.FILE_SIZE, { relativePath });
return r?.success ? r.data : null;
},
/** 检查文件/目录是否存在 */
async fileExists(relativePath: string): Promise<boolean> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.FILE_EXISTS, { relativePath });
return r?.success ? r.data : false;
},
/** 绝对路径文件写入(绕过 heixiu-data 沙箱) */
async writeFileAbsolute(absolutePath: string, data: string, encoding?: 'base64' | 'utf8'): Promise<string | null> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.FILE_WRITE_ABSOLUTE, { absolutePath, data, encoding });
return r?.success ? r.data : null;
},
/** 绝对路径目录创建(绕过 heixiu-data 沙箱) */
async makeDirAbsolute(absolutePath: string): Promise<string | null> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.MAKE_DIR_ABSOLUTE, { absolutePath });
return r?.success ? r.data : null;
},
/** 主进程下载文件到绝对路径(绕过浏览器 CORS 限制),失败时 reject 并携带错误信息 */
async downloadToPath(url: string, destPath: string, headers?: Record<string, string>): Promise<string> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.DOWNLOAD_TO_PATH, { url, destPath, headers });
if (r?.success) return r.data;
throw new Error(r?.error || '主进程下载失败');
},
/** 获取系统下载目录路径 */
async getDownloadsPath(): Promise<string | null> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.GET_DOWNLOADS_PATH);
return r?.success ? r.data : null;
},
/** 生成图片缩略图(读取原图 → 缩放 → JPEG 写入目标路径) */
async generateThumbnail(srcPath: string, destPath: string, maxWidth?: number, quality?: number): Promise<string> {
const r = await ipcRenderer.invoke(BIDIRECTIONAL.GENERATE_THUMBNAIL, { srcPath, destPath, maxWidth, quality });
if (r?.success) return r.data;
throw new Error(r?.error || '缩略图生成失败');
},
});
}

11
electron/preload/index.ts Normal file
View File

@@ -0,0 +1,11 @@
// ============================================================
// preload/index.ts — 汇总导出所有 preload 模块
//
// 在主 preload.ts 中调用各模块的 expose 函数
// ============================================================
export { exposeBaseIpc } from './base';
export { exposeSafeStorage } from './safe-storage';
export { exposeFileStorage } from './file-storage';
export { exposeAppRuntime } from './app-runtime';
export { exposeCanvas } from './canvas';

View File

@@ -0,0 +1,33 @@
// ============================================================
// preload/safe-storage.ts — 安全加密 API基于 Electron safeStorage
//
// 提供加密/解密接口,渲染进程通过 window.safeStorage 调用
// ============================================================
import { ipcRenderer, contextBridge } from 'electron';
import { BIDIRECTIONAL } from '../../shared/constants/ipc';
/** 安全加密 API */
export function exposeSafeStorage(): void {
contextBridge.exposeInMainWorld('safeStorage', {
/**
* 加密明文字符串
* @param plaintext - 待加密的明文
* @returns Base64 编码的密文,失败返回 null
*/
async encrypt(plaintext: string): Promise<string | null> {
const result = await ipcRenderer.invoke(BIDIRECTIONAL.SAFESTORAGE_ENCRYPT, plaintext);
return result?.success ? result.data : null;
},
/**
* 解密 Base64 密文
* @param encryptedBase64 - Base64 编码的密文
* @returns 解密后的明文,失败返回 null
*/
async decrypt(encryptedBase64: string): Promise<string | null> {
const result = await ipcRenderer.invoke(BIDIRECTIONAL.SAFESTORAGE_DECRYPT, encryptedBase64);
return result?.success ? result.data : null;
},
});
}