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:
219
electron/main/updater/index.ts
Normal file
219
electron/main/updater/index.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
102
electron/main/updater/platform-updaters.ts
Normal file
102
electron/main/updater/platform-updaters.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
172
electron/main/updater/provider.ts
Normal file
172
electron/main/updater/provider.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user