三栏布局 - ResizeObserver 等比例分配替代固定默认值恢复 - 修复拖拽+窗口调整竞态卡死(needsRecalcRef 延迟补算) - containerRef.current 动态读取 DOM 替代闭包 el Seedance2Content - 从占位文本重构为内容编排控件(文本+图片/视频/音频上传) - 根据 spec.image_files/video_files/audio_files 动态渲染上传区域 - 值 JSON 序列化 → 提交时转 ContentItem[](text/image_url/video_url/audio_url) 图片上传兼容视频文件 - buildAccept 根据 file_filter 区分 image/video 前缀 - createMediaBeforeUpload 动态放行 video/* MIME - 公共模块 media-upload-utils.ts 本地存储基建 - sql.js (WASM SQLite) + safeStorage 加密持久化 - 8个IPC文件操作通道 + resolveSafe 路径穿越防护 - <userData>/heixiu-data/ 沙箱 + JWT用户隔离 - 7表DDL + 游标分页 + LRU媒体淘汰 其他 - 修复 errorMessageRef 渲染期写入警告 - 媒体预览回退直接URL(<img>无CORS限制)+ output_type类型补充 - CSP wasm-unsafe-eval + https: CDN媒体源
90 lines
3.9 KiB
TypeScript
90 lines
3.9 KiB
TypeScript
import { ipcRenderer, contextBridge } from 'electron';
|
||
import { BIDIRECTIONAL } from '../shared/constants/ipc-channels';
|
||
|
||
// --------- Expose some API to the Renderer process ---------
|
||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
||
on(...args: Parameters<typeof ipcRenderer.on>) {
|
||
const [channel, listener] = args;
|
||
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args));
|
||
},
|
||
off(...args: Parameters<typeof ipcRenderer.off>) {
|
||
const [channel, ...omit] = args;
|
||
return ipcRenderer.off(channel, ...omit);
|
||
},
|
||
send(...args: Parameters<typeof ipcRenderer.send>) {
|
||
const [channel, ...omit] = args;
|
||
return ipcRenderer.send(channel, ...omit);
|
||
},
|
||
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
|
||
const [channel, ...omit] = args;
|
||
return ipcRenderer.invoke(channel, ...omit);
|
||
},
|
||
});
|
||
|
||
// --------- 安全加密 API(基于 Electron safeStorage)---------
|
||
contextBridge.exposeInMainWorld('safeStorage', {
|
||
/**
|
||
* 加密明文字符串
|
||
* @param plaintext - 待加密的明文
|
||
* @returns Base64 编码的密文,失败返回 null
|
||
*/
|
||
async encrypt(plaintext: string): Promise<string | null> {
|
||
const result = await ipcRenderer.invoke(BIDIRECTIONAL.SAFESTORAGE_ENCRYPT, plaintext);
|
||
return result?.success ? result.data : null;
|
||
},
|
||
|
||
/**
|
||
* 解密 Base64 密文
|
||
* @param encryptedBase64 - Base64 编码的密文
|
||
* @returns 解密后的明文,失败返回 null
|
||
*/
|
||
async decrypt(encryptedBase64: string): Promise<string | null> {
|
||
const result = await ipcRenderer.invoke(BIDIRECTIONAL.SAFESTORAGE_DECRYPT, encryptedBase64);
|
||
return result?.success ? result.data : null;
|
||
},
|
||
});
|
||
|
||
// --------- 本地存储文件 API(主进程 fs 桥接)---------
|
||
contextBridge.exposeInMainWorld('fileStorage', {
|
||
/** 获取 heixiu-data 根目录路径 */
|
||
getDataDir(): Promise<string | null> {
|
||
return ipcRenderer.invoke(BIDIRECTIONAL.GET_DATA_DIR)
|
||
.then((r: { success: boolean; data: string }) => r?.success ? r.data : null);
|
||
},
|
||
/** 写入文件(Base64 → 磁盘),返回写入的绝对路径 */
|
||
writeFile(relativePath: string, data: string, encoding?: 'base64' | 'utf8'): Promise<string | null> {
|
||
return ipcRenderer.invoke(BIDIRECTIONAL.FILE_WRITE, { relativePath, data, encoding })
|
||
.then((r: { success: boolean; data: string }) => r?.success ? r.data : null);
|
||
},
|
||
/** 读取文件(磁盘 → Base64) */
|
||
readFile(relativePath: string): Promise<string | null> {
|
||
return ipcRenderer.invoke(BIDIRECTIONAL.FILE_READ, { relativePath })
|
||
.then((r: { success: boolean; data: string }) => r?.success ? r.data : null);
|
||
},
|
||
/** 删除文件 */
|
||
deleteFile(relativePath: string): Promise<boolean> {
|
||
return ipcRenderer.invoke(BIDIRECTIONAL.FILE_DELETE, { relativePath })
|
||
.then((r: { success: boolean }) => r?.success ?? false);
|
||
},
|
||
/** 递归创建目录 */
|
||
makeDir(relativePath: string): Promise<string | null> {
|
||
return ipcRenderer.invoke(BIDIRECTIONAL.MAKE_DIR, { relativePath })
|
||
.then((r: { success: boolean; data: string }) => r?.success ? r.data : null);
|
||
},
|
||
/** 在文件管理器中显示文件(绝对路径) */
|
||
showItemInFolder(filePath: string): Promise<boolean> {
|
||
return ipcRenderer.invoke(BIDIRECTIONAL.SHOW_ITEM_IN_FOLDER, { filePath })
|
||
.then((r: { success: boolean }) => r?.success ?? false);
|
||
},
|
||
/** 获取文件大小(字节) */
|
||
fileSize(relativePath: string): Promise<number | null> {
|
||
return ipcRenderer.invoke(BIDIRECTIONAL.FILE_SIZE, { relativePath })
|
||
.then((r: { success: boolean; data: number }) => r?.success ? r.data : null);
|
||
},
|
||
/** 检查文件/目录是否存在 */
|
||
fileExists(relativePath: string): Promise<boolean> {
|
||
return ipcRenderer.invoke(BIDIRECTIONAL.FILE_EXISTS, { relativePath })
|
||
.then((r: { success: boolean; data: boolean }) => r?.success ? r.data : false);
|
||
},
|
||
});
|