Files
ele-HeiXiu/src/infrastructure/storage/db-core.ts
YoungestSongMo e4c9ae8fc6 feat: 三栏比例缩放 + SeedanceContent视频上传 + 本地存储基建 (0.0.22)
三栏布局
- 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媒体源
2026-06-18 19:22:28 +08:00

177 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// infrastructure/storage/db-core.ts — sql.js 数据库核心封装
//
// 职责:
// - 初始化 sql.js WASM 运行时 + 创建/加载 Database
// - 执行 DDL 建表 / 迁移
// - 提供通用 CRUD 操作execute / query / run / get
// - 导出数据库为 Uint8Array供加密落盘
//
// 安全:
// - 数据库完全在内存中运行,不直接操作磁盘
// - 数据导出由 db-encrypt.ts 加密后写入磁盘
// ============================================================
import initSqlJs, { type Database, type SqlJsStatic, type SqlValue } from 'sql.js';
import { DDL_STATEMENTS, INIT_VERSION_SQL } from './schema';
import { logger } from '@/utils/logger';
// ---------- 内部状态 ----------
let SQL: SqlJsStatic | null = null;
let db: Database | null = null;
// ---------- 初始化 ----------
/**
* 初始化 sql.js 运行时WASM 加载),仅需调用一次。
*/
async function ensureRuntime(): Promise<SqlJsStatic> {
if (SQL) return SQL;
try {
SQL = await initSqlJs({
locateFile: (file: string) => {
const url = import.meta.env.DEV ? `/${file}` : `./${file}`;
console.log(`[storage] 加载 sql.js WASM${url}`);
return url;
},
});
console.log('[storage] sql.js 运行时初始化成功');
return SQL;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[storage] sql.js WASM 加载失败:${msg}`);
logger.error('storage', `sql.js WASM 加载失败:${msg}`, err instanceof Error ? err : undefined);
throw err;
}
}
/**
* 打开或创建数据库。
*
* @param encryptedData - 从磁盘读取的加密数据(由 db-encrypt.ts 解密后提供),
* 未提供则创建空库。
* @returns true 表示成功
*/
export async function openDatabase(encryptedData?: Uint8Array): Promise<boolean> {
try {
const sql = await ensureRuntime();
db = new sql.Database(encryptedData ?? undefined);
// 执行建表 DDL
db.run('PRAGMA journal_mode=OFF');
db.run('PRAGMA synchronous=0');
for (const stmt of DDL_STATEMENTS) {
db.run(stmt);
}
db.run(INIT_VERSION_SQL);
const size = (db.export()?.byteLength ?? 0);
console.log(`[storage] 数据库已打开(${encryptedData ? '从磁盘恢复' : '新建'}${DDL_STATEMENTS.length} 条 DDL${size} bytes`);
logger.info('storage', `数据库已打开(${encryptedData ? '从磁盘恢复' : '新建'}`);
return true;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[storage] 数据库打开失败:${msg}`);
logger.error('storage', `数据库打开失败:${msg}`, err instanceof Error ? err : undefined);
return false;
}
}
// ---------- 导出 ----------
/**
* 导出当前数据库为 Uint8Array供加密后写入磁盘
*/
export function exportDatabase(): Uint8Array | null {
if (!db) return null;
try {
return db.export();
} catch (err) {
logger.error('storage', '数据库导出失败', err instanceof Error ? err : undefined);
return null;
}
}
// ---------- 关闭 ----------
export function closeDatabase(): void {
if (db) {
db.close();
db = null;
logger.info('storage', '数据库已关闭');
}
}
export function isOpen(): boolean {
return db !== null;
}
// ---------- 查询 API ----------
/** 确保数据库已打开 */
function guard(): Database {
if (!db) throw new Error('数据库未初始化 — 请确认已调用 initStorage() 并等待其完成');
return db;
}
/** 执行一条写操作 SQLINSERT / UPDATE / DELETE返回受影响行数 */
export function execute(sql: string, params?: SqlValue[]): number {
try {
guard().run(sql, params);
return guard().getRowsModified();
} catch (err) {
logger.error('storage', `SQL 执行失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined);
throw err;
}
}
/** 查询多条记录 */
export function queryAll<T = Record<string, unknown>>(sql: string, params?: SqlValue[]): T[] {
try {
const stmt = guard().prepare(sql);
if (params) stmt.bind(params);
const rows: T[] = [];
while (stmt.step()) {
rows.push(stmt.getAsObject() as T);
}
stmt.free();
return rows;
} catch (err) {
logger.error('storage', `SQL 查询失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined);
return [];
}
}
/** 查询单条记录 */
export function queryOne<T = Record<string, unknown>>(sql: string, params?: SqlValue[]): T | null {
try {
const stmt = guard().prepare(sql);
if (params) stmt.bind(params);
let row: T | null = null;
if (stmt.step()) {
row = stmt.getAsObject() as T;
}
stmt.free();
return row;
} catch (err) {
logger.error('storage', `SQL 查询失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined);
return null;
}
}
/** 执行多条 SQL事务包裹 */
export function executeBatch(statements: string[]): void {
const d = guard();
try {
d.run('BEGIN');
for (const stmt of statements) {
d.run(stmt);
}
d.run('COMMIT');
} catch (err) {
try { d.run('ROLLBACK'); } catch { /* ignore */ }
throw err;
}
}