feat: 去除构建时版控变量 + 窗口标题动态版控 + 退出登录保留缓存 (0.0.24)

版控体系重构(13 文件):
- 移除 build.mjs --personal/--enterprise 参数和 EDITION 环境变量
- 统一安装包文件名:船长-HeiXiu-{os}-{版本号}-Setup.{ext}
- 简化 package.json 构建脚本,移除 :enterprise 变体
- scripts/*.mjs 不再接受 edition 参数
- 移除 shared/constants/app.ts 的 parseEdition()

窗口标题动态版控:
- 新格式:船长-HeiXiu-{os}-{版本号}(登录前)/ ·{版控} 后缀(登录后)
- 链路:AppProvider(account_type) → useEffect → appRuntime IPC → main process
- 移除 updater.ts 更新检查 API 的 edition 查询参数

本地存储生命周期修正:
- 退出登录不再自动清空缓存(user_id 隔离,重新登录命中缓存)
- clearUserStorage 添加 isOpen() 竞态守卫
- 移除 closeDatabase() 避免退出→重新登录后数据库不可用
- 手动「清理缓存」按钮保留

文档:
- CHANGELOG.md 新增 0.0.24 条目
- TODO.md 新增已完成项
This commit is contained in:
2026-06-22 13:36:42 +08:00
parent e4d66a2ac0
commit 8b697e775d
95 changed files with 1158 additions and 1295 deletions

View File

@@ -0,0 +1,21 @@
// ============================================================
// 日志 IPC 通道注册 — 主进程接收渲染进程日志
// ============================================================
import { ipcMain } from 'electron';
import { RENDERER_TO_MAIN } from '../../../shared/constants/ipc-channels';
import { writeRendererLog } from '../logger';
import type { LogEntry } from '../../../shared/types/logging';
/**
* 注册日志 IPC 监听器
* 使用 ipcMain.onfire-and-forget而非 handle无需响应
* 在 app.whenReady() 中调用
*/
export function registerLogIpcHandlers(): void {
ipcMain.on(RENDERER_TO_MAIN.LOG_MESSAGE, (_event, entry: LogEntry) => {
// 基础校验:防御渲染进程异常数据
if (!entry || !entry.level || !entry.category || !entry.message) return;
writeRendererLog(entry);
});
}

View File

@@ -0,0 +1,81 @@
// ============================================================
// safe-storage-ipc — safeStorage 加密/解密 IPC 处理器
//
// 在主进程注册,渲染进程通过 preload 暴露的安全方法调用。
// 加密后的数据为 Base64 字符串,方便存储在 localStorage。
//
// 平台对应:
// Windows → DPAPI
// macOS → Keychain
// Linux → libsecret
//
// 注意:
// - safeStorage 加密绑定当前 OS 用户账户,无法跨用户/跨设备解密
// - 用户重装 OS 后加密数据将无法恢复(需重新登录)
// - safeStorage.isEncryptionAvailable() 在部分 Linux 发行版可能返回 false
// ============================================================
import { ipcMain, safeStorage } from 'electron';
import { BIDIRECTIONAL } from '../../../shared/constants/ipc-channels';
import { logger } from '../logger';
/** 检查 safeStorage 是否可用,不可用时记录警告 */
function checkAvailability(): boolean {
if (!safeStorage.isEncryptionAvailable()) {
logger.warn('safe-storage', 'safeStorage 加密不可用(可能缺少 libsecret 等系统密钥服务)');
return false;
}
return true;
}
/**
* 注册 safeStorage IPC 处理器
* 在 app.whenReady() 中调用
*/
export function registerSafeStorageIpcHandlers(): void {
// ---------- 加密 ----------
ipcMain.handle(BIDIRECTIONAL.SAFESTORAGE_ENCRYPT, (_event, plaintext: string) => {
if (typeof plaintext !== 'string' || plaintext.length === 0) {
logger.warn('safe-storage', '加密参数无效');
return { success: false, error: '参数无效:需要非空字符串' };
}
if (!checkAvailability()) {
return { success: false, error: 'safeStorage 不可用' };
}
try {
const encrypted = safeStorage.encryptString(plaintext);
const base64 = encrypted.toString('base64');
return { success: true, data: base64 };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error('safe-storage', '加密失败', err instanceof Error ? err : new Error(message));
return { success: false, error: message };
}
});
// ---------- 解密 ----------
ipcMain.handle(BIDIRECTIONAL.SAFESTORAGE_DECRYPT, (_event, encryptedBase64: string) => {
if (typeof encryptedBase64 !== 'string' || encryptedBase64.length === 0) {
logger.warn('safe-storage', '解密参数无效');
return { success: false, error: '参数无效:需要非空字符串' };
}
if (!checkAvailability()) {
return { success: false, error: 'safeStorage 不可用' };
}
try {
const buffer = Buffer.from(encryptedBase64, 'base64');
const plaintext = safeStorage.decryptString(buffer);
return { success: true, data: plaintext };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error('safe-storage', '解密失败', err instanceof Error ? err : new Error(message));
return { success: false, error: message };
}
});
logger.info('safe-storage', 'safeStorage IPC 处理器已注册');
}

View File

@@ -0,0 +1,161 @@
// ============================================================
// storage-ipc.ts — 本地存储 IPC 处理器(主进程)
//
// 职责:
// - 文件写入/读取/删除Buffer 基础操作)
// - 递归创建目录
// - shell.showItemInFolder右键"在文件夹中查看"
// - 获取应用数据目录路径
// - 所有操作在 heixiu-data/ 沙箱内进行
//
// 安全:
// - 路径校验:拒绝相对路径中的 ../ 穿越
// - 所有写入操作 base 于 getDataDir()
// ============================================================
import { ipcMain, shell, app } from 'electron';
import fs from 'node:fs';
import path from 'node:path';
import { BIDIRECTIONAL } from '../../../shared/constants/ipc-channels';
import { logger } from '../logger';
// ---------- 内部工具 ----------
/** 获取 heixiu-data 根目录 */
function getDataDir(): string {
return path.join(app.getPath('userData'), 'heixiu-data');
}
/**
* 将相对路径解析为绝对路径,并校验是否在 dataDir 内。
* 防止路径穿越攻击。
*/
function resolveSafe(dataDir: string, relativePath: string): string {
const resolved = path.resolve(dataDir, relativePath);
if (!resolved.startsWith(dataDir + path.sep) && resolved !== dataDir) {
throw new Error(`路径越界:${relativePath}`);
}
return resolved;
}
/** 确保目录存在(递归创建) */
function ensureDir(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
// ---------- 注册 IPC 处理器 ----------
export function registerStorageIpcHandlers(): void {
const dataDir = getDataDir();
ensureDir(dataDir);
logger.info('app', `本地存储目录:${dataDir}`);
// ---- 获取数据目录 ----
ipcMain.handle(BIDIRECTIONAL.GET_DATA_DIR, () => {
return { success: true, data: dataDir };
});
// ---- 文件写入 ----
ipcMain.handle(
BIDIRECTIONAL.FILE_WRITE,
async (
_event,
params: { relativePath: string; data: string; encoding?: 'base64' | 'utf8' },
) => {
try {
const filePath = resolveSafe(dataDir, params.relativePath);
ensureDir(path.dirname(filePath));
const buffer = Buffer.from(params.data, params.encoding || 'base64');
fs.writeFileSync(filePath, buffer);
console.log(`[storage IPC] 文件写入成功:${filePath}${buffer.length} bytes`);
return { success: true, data: filePath };
} catch (err) {
const msg = (err as Error).message;
console.error(`[storage IPC] 文件写入失败:${params.relativePath}${msg}`);
logger.warn('storage', `文件写入失败:${params.relativePath}`, err instanceof Error ? err : undefined);
return { success: false, error: msg };
}
},
);
// ---- 文件读取(返回 Base64 ----
ipcMain.handle(BIDIRECTIONAL.FILE_READ, async (_event, params: { relativePath: string }) => {
try {
const filePath = resolveSafe(dataDir, params.relativePath);
if (!fs.existsSync(filePath)) {
return { success: false, error: '文件不存在' };
}
const buffer = fs.readFileSync(filePath);
return { success: true, data: buffer.toString('base64') };
} catch (err) {
logger.warn('storage', `文件读取失败:${params.relativePath}`, err instanceof Error ? err : undefined);
return { success: false, error: (err as Error).message };
}
});
// ---- 文件删除 ----
ipcMain.handle(BIDIRECTIONAL.FILE_DELETE, async (_event, params: { relativePath: string }) => {
try {
const filePath = resolveSafe(dataDir, params.relativePath);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
return { success: true };
} catch (err) {
logger.warn('storage', `文件删除失败:${params.relativePath}`, err instanceof Error ? err : undefined);
return { success: false, error: (err as Error).message };
}
});
// ---- 创建目录 ----
ipcMain.handle(BIDIRECTIONAL.MAKE_DIR, async (_event, params: { relativePath: string }) => {
try {
const dirPath = resolveSafe(dataDir, params.relativePath);
ensureDir(dirPath);
return { success: true, data: dirPath };
} catch (err) {
logger.warn('storage', `目录创建失败:${params.relativePath}`, err instanceof Error ? err : undefined);
return { success: false, error: (err as Error).message };
}
});
// ---- 在文件管理器中显示 ----
ipcMain.handle(BIDIRECTIONAL.SHOW_ITEM_IN_FOLDER, async (_event, params: { filePath: string }) => {
try {
shell.showItemInFolder(params.filePath);
return { success: true };
} catch (err) {
logger.warn('storage', `showItemInFolder 失败:${params.filePath}`, err instanceof Error ? err : undefined);
return { success: false, error: (err as Error).message };
}
});
// ---- 文件大小 ----
ipcMain.handle(BIDIRECTIONAL.FILE_SIZE, async (_event, params: { relativePath: string }) => {
try {
const filePath = resolveSafe(dataDir, params.relativePath);
if (!fs.existsSync(filePath)) {
return { success: false, error: '文件不存在' };
}
const stat = fs.statSync(filePath);
return { success: true, data: stat.size };
} catch (err) {
return { success: false, error: (err as Error).message };
}
});
// ---- 文件/目录是否存在 ----
ipcMain.handle(BIDIRECTIONAL.FILE_EXISTS, async (_event, params: { relativePath: string }) => {
try {
const filePath = resolveSafe(dataDir, params.relativePath);
return { success: true, data: fs.existsSync(filePath) };
} catch {
return { success: true, data: false };
}
});
logger.info('app', '存储 IPC 处理器已注册');
}