笔记独立窗口从「一窗一条」改为单窗口多标签,与阅读器一致: 标签集在主进程侧为权威,notes:tabsChanged 只能收窄不能新增, 否则渲染层可以谎报持有某条笔记来越权读取。存活编辑器上限 3 并 LRU 回收,回收前序列化未保存内容。笔记没有自动保存,关标签与 关窗都做二次确认,取消关闭必须回报主进程复位 closePending, 否则窗口再也关不掉而看门狗仍会销毁未保存内容。 AI 助手支持多轮会话:会话独立落盘,先取历史再写提问, 历史只发文本不重发图像,失败与取消都保留已流出的残片。 新增 TXT/MD 内置阅读(转内存 EPUB 复用 epub 渲染管线), 补上渲染层遗漏的可阅读格式白名单:主进程本就放行 txt/md, 但渲染层另有两份白名单漏了,表现为卡片上没有「阅读」按钮。 书库卡片封面改用 contain 完整显示,留白由同图模糊层垫底, 修正不同比例封面被裁切程度不一导致的观感不一致;多选复选框 去掉衬底色块,恢复原生外观。 其余:PDF 画质档位与画布尺寸钳制、原子写入、笔记资源托管、 GitHub Pages 站点。
79 lines
2.5 KiB
JavaScript
79 lines
2.5 KiB
JavaScript
// 元数据落盘的统一入口。
|
|
//
|
|
// rename 只保证目录项替换是原子的,不保证被替换的数据已经到磁盘:
|
|
// 断电后可能出现"改名成功但文件内容是一段空洞"的结果,长度正常、内容全零。
|
|
// 因此改名前必须先 fsync 数据本身,改名后再 fsync 目录让目录项落盘。
|
|
// Windows 不允许对目录取句柄,那一步失败时忽略即可。
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
function writeSynced(file, data, encoding = 'utf8') {
|
|
const fd = fs.openSync(file, 'w');
|
|
try {
|
|
fs.writeFileSync(fd, data, encoding === null ? undefined : { encoding });
|
|
fs.fsyncSync(fd);
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
}
|
|
|
|
function syncDirectory(dir) {
|
|
let fd = null;
|
|
try {
|
|
fd = fs.openSync(dir, 'r');
|
|
fs.fsyncSync(fd);
|
|
} catch (e) {
|
|
// Windows 无法 fsync 目录;其它平台失败也不该让写入整体失败
|
|
} finally {
|
|
if (fd !== null) {
|
|
try { fs.closeSync(fd); } catch (e) { /* ignore */ }
|
|
}
|
|
}
|
|
}
|
|
|
|
// 写入 dest,保留一份 .bak 以便下次读取时恢复。
|
|
// 失败时清理临时文件,并在目标已被改走时把备份换回去。
|
|
function writeJson(dest, value) {
|
|
const temp = `${dest}.tmp`;
|
|
const backup = `${dest}.bak`;
|
|
let backedUp = false;
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
try {
|
|
writeSynced(temp, JSON.stringify(value, null, 2));
|
|
if (fs.existsSync(backup)) fs.unlinkSync(backup);
|
|
if (fs.existsSync(dest)) {
|
|
fs.renameSync(dest, backup);
|
|
backedUp = true;
|
|
}
|
|
fs.renameSync(temp, dest);
|
|
syncDirectory(path.dirname(dest));
|
|
if (backedUp) {
|
|
try { fs.unlinkSync(backup); } catch (e) { /* 保留备份不影响写入结果 */ }
|
|
}
|
|
} catch (e) {
|
|
try { if (fs.existsSync(temp)) fs.unlinkSync(temp); } catch (cleanup) { /* ignore */ }
|
|
try {
|
|
if (backedUp && !fs.existsSync(dest) && fs.existsSync(backup)) fs.renameSync(backup, dest);
|
|
} catch (rollback) { /* 下次读取时从 .bak 恢复 */ }
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// 二进制落地(封面等)。已存在则不覆盖,由调用方决定语义。
|
|
function writeBytesExclusive(dest, bytes) {
|
|
const temp = `${dest}.tmp`;
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
const fd = fs.openSync(temp, 'wx');
|
|
try {
|
|
fs.writeFileSync(fd, bytes);
|
|
fs.fsyncSync(fd);
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
fs.renameSync(temp, dest);
|
|
syncDirectory(path.dirname(dest));
|
|
}
|
|
|
|
module.exports = { writeJson, writeSynced, syncDirectory, writeBytesExclusive };
|