feat: 笔记独立窗口改为多标签,AI 多轮会话与 TXT/MD 阅读
笔记独立窗口从「一窗一条」改为单窗口多标签,与阅读器一致: 标签集在主进程侧为权威,notes:tabsChanged 只能收窄不能新增, 否则渲染层可以谎报持有某条笔记来越权读取。存活编辑器上限 3 并 LRU 回收,回收前序列化未保存内容。笔记没有自动保存,关标签与 关窗都做二次确认,取消关闭必须回报主进程复位 closePending, 否则窗口再也关不掉而看门狗仍会销毁未保存内容。 AI 助手支持多轮会话:会话独立落盘,先取历史再写提问, 历史只发文本不重发图像,失败与取消都保留已流出的残片。 新增 TXT/MD 内置阅读(转内存 EPUB 复用 epub 渲染管线), 补上渲染层遗漏的可阅读格式白名单:主进程本就放行 txt/md, 但渲染层另有两份白名单漏了,表现为卡片上没有「阅读」按钮。 书库卡片封面改用 contain 完整显示,留白由同图模糊层垫底, 修正不同比例封面被裁切程度不一导致的观感不一致;多选复选框 去掉衬底色块,恢复原生外观。 其余:PDF 画质档位与画布尺寸钳制、原子写入、笔记资源托管、 GitHub Pages 站点。
This commit is contained in:
+43
-19
@@ -4,6 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const atomic = require('../atomic-file');
|
||||
|
||||
const VERSION = 6;
|
||||
const STANDALONE_ENTRY_ID = 'system:standalone-notes';
|
||||
@@ -792,24 +793,7 @@ function migrate(raw) {
|
||||
}
|
||||
|
||||
function save() {
|
||||
const dest = getFilePath();
|
||||
const temp = `${dest}.tmp`;
|
||||
const backup = `${dest}.bak`;
|
||||
let backedUp = false;
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
try {
|
||||
fs.writeFileSync(temp, JSON.stringify(cache, null, 2), 'utf8');
|
||||
if (fs.existsSync(backup)) fs.unlinkSync(backup);
|
||||
if (fs.existsSync(dest)) { fs.renameSync(dest, backup); backedUp = true; }
|
||||
fs.renameSync(temp, dest);
|
||||
if (backedUp) { try { fs.unlinkSync(backup); } catch (e) { /* ignore */ } }
|
||||
} 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) { /* 下次 load 时恢复 */ }
|
||||
throw e;
|
||||
}
|
||||
atomic.writeJson(getFilePath(), cache);
|
||||
}
|
||||
|
||||
function load() {
|
||||
@@ -1345,6 +1329,46 @@ function removeCollection(collectionId) {
|
||||
});
|
||||
}
|
||||
|
||||
// 与书库对账:列出书库里已不存在的条目。这些阅读资料在「我的笔记」里仍然可见,
|
||||
// 属于有意保留,因此只报告不自动删除,由用户显式决定。
|
||||
function orphanReport(knownIds) {
|
||||
const known = new Set((Array.isArray(knownIds) ? knownIds : []).map((id) => String(id)));
|
||||
const c = load();
|
||||
const orphans = [];
|
||||
for (const [entryId, entry] of Object.entries(c.entries)) {
|
||||
if (entryId === STANDALONE_ENTRY_ID) continue;
|
||||
if (known.has(entryId)) continue;
|
||||
const notes = Array.isArray(entry.notes) ? entry.notes.length : 0;
|
||||
const bookmarks = Array.isArray(entry.bookmarks) ? entry.bookmarks.length : 0;
|
||||
const snapshot = entry.book || {};
|
||||
orphans.push({
|
||||
entryId,
|
||||
title: snapshot.title || '',
|
||||
notes,
|
||||
bookmarks,
|
||||
hasProgress: !!entry.progress
|
||||
});
|
||||
}
|
||||
orphans.sort((a, b) => b.notes - a.notes || a.entryId.localeCompare(b.entryId));
|
||||
return orphans;
|
||||
}
|
||||
|
||||
function forgetMany(entryIds) {
|
||||
const ids = (Array.isArray(entryIds) ? entryIds : []).map((id) => safeId(id, '条目 ID'));
|
||||
if (!ids.length) return 0;
|
||||
let removed = 0;
|
||||
mutateCache((current) => {
|
||||
for (const id of ids) {
|
||||
if (id === STANDALONE_ENTRY_ID) continue;
|
||||
if (!Object.prototype.hasOwnProperty.call(current.entries, id)) continue;
|
||||
delete current.entries[id];
|
||||
removed++;
|
||||
}
|
||||
return removed > 0;
|
||||
});
|
||||
return removed;
|
||||
}
|
||||
|
||||
// forget 是显式删除:只清掉指定条目的阅读数据,不影响其它条目或笔记本。
|
||||
function forget(value) {
|
||||
const id = safeId(value, '条目 ID');
|
||||
@@ -1363,5 +1387,5 @@ module.exports = {
|
||||
addNote, addStandaloneNote, updateNote, removeNote, listNotes, getNoteCounts,
|
||||
noteAssetIds,
|
||||
listCollections, addCollection, updateCollection, removeCollection,
|
||||
forget
|
||||
orphanReport, forgetMany, forget
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user