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:
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