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:
@@ -16,8 +16,9 @@
|
||||
import { ipcMain, shell, app } from 'electron';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { BIDIRECTIONAL } from '../../../shared/constants/ipc-channels';
|
||||
import { BIDIRECTIONAL_STORAGE } from '../../../shared/constants/ipc/storage';
|
||||
import { logger } from '../logger';
|
||||
import { resolveSafe, ensureDir, downloadToFile, generateThumbnail } from './utils';
|
||||
|
||||
// ---------- 内部工具 ----------
|
||||
|
||||
@@ -25,145 +26,200 @@ let dataDirResolver: (() => string) | null = null;
|
||||
|
||||
/** 注入自定义数据目录解析器(在 registerStorageIpcHandlers() 之前调用) */
|
||||
export function setDataDirResolver(resolver: () => string): void {
|
||||
dataDirResolver = resolver;
|
||||
dataDirResolver = resolver;
|
||||
}
|
||||
|
||||
/** 获取 heixiu-data 根目录 */
|
||||
function getDataDir(): string {
|
||||
return dataDirResolver ? dataDirResolver() : path.join(app.getPath('userData'), 'heixiu-data');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将相对路径解析为绝对路径,并校验是否在 dataDir 内。
|
||||
* 防止路径穿越攻击。
|
||||
*/
|
||||
function resolveSafe(dataDir: string, relativePath: string): string {
|
||||
const resolved = path.resolve(dataDir, relativePath);
|
||||
if (!resolved.startsWith(dataDir + path.sep) && resolved !== dataDir) {
|
||||
throw new Error(`路径越界:${relativePath}`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/** 确保目录存在(递归创建) */
|
||||
function ensureDir(dirPath: string): void {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
return dataDirResolver ? dataDirResolver() : path.join(app.getPath('userData'), 'heixiu-data');
|
||||
}
|
||||
|
||||
// ---------- 注册 IPC 处理器 ----------
|
||||
|
||||
export function registerStorageIpcHandlers(): void {
|
||||
const dataDir = getDataDir();
|
||||
ensureDir(dataDir);
|
||||
logger.info('app', `本地存储目录:${dataDir}`);
|
||||
const dataDir = getDataDir();
|
||||
ensureDir(dataDir);
|
||||
logger.info('app', `本地存储目录:${dataDir}`);
|
||||
|
||||
// ---- 获取数据目录 ----
|
||||
ipcMain.handle(BIDIRECTIONAL.GET_DATA_DIR, () => {
|
||||
return { success: true, data: dataDir };
|
||||
});
|
||||
// ---- 获取数据目录 ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.GET_DATA_DIR, () => {
|
||||
return { success: true, data: dataDir };
|
||||
});
|
||||
|
||||
// ---- 文件写入 ----
|
||||
ipcMain.handle(
|
||||
BIDIRECTIONAL.FILE_WRITE,
|
||||
async (
|
||||
_event,
|
||||
params: { relativePath: string; data: string; encoding?: 'base64' | 'utf8' },
|
||||
) => {
|
||||
// ---- 文件写入 ----
|
||||
ipcMain.handle(
|
||||
BIDIRECTIONAL_STORAGE.FILE_WRITE,
|
||||
async (_event, params: { relativePath: string; data: string; encoding?: 'base64' | 'utf8' }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
ensureDir(path.dirname(filePath));
|
||||
|
||||
const buffer = Buffer.from(params.data, params.encoding || 'base64');
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
logger.info('storage', `文件写入成功:${filePath}(${buffer.length} bytes)`);
|
||||
return { success: true, data: filePath };
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
logger.warn('storage', `文件写入失败:${params.relativePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ---- 文件读取(返回 Base64) ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.FILE_READ, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
ensureDir(path.dirname(filePath));
|
||||
|
||||
const buffer = Buffer.from(params.data, params.encoding || 'base64');
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
console.log(`[storage IPC] 文件写入成功:${filePath}(${buffer.length} bytes)`);
|
||||
return { success: true, data: filePath };
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { success: false, error: '文件不存在' };
|
||||
}
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return { success: true, data: buffer.toString('base64') };
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
console.error(`[storage IPC] 文件写入失败:${params.relativePath} — ${msg}`);
|
||||
logger.warn('storage', `文件写入失败:${params.relativePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: msg };
|
||||
logger.warn('storage', `文件读取失败:${params.relativePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ---- 文件读取(返回 Base64) ----
|
||||
ipcMain.handle(BIDIRECTIONAL.FILE_READ, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { success: false, error: '文件不存在' };
|
||||
// ---- 文件删除 ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.FILE_DELETE, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
logger.warn('storage', `文件删除失败:${params.relativePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return { success: true, data: buffer.toString('base64') };
|
||||
} catch (err) {
|
||||
logger.warn('storage', `文件读取失败:${params.relativePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 文件删除 ----
|
||||
ipcMain.handle(BIDIRECTIONAL.FILE_DELETE, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
// ---- 创建目录 ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.MAKE_DIR, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const dirPath = resolveSafe(dataDir, params.relativePath);
|
||||
ensureDir(dirPath);
|
||||
return { success: true, data: dirPath };
|
||||
} catch (err) {
|
||||
logger.warn('storage', `目录创建失败:${params.relativePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
logger.warn('storage', `文件删除失败:${params.relativePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 创建目录 ----
|
||||
ipcMain.handle(BIDIRECTIONAL.MAKE_DIR, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const dirPath = resolveSafe(dataDir, params.relativePath);
|
||||
ensureDir(dirPath);
|
||||
return { success: true, data: dirPath };
|
||||
} catch (err) {
|
||||
logger.warn('storage', `目录创建失败:${params.relativePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 在文件管理器中显示 ----
|
||||
ipcMain.handle(BIDIRECTIONAL.SHOW_ITEM_IN_FOLDER, async (_event, params: { filePath: string }) => {
|
||||
try {
|
||||
shell.showItemInFolder(params.filePath);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
logger.warn('storage', `showItemInFolder 失败:${params.filePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 文件大小 ----
|
||||
ipcMain.handle(BIDIRECTIONAL.FILE_SIZE, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { success: false, error: '文件不存在' };
|
||||
// ---- 在文件管理器中显示 ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.SHOW_ITEM_IN_FOLDER, async (_event, params: { filePath: string }) => {
|
||||
try {
|
||||
shell.showItemInFolder(params.filePath);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
logger.warn('storage', `showItemInFolder 失败:${params.filePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
const stat = fs.statSync(filePath);
|
||||
return { success: true, data: stat.size };
|
||||
} catch (err) {
|
||||
logger.warn('storage', '获取文件大小失败', err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 文件/目录是否存在 ----
|
||||
ipcMain.handle(BIDIRECTIONAL.FILE_EXISTS, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
return { success: true, data: fs.existsSync(filePath) };
|
||||
} catch {
|
||||
return { success: true, data: false };
|
||||
}
|
||||
});
|
||||
// ---- 文件大小 ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.FILE_SIZE, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { success: false, error: '文件不存在' };
|
||||
}
|
||||
const stat = fs.statSync(filePath);
|
||||
return { success: true, data: stat.size };
|
||||
} catch (err) {
|
||||
logger.warn('storage', '获取文件大小失败', err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('app', '存储 IPC 处理器已注册');
|
||||
// ---- 文件/目录是否存在 ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.FILE_EXISTS, async (_event, params: { relativePath: string }) => {
|
||||
try {
|
||||
const filePath = resolveSafe(dataDir, params.relativePath);
|
||||
return { success: true, data: fs.existsSync(filePath) };
|
||||
} catch {
|
||||
return { success: true, data: false };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 绝对路径文件写入(绕过沙箱,用于用户自定义输出目录) ----
|
||||
ipcMain.handle(
|
||||
BIDIRECTIONAL_STORAGE.FILE_WRITE_ABSOLUTE,
|
||||
async (_event, params: { absolutePath: string; data: string; encoding?: 'base64' | 'utf8' }) => {
|
||||
try {
|
||||
ensureDir(path.dirname(params.absolutePath));
|
||||
const buffer = Buffer.from(params.data, params.encoding || 'base64');
|
||||
fs.writeFileSync(params.absolutePath, buffer);
|
||||
logger.info('storage', `绝对路径写入成功:${params.absolutePath}(${buffer.length} bytes)`);
|
||||
return { success: true, data: params.absolutePath };
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
logger.warn('storage', `绝对路径写入失败:${params.absolutePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ---- 绝对路径目录创建(绕过沙箱) ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.MAKE_DIR_ABSOLUTE, async (_event, params: { absolutePath: string }) => {
|
||||
try {
|
||||
ensureDir(params.absolutePath);
|
||||
return { success: true, data: params.absolutePath };
|
||||
} catch (err) {
|
||||
logger.warn('storage', `绝对路径目录创建失败:${params.absolutePath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 获取系统下载目录 ----
|
||||
ipcMain.handle(BIDIRECTIONAL_STORAGE.GET_DOWNLOADS_PATH, () => {
|
||||
try {
|
||||
return { success: true, data: app.getPath('downloads') };
|
||||
} catch {
|
||||
return { success: false, error: '无法获取下载目录' };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 主进程下载文件到绝对路径(绕过浏览器 CORS) ----
|
||||
ipcMain.handle(
|
||||
BIDIRECTIONAL_STORAGE.DOWNLOAD_TO_PATH,
|
||||
async (
|
||||
_event,
|
||||
params: { url: string; destPath: string; headers?: Record<string, string> },
|
||||
): Promise<{ success: boolean; data?: string; error?: string }> => {
|
||||
const { url, destPath, headers } = params;
|
||||
try {
|
||||
await downloadToFile(url, destPath, headers);
|
||||
logger.info('storage', `主进程下载成功:${destPath}`);
|
||||
return { success: true, data: destPath };
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message || '下载失败';
|
||||
logger.warn('storage', `主进程下载失败:${url}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ---- 生成图片缩略图(sharp 流式管线) ----
|
||||
ipcMain.handle(
|
||||
BIDIRECTIONAL_STORAGE.GENERATE_THUMBNAIL,
|
||||
async (
|
||||
_event,
|
||||
params: { srcPath: string; destPath: string; maxWidth?: number; quality?: number },
|
||||
): Promise<{ success: boolean; data?: string; error?: string }> => {
|
||||
const { srcPath, destPath, maxWidth = 256, quality = 60 } = params;
|
||||
try {
|
||||
await generateThumbnail(srcPath, destPath, maxWidth, quality);
|
||||
logger.info('storage', `缩略图生成成功:${destPath}`);
|
||||
return { success: true, data: destPath };
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message || '缩略图生成失败';
|
||||
logger.warn('storage', `缩略图生成失败:${srcPath}`, err instanceof Error ? err : undefined);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
logger.info('app', '存储 IPC 处理器已注册');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user