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:
lofyer
2026-08-04 16:19:06 +08:00
parent 522b0f74a5
commit 0fd7c59e08
68 changed files with 11721 additions and 301 deletions
+71
View File
@@ -219,6 +219,77 @@ $('semanticKeyClearBtn').onclick = async () => {
refreshSemanticKeyStatus();
let orphanFound = null;
function describeOrphans(data) {
const notes = data.notes || [];
const annotations = data.annotations || [];
if (!notes.length && !annotations.length) return '没有残留数据';
const parts = [];
if (notes.length) {
const noteTotal = notes.reduce((sum, item) => sum + item.notes, 0);
parts.push(`${notes.length} 本已移除书籍留有阅读资料(含 ${noteTotal} 条笔记)`);
}
if (annotations.length) {
const mb = (data.totalBytes || 0) / 1024 / 1024;
const size = mb >= 0.1 ? `${mb.toFixed(1)} MB` : `${Math.round((data.totalBytes || 0) / 1024)} KB`;
parts.push(`${annotations.length} 份孤立批注(约 ${size}`);
}
return parts.join('');
}
$('orphanScanBtn').onclick = async () => {
const btn = $('orphanScanBtn');
btn.disabled = true;
btn.textContent = '检查中...';
const r = await window.api.reader.orphanReport();
btn.disabled = false;
btn.textContent = '检查';
if (!r || !r.ok) {
$('orphanStatus').textContent = `检查失败:${(r && r.error) || '未知错误'}`;
return;
}
orphanFound = r.data;
$('orphanStatus').textContent = describeOrphans(r.data);
const hasAny = (r.data.notes || []).length > 0 || (r.data.annotations || []).length > 0;
$('orphanPurgeBtn').classList.toggle('hidden', !hasAny);
};
$('orphanPurgeBtn').onclick = async () => {
if (!orphanFound) return;
const notes = orphanFound.notes || [];
const annotations = orphanFound.annotations || [];
// 笔记是用户创作,删除不可撤销,必须让用户单独确认这一项
const lines = [];
if (annotations.length) lines.push(`<li>${annotations.length} 份孤立批注</li>`);
if (notes.length) {
const noteTotal = notes.reduce((sum, item) => sum + item.notes, 0);
lines.push(`<li>${notes.length} 本已移除书籍的阅读资料,含 <b>${noteTotal} 条笔记</b></li>`);
}
const choice = await openModal('清理残留阅读资料', `
<p>检查到:</p>
<ul class="library-bulk-preview">${lines.join('')}</ul>
<p style="margin-top:8px"><label><input type="checkbox" id="orphanDelAnnotations" checked /> 清理孤立批注</label></p>
${notes.length ? `<p style="margin-top:6px"><label><input type="checkbox" id="orphanDelNotes" /> 同时删除笔记、书签与进度</label></p>
<p class="muted" style="margin-top:6px">这些笔记目前仍可在「我的笔记」中查看,删除后无法恢复。</p>` : ''}
`, () => ({
annotations: !!(document.getElementById('orphanDelAnnotations') || {}).checked,
notes: !!(document.getElementById('orphanDelNotes') || {}).checked
}));
if (!choice) return;
if (!choice.annotations && !choice.notes) return;
const r = await window.api.reader.purgeOrphans(choice);
if (!r || !r.ok) {
await confirmModal('清理失败', (r && r.error) || '未知错误');
return;
}
const done = r.data || {};
$('orphanStatus').textContent =
`已清理 ${done.annotationsRemoved || 0} 份批注、${done.notesRemoved || 0} 本阅读资料`;
$('orphanPurgeBtn').classList.add('hidden');
orphanFound = null;
};
const AI_PROTOCOL_INFO = {
anthropic: {
label: 'Anthropic',