- 任务右键菜单:实现打开输出目录(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 行
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
// ============================================================
|
||
// download.ts — 文件下载工具函数
|
||
//
|
||
// 职责:
|
||
// - 通过 Electron net 模块下载文件到磁盘
|
||
// - 支持重定向、超时、自定义请求头
|
||
//
|
||
// 设计:
|
||
// - net 基于 Chromium 网络栈,自动携带浏览器标准头,不受 CORS 限制
|
||
// - 不能用 netRequest(它 chunk.toString() 会损坏二进制数据)
|
||
// - 不能用 Node.js https(缺少浏览器头导致 CDN 返回 400)
|
||
// ============================================================
|
||
|
||
import { net } from 'electron';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { ensureDir } from './path';
|
||
|
||
/**
|
||
* 通过 Electron net 模块下载文件到磁盘。
|
||
*
|
||
* @param url - 下载地址
|
||
* @param destPath - 目标文件路径
|
||
* @param headers - 可选的自定义请求头
|
||
*/
|
||
export function downloadToFile(
|
||
url: string,
|
||
destPath: string,
|
||
headers?: Record<string, string>,
|
||
): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
const req = net.request({ method: 'GET', url });
|
||
|
||
// 自定义请求头
|
||
if (headers) {
|
||
for (const [key, value] of Object.entries(headers)) {
|
||
req.setHeader(key, value);
|
||
}
|
||
}
|
||
|
||
// 超时 120s
|
||
let timedOut = false;
|
||
const timer = setTimeout(() => {
|
||
timedOut = true;
|
||
req.abort();
|
||
reject(new Error('下载超时(120s)'));
|
||
}, 120_000);
|
||
|
||
req.on('response', (response) => {
|
||
clearTimeout(timer);
|
||
const status = response.statusCode;
|
||
|
||
// 3xx 重定向
|
||
if (status >= 300 && status < 400) {
|
||
const location = response.headers['location']?.[0];
|
||
if (location && location !== url) {
|
||
downloadToFile(location, destPath, headers).then(resolve).catch(reject);
|
||
} else {
|
||
reject(new Error('重定向循环或缺少 Location 头'));
|
||
}
|
||
response.on('data', () => {}); // 消费响应体
|
||
return;
|
||
}
|
||
|
||
if (status >= 400) {
|
||
reject(new Error(`HTTP ${status}`));
|
||
response.on('data', () => {});
|
||
return;
|
||
}
|
||
|
||
// 收集二进制 chunks → 写入文件
|
||
const chunks: Buffer[] = [];
|
||
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||
response.on('end', () => {
|
||
try {
|
||
ensureDir(path.dirname(destPath));
|
||
const buffer = Buffer.concat(chunks);
|
||
fs.writeFileSync(destPath, buffer);
|
||
resolve();
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
});
|
||
response.on('error', (err) => {
|
||
reject(new Error(`响应读取失败: ${err.message}`));
|
||
});
|
||
});
|
||
|
||
req.on('error', (err) => {
|
||
clearTimeout(timer);
|
||
if (timedOut) return;
|
||
reject(new Error(`网络请求失败: ${err.message}`));
|
||
});
|
||
|
||
req.end();
|
||
});
|
||
}
|