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,219 @@
// ============================================================
// updater/index.ts — 自动更新主入口
//
// 流程图:调用自有 API → 获取版本信息 → electron-updater 差分下载 → SHA512 校验 → 安装
// ============================================================
import { BrowserWindow, ipcMain } from 'electron';
import type { NsisUpdater, MacUpdater, AppImageUpdater } from 'electron-updater';
import type { UpdateInfo } from 'builder-util-runtime';
import { APP_VERSION } from '../../../shared/constants/version';
import { MAIN_TO_RENDERER_UPDATER, RENDERER_TO_MAIN_UPDATER } from '../../../shared/constants/ipc/updater';
import { logger } from '../logger';
import { HeiXiuProvider, getApiBaseUrl, getDeviceFingerprint, BUILD_CHANNEL, BUILD_EDITION } from './provider';
import { createPlatformUpdater } from './platform-updaters';
// ---------- 状态 ----------
let updateWindow: BrowserWindow | null = null;
let provider: HeiXiuProvider | null = null;
let platformUpdater: NsisUpdater | MacUpdater | AppImageUpdater | null = null;
// ============================================================
// 初始化
// ============================================================
export function initUpdater(mainWindow: BrowserWindow): void {
updateWindow = mainWindow;
console.log('[updater] initUpdater 被调用');
console.log(`[updater] API URL: ${getApiBaseUrl()}`);
console.log(`[updater] 渠道: ${BUILD_CHANNEL}`);
console.log(`[updater] 版本类型: ${BUILD_EDITION}`);
console.log(`[updater] 当前版本: ${APP_VERSION}`);
console.log(`[updater] 设备指纹: ${getDeviceFingerprint()}`);
// 创建自定义 Provider
provider = new HeiXiuProvider({
isUseMultipleRangeRequest: true,
platform: process.platform as 'win32' | 'darwin' | 'linux',
executor: null as never,
});
// 创建平台 Updater
platformUpdater = createPlatformUpdater(provider);
// 将 electron-updater 事件桥接到现有 IPC 通道
platformUpdater.on('checking-for-update', () => {
logger.debug('updater', '正在检查更新');
});
platformUpdater.on('update-available', (info: UpdateInfo) => {
// releaseNotes 可能是 string | ReleaseNoteInfo[] | null统一转为 string
let notes = '';
if (typeof info.releaseNotes === 'string') {
notes = info.releaseNotes;
} else if (Array.isArray(info.releaseNotes)) {
notes = info.releaseNotes.map((n) => n.note).filter(Boolean).join('\n');
}
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_AVAILABLE, {
version: info.version,
releaseDate: info.releaseDate,
releaseNotes: notes,
forceUpdate: false,
});
logger.info('updater', '发现新版本', {
version: info.version,
releaseNotes: notes ? `${notes.slice(0, 50)}...` : '(空)',
});
});
platformUpdater.on('update-not-available', (info: UpdateInfo) => {
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_NOT_AVAILABLE);
logger.info('updater', '当前已是最新版本', { currentVersion: info.version });
});
platformUpdater.on('download-progress', (progress: {
percent: number;
bytesPerSecond: number;
transferred: number;
total: number;
}) => {
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_PROGRESS, {
percent: Math.round(progress.percent),
bytesPerSecond: progress.bytesPerSecond,
transferred: progress.transferred,
total: progress.total,
});
});
platformUpdater.on('update-downloaded', () => {
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_DOWNLOADED);
logger.info('updater', '更新已下载完成');
});
platformUpdater.on('error', (error: Error) => {
logger.error('updater', '更新流程出错', error);
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_ERROR, {
message: error.message || '更新失败',
});
});
// 启动 5 秒后静默检查
setTimeout(() => {
checkForUpdates();
}, 5000);
}
// ============================================================
// 操作方法
// ============================================================
let updateChecked = false;
/** 静默检查更新(自动下载) */
export async function checkForUpdates(): Promise<void> {
if (updateChecked) {
logger.debug('updater', '已检查过更新,跳过');
return;
}
if (!platformUpdater || !provider) return;
try {
updateChecked = true;
// ① 先自行检查有无更新(不经过 electron-updater
const info = await provider.getLatestVersion();
if (info.version === APP_VERSION) {
// 无更新 → 直接通知 UI不触发 electron-updater 的任何事件
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_NOT_AVAILABLE);
logger.debug('updater', '当前已是最新版本', { version: APP_VERSION });
return;
}
// ② 有新版本 → 交给 electron-updater 触发 'update-available' 事件(不自动下载)
await platformUpdater.checkForUpdates();
} catch (error) {
updateChecked = false;
logger.error('updater', '更新流程失败', error as Error, { apiUrl: getApiBaseUrl() });
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_ERROR, {
message: (error as Error).message || '更新检查失败',
});
}
}
/** 手动检查更新(用户点击触发) */
export async function checkForUpdatesManual(): Promise<void> {
if (!platformUpdater || !provider) return;
try {
updateChecked = false;
// ① 先自行检查有无更新
const info = await provider.getLatestVersion();
if (info.version === APP_VERSION) {
// 无更新 → 直接通知 UI
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_NOT_AVAILABLE);
logger.debug('updater', '手动检查:当前已是最新版本', { version: APP_VERSION });
return;
}
// ② 有新版本 → 交给 electron-updater 触发 'update-available' 事件(不自动下载)
await platformUpdater.checkForUpdates();
} catch (error) {
logger.error('updater', '手动更新失败', error as Error);
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_ERROR, {
message: (error as Error).message || '检查更新失败',
});
}
}
/** 用户确认后开始下载更新 */
export async function downloadUpdate(): Promise<void> {
if (!platformUpdater) {
logger.warn('updater', 'downloadUpdate: Updater 未初始化');
return;
}
try {
await platformUpdater.downloadUpdate();
} catch (error) {
logger.error('updater', '下载更新失败', error as Error);
updateWindow?.webContents.send(MAIN_TO_RENDERER_UPDATER.UPDATE_ERROR, {
message: (error as Error).message || '下载更新失败',
});
}
}
/** 安装已下载的更新并退出应用 */
export function installUpdateNow(): void {
if (!platformUpdater) {
logger.warn('updater', 'Updater 未初始化');
return;
}
logger.info('updater', '开始安装更新并退出应用');
setImmediate(() => {
platformUpdater!.quitAndInstall(false, true);
});
}
// ============================================================
// IPC 注册
// ============================================================
export function registerUpdateIpcHandlers(): void {
ipcMain.handle(RENDERER_TO_MAIN_UPDATER.CHECK_FOR_UPDATE, async () => {
await checkForUpdatesManual();
});
ipcMain.handle(RENDERER_TO_MAIN_UPDATER.DOWNLOAD_UPDATE, async () => {
await downloadUpdate();
});
ipcMain.on(RENDERER_TO_MAIN_UPDATER.INSTALL_UPDATE, () => {
installUpdateNow();
});
}

View File

@@ -0,0 +1,102 @@
// ============================================================
// platform-updaters.ts — 平台特定 Updater
//
// 覆盖 getUpdateInfoAndProvider() 以注入 HeiXiuProvider。
// 其他行为NSIS 安装、签名校验等)完全由父类处理。
// ============================================================
import {
NsisUpdater,
MacUpdater,
AppImageUpdater,
Provider,
} from 'electron-updater';
import type { UpdateInfo } from 'builder-util-runtime';
import type { HeiXiuProvider } from './provider';
// ---------- Windows NSIS ----------
export class HeiXiuNsisUpdater extends NsisUpdater {
private heiXiuProvider: HeiXiuProvider;
constructor(heiXiuProvider: HeiXiuProvider) {
// 传 undefined 跳过内置 Provider 创建,由我们自己注入
super(undefined);
this.heiXiuProvider = heiXiuProvider;
this.autoDownload = false; // 由我们手动控制流程,保持 IPC 兼容
this.autoInstallOnAppQuit = false;
this.forceDevUpdateConfig = true; // 绕过 electron-updater 的 dev 模式检查
}
/** @internal — 注入自定义 Provider */
protected override async getUpdateInfoAndProvider(): Promise<{
info: UpdateInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: Provider<any>;
}> {
const info = await this.heiXiuProvider.getLatestVersion();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { info, provider: this.heiXiuProvider as Provider<any> };
}
}
// ---------- macOS ----------
export class HeiXiuMacUpdater extends MacUpdater {
private heiXiuProvider: HeiXiuProvider;
constructor(heiXiuProvider: HeiXiuProvider) {
super(undefined);
this.heiXiuProvider = heiXiuProvider;
this.autoDownload = false;
this.autoInstallOnAppQuit = false;
}
protected override async getUpdateInfoAndProvider(): Promise<{
info: UpdateInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: Provider<any>;
}> {
const info = await this.heiXiuProvider.getLatestVersion();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { info, provider: this.heiXiuProvider as Provider<any> };
}
}
// ---------- Linux AppImage ----------
export class HeiXiuAppImageUpdater extends AppImageUpdater {
private heiXiuProvider: HeiXiuProvider;
constructor(heiXiuProvider: HeiXiuProvider) {
super(null);
this.heiXiuProvider = heiXiuProvider;
this.autoDownload = false;
this.autoInstallOnAppQuit = false;
}
protected override async getUpdateInfoAndProvider(): Promise<{
info: UpdateInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: Provider<any>;
}> {
const info = await this.heiXiuProvider.getLatestVersion();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { info, provider: this.heiXiuProvider as Provider<any> };
}
}
// ---------- 工厂函数 ----------
/** 工厂:按平台创建对应的 Updater 实例 */
export function createPlatformUpdater(provider: HeiXiuProvider) {
switch (process.platform) {
case 'win32':
return new HeiXiuNsisUpdater(provider);
case 'darwin':
return new HeiXiuMacUpdater(provider);
default:
return new HeiXiuAppImageUpdater(provider);
}
}

View File

@@ -0,0 +1,172 @@
// ============================================================
// provider.ts — 自定义更新 Provider
//
// 封装与自有版本检查 API 的交互逻辑,
// 返回 electron-updater 格式的 UpdateInfo。
// ============================================================
import { app } from 'electron';
import { createHash } from 'node:crypto';
import { URL } from 'node:url';
import { Provider } from 'electron-updater';
import type { ProviderRuntimeOptions } from 'electron-updater/out/providers/Provider';
import type { UpdateInfo, UpdateFileInfo } from 'builder-util-runtime';
import type { ResolvedUpdateFileInfo } from 'electron-updater/out/types';
import type { UpdateCheckResult } from '../../../shared/types';
import { APP_VERSION } from '../../../shared/constants/version';
import { logger } from '../logger';
import { netRequest } from '../net-request';
// ---------- 构建时注入的渠道信息 ----------
/** 构建渠道(由 build.mjs 通过 VITE_BUILD_CHANNEL 注入Vite 编译时内联) */
export const BUILD_CHANNEL: string =
(typeof import.meta !== 'undefined' && (import.meta as unknown as Record<string, unknown>).env
? ((import.meta as unknown as Record<string, unknown>).env as Record<string, string>).VITE_BUILD_CHANNEL
: undefined)
|| process.env.VITE_BUILD_CHANNEL
|| 'stable';
/** 构建版本类型(由 build.mjs 通过 VITE_BUILD_EDITION 注入) */
export const BUILD_EDITION: string =
(typeof import.meta !== 'undefined' && (import.meta as unknown as Record<string, unknown>).env
? ((import.meta as unknown as Record<string, unknown>).env as Record<string, string>).VITE_BUILD_EDITION
: undefined)
|| process.env.VITE_BUILD_EDITION
|| 'personal';
// ---------- 设备指纹(稳定的机器标识,用于灰度分组)----------
let _deviceFingerprint: string | null = null;
/** 获取设备指纹(基于机器特定信息的 SHA-256 前 16 位) */
export function getDeviceFingerprint(): string {
if (_deviceFingerprint) return _deviceFingerprint;
try {
const parts = [
process.platform,
process.arch,
app.getPath('home'),
];
_deviceFingerprint = createHash('sha256')
.update(parts.join('|'))
.digest('hex')
.slice(0, 16);
} catch {
_deviceFingerprint = 'unknown-device';
}
return _deviceFingerprint;
}
// ---------- 配置 ----------
export function getApiBaseUrl(): string {
const url = (process.env.UPDATE_API_URL || 'https://www.heixiu.com').trim();
console.log(`[updater] UPDATE_API_URL=${process.env.UPDATE_API_URL} → 使用: ${url}`);
return url;
}
// ============================================================
// HeiXiuProvider
// ============================================================
export class HeiXiuProvider extends Provider<UpdateInfo> {
private apiBaseUrl: string;
/** 短时缓存:避免 checkForUpdates 预检 + electron-updater 内部调用导致两次 API 请求 */
private _cachedResult: { data: UpdateInfo; ts: number } | null = null;
constructor(runtimeOptions: ProviderRuntimeOptions) {
super(runtimeOptions);
this.apiBaseUrl = getApiBaseUrl();
}
/**
* 调用自有版本检查 API返回 electron-updater 格式的 UpdateInfo。
* blockMapSize > 0 时 electron-updater 自动启用差分下载。
* 无更新时返回当前版本号APP_VERSION
*/
async getLatestVersion(): Promise<UpdateInfo> {
// 短时缓存500ms预检和 electron-updater 内部调用共享同一结果
if (this._cachedResult && Date.now() - this._cachedResult.ts < 500) {
return this._cachedResult.data;
}
const data = await this._doGetLatestVersion();
this._cachedResult = { data, ts: Date.now() };
return data;
}
private async _doGetLatestVersion(): Promise<UpdateInfo> {
const { data } = await netRequest.get<{ code: number; data?: UpdateCheckResult } & UpdateCheckResult>(
'/api/v1/update/check',
{
baseURL: this.apiBaseUrl,
params: {
platform: process.platform,
version: APP_VERSION,
channel: BUILD_CHANNEL,
edition: BUILD_EDITION,
deviceFingerprint: getDeviceFingerprint(),
},
},
);
// 兼容两种 API 响应格式
const result: UpdateCheckResult =
(data as { data?: UpdateCheckResult }).data ?? (data as UpdateCheckResult);
// 无更新 → 返回当前版本号
if (!result.hasUpdate) {
return {
version: APP_VERSION,
files: [],
path: '',
sha512: '',
releaseDate: new Date().toISOString(),
};
}
const file: UpdateFileInfo = {
url: result.downloadUrl,
sha512: result.sha512,
size: result.fileSize,
};
// 后端 API 返回 blockMapSize 时启用增量更新
if (result.blockMapSize) {
file.blockMapSize = result.blockMapSize;
}
logger.info('updater', 'Provider 获取到新版本', {
latestVersion: result.latestVersion,
hasBlockmap: file.blockMapSize != null,
});
return {
version: result.latestVersion,
files: [file],
path: result.downloadUrl,
sha512: result.sha512,
releaseDate: result.releaseDate,
releaseNotes: result.changelog,
};
}
/**
* 将 UpdateInfo 中的相对路径解析为完整的下载 URL。
* electron-updater 在下载前调用此方法。
*/
resolveFiles(updateInfo: UpdateInfo): Array<ResolvedUpdateFileInfo> {
return updateInfo.files.map((f) => {
// 使用 downloadUrl 作为 base URL 来解析文件路径
const baseUrl = new URL(updateInfo.path);
const fileUrl = new URL(f.url, baseUrl.origin);
return {
url: fileUrl,
info: f,
};
});
}
}