- 任务右键菜单:实现打开输出目录(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 行
85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
// ============================================================
|
||
// canvas/thumbnail.ts — 画布缩略图生成
|
||
//
|
||
// 职责:
|
||
// - 生成画布专用缩略图(返回 base64 数据)
|
||
// - 支持缓存优先策略(.cache 目录)
|
||
//
|
||
// 优先级:
|
||
// 1. .cache/{filename}.webp — 预生成 WEBP 缩略图(最快)
|
||
// 2. .cache/{filename} — 预生成 PNG 缩略图
|
||
// 3. sharp — 实时缩放(可选依赖)
|
||
// 4. 原始文件字节 — 浏览器端缩放回退
|
||
// ============================================================
|
||
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import sharp from 'sharp';
|
||
import { getMimeType } from './mime';
|
||
import type { ThumbnailResult } from './types';
|
||
|
||
/**
|
||
* 生成画布缩略图。
|
||
*
|
||
* @param filePath - 源文件路径
|
||
* @param size - 缩略图尺寸
|
||
* @returns 缩略图数据(base64)
|
||
*/
|
||
export async function generateCanvasThumbnail(
|
||
filePath: string,
|
||
size: number,
|
||
): Promise<ThumbnailResult> {
|
||
const dir = path.dirname(filePath);
|
||
const baseName = path.basename(filePath);
|
||
const cacheDir = path.join(dir, '.cache');
|
||
|
||
// ── 优先级 1:.cache/{filename}.webp ──
|
||
const cachedWebp = path.join(cacheDir, `${baseName}.webp`);
|
||
if (fs.existsSync(cachedWebp)) {
|
||
const buffer = fs.readFileSync(cachedWebp);
|
||
return {
|
||
data: buffer.toString('base64'),
|
||
mimeType: 'image/webp',
|
||
width: 0,
|
||
height: 0,
|
||
};
|
||
}
|
||
|
||
// ── 优先级 2:.cache/{filename}(PNG 副本) ──
|
||
const cachedPng = path.join(cacheDir, baseName);
|
||
if (fs.existsSync(cachedPng)) {
|
||
const buffer = fs.readFileSync(cachedPng);
|
||
return {
|
||
data: buffer.toString('base64'),
|
||
mimeType: 'image/png',
|
||
width: 0,
|
||
height: 0,
|
||
};
|
||
}
|
||
|
||
if (sharp) {
|
||
const image = sharp(filePath);
|
||
const metadata = await image.metadata();
|
||
const resized = await image
|
||
.resize(size, size, { fit: 'inside', withoutEnlargement: true })
|
||
.jpeg({ quality: 85 })
|
||
.toBuffer();
|
||
return {
|
||
data: resized.toString('base64'),
|
||
mimeType: 'image/jpeg',
|
||
width: metadata.width || 0,
|
||
height: metadata.height || 0,
|
||
};
|
||
}
|
||
|
||
// ── 优先级 4:回退原始文件 ──
|
||
const buffer = fs.readFileSync(filePath);
|
||
const mimeType = getMimeType(filePath);
|
||
return {
|
||
data: buffer.toString('base64'),
|
||
mimeType,
|
||
width: 0,
|
||
height: 0,
|
||
};
|
||
}
|