Update plugin 隐阅盒 v1.4.0#271
Conversation
- feat: 初始化ZTools插件项目 - feat: 实现隐阅盒阅读器插件 - chore(gitignore): add ignore rules - chore: 相关标识重命名 - chore: add changelog - fix: 修复书架导入书籍异常未捕获、完善本地存储数据兼容逻辑 - docs: 更新README - docs: 重写README - feat: 发布1.1.0版本,新增多项功能并修复多个bug - fix(窗口拖动): 修复阅读窗口拖动拉伸卡顿问题 - chore: bump plugin version to 1.1.1 - chore: 重命名部分称呼 - feat(setting): add plain cover option, remove epub cache storage - feat: 新增MOBI电子书格式支持 - feat: 迁移封面和章节缓存到ztools.db - feat: 新增主题模式切换功能,优化深色主题样式 - chore: release v1.3.0, update plugin info and changelog - style: 优化窗口进度显示 - chore: 发布v1.3.1版本,更新功能与修复问题 - chore: bump plugin version to 1.3.1 - fix: 修复多个已知问题并优化部分功能 - style(settings): 优化设置项的封面提示文案 - fix(bookshelf): 修复纯色封面关闭后MOBI封面未恢复的问题 - docs: 更新README中的截图 - feat(bookshelf): 实现拖拽导入书籍功能 - feat(bookshelf): 增加快捷导入命令 - chore: update gitignore and changelog - feat(tips): 增加提示文本 ,更新版本标识 - feat: 新增多选批量操作、重载元数据和恢复封面功能 - style(plugin.json): 调整插件命令列表的排序顺序 - feat: 新增书籍信息弹窗与阅读数据追踪,优化元数据解析 - refactor: 完成bug修复 ### 详细变更 1. 修复MOBI封面解析:将Blob URL替换为可持久化的Base64格式,解决IndexedDB缓存失效问题 2. 优化书籍导入查重逻辑:通过文件名匹配避免重复导入同书籍 3. 重构阅读时长与速度统计:新增会话计时器避免跨会话闲置时间被计入,修复首页阅读时间丢失问题 4. 完善元数据加载容错:文件读取失败时不再静默清空原有元数据,抛出明确错误提示 5. 优化书籍信息编辑交互:新增分类批量添加、快捷键触发功能,修复分类选择UI缺陷 - docs: update README - release: bump version to 1.3.2 and update docs - feat: 发布v1.3.3版本,新增多项功能与优化 - release: bump version to 1.3.3 - refactor(settings): 分离配置和书籍的导入导出,修复配置导入问题 - fix: 修复多项问题并调整阅读定时器通知逻辑 - fix(Settings): 为新书籍数组添加类型声明,修复build错误 - feat: 发布v1.4.0版本,新增书签、已读标记等功能 - feat(bookshelf): 新增全文搜索跳转和批量设置分类功能 - feat(bookshelf): 新增视图切换按钮,增加书记格式标识,优化已读完标识的显示逻辑
There was a problem hiding this comment.
Code Review
This pull request introduces several major features to the hushreader plugin, including a "read finished" marker, a bookshelf statistics bar, bookmarking capabilities with shortcuts, full-text search with jump-to-location, batch category setting, and a card/list view toggle. Key feedback points out a critical XSS vulnerability in the search results rendering, a performance bottleneck in the search loop for large books, a bug where marking a book as unfinished is immediately overridden, a window reference leak on destroy, and a logical improvement for the bookshelf statistics calculation.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function highlightKeyword(text: string, keyword: string): string { | ||
| if (!keyword) return text | ||
| const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | ||
| return text.replace(new RegExp(escaped, 'gi'), '<mark>$&</mark>') | ||
| } |
There was a problem hiding this comment.
在搜索结果展示中,直接使用 v-html 渲染未经过滤的图书内容和搜索关键词,存在跨站脚本攻击(XSS)的安全隐患。如果图书内容中包含恶意的 HTML 标签(例如 <script> 或 <img src=x onerror=...>),在 Electron 环境下运行可能会导致远程代码执行(RCE)等严重安全问题。\n\n建议在进行关键词高亮替换前,先对文本和关键词进行 HTML 实体转义。
function escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '"')\n .replace(/'/g, ''')\n}\n\nfunction highlightKeyword(text: string, keyword: string): string {\n const safeText = escapeHtml(text)\n if (!keyword) return safeText\n const safeKeyword = escapeHtml(keyword)\n const escaped = safeKeyword.replace(/[.*+?^${}()|[\\\/]\\\\]/g, '\\$&')\n return safeText.replace(new RegExp(escaped, 'gi'), '<mark>$&</mark>')\n}
| const kwLower = keyword.toLowerCase() | ||
| const results: typeof searchResults.value = [] | ||
| let pos = 0 | ||
|
|
||
| while (pos < fullText.length) { | ||
| const kwIdx = fullText.toLowerCase().indexOf(kwLower, pos) | ||
| if (kwIdx === -1) break | ||
|
|
||
| let start = 0 | ||
| const beforeKw = fullText.slice(0, kwIdx) | ||
| let lastBoundary = -1 | ||
| let m: RegExpExecArray | null | ||
| boundaryRe.lastIndex = 0 | ||
| while ((m = boundaryRe.exec(beforeKw)) !== null) { | ||
| lastBoundary = m.index + m[0].length | ||
| } | ||
| if (lastBoundary >= 0) { | ||
| start = lastBoundary | ||
| while (start < fullText.length && /[\s\u3000]/.test(fullText[start])) start++ | ||
| } | ||
| if (start > kwIdx) start = 0 |
There was a problem hiding this comment.
在 executeSearch 的 while 循环中,每次迭代都调用了 fullText.toLowerCase()。对于大体积的图书(例如数兆字节的 TXT 或 EPUB),这会导致在每次循环中都重新生成一个巨大的小写字符串,极易造成内存溢出(OOM)和界面卡死。\n\n此外,每次都从索引 0 开始截取 beforeKw 并用正则循环扫描,在匹配项较多时会导致 fullText.toLowerCase() 提取到循环外部,并限制向前查找句子边界的字符窗口(例如最多向前查找 200 个字符),从而将复杂度降低到
const kwLower = keyword.toLowerCase()\n const fullTextLower = fullText.toLowerCase()\n const results: typeof searchResults.value = []\n let pos = 0\n\n while (pos < fullText.length) {\n const kwIdx = fullTextLower.indexOf(kwLower, pos)\n if (kwIdx === -1) break\n\n let start = 0\n const searchStart = Math.max(0, kwIdx - 200)\n const beforeKw = fullText.slice(searchStart, kwIdx)\n let lastBoundary = -1\n let m: RegExpExecArray | null\n boundaryRe.lastIndex = 0\n while ((m = boundaryRe.exec(beforeKw)) !== null) {\n lastBoundary = m.index + m[0].length\n }\n if (lastBoundary >= 0) {\n start = searchStart + lastBoundary\n while (start < fullText.length && /[\\s\\u3000]/.test(fullText[start])) start++\n } else {\n start = searchStart\n }\n if (start > kwIdx) start = searchStart
| function markUnfinished(bookId: string) { | ||
| closeContextMenu() | ||
| bookStore.updateBook(bookId, { finishedAt: undefined }) | ||
| toast('已标记为未读完', 'success') | ||
| } |
There was a problem hiding this comment.
在 markUnfinished 中,仅将 finishedAt 设置为 undefined 是不够的。因为书籍的阅读进度(progressIndex)仍然处于 100% 的完成状态。当用户再次打开该书或触发任何进度保存操作时,saveReadingProgress 会检测到进度为 100% 并立即重新将书籍标记为“已读完”。\n\n建议在标记为未读完时,将 progressIndex 重置为 0(即回到最后一章的起点),以使进度低于 100%,避免被自动重新标记。
function markUnfinished(bookId: string) {\n closeContextMenu()\n bookStore.updateBook(bookId, { \n finishedAt: undefined,\n progressIndex: 0\n })\n toast('已标记为未读完', 'success')\n}
| else if (command === 'close') closePlugin() | ||
| else if (command === 'auto') toggleAutoPaging() | ||
| else if (command === 'close-reader') { isReaderHidden.value = true; blurHushreaderKeyboard() } | ||
| else if (command === 'destroy') { saveReadingProgress(); hushreaderActivated.value = false; stopReadingTimer(); hushreaderWindow?.close?.() } |
There was a problem hiding this comment.
在执行 destroy 命令时,虽然调用了 hushreaderWindow?.close?.(),但并没有将 hushreaderWindow 引用置为 null。\n\n在 ensureHushreaderWindow 中,会通过 !hushreaderWindow.isDestroyed?.() 来判断窗口是否存活。如果底层代理对象不支持 isDestroyed 方法(返回 undefined),!undefined 会评估为 true,导致程序误认为旧窗口依然存活而拒绝创建新窗口,从而导致用户无法再次打开阅读器。\n\n建议在关闭窗口后显式将 hushreaderWindow 置为 null。
else if (command === 'destroy') { saveReadingProgress(); hushreaderActivated.value = false; stopReadingTimer(); hushreaderWindow?.close?.(); hushreaderWindow = null }
| const statsTotal = computed(() => bookStore.books.length) | ||
| const statsRead = computed(() => bookStore.books.filter(b => b.lastReadAt).length) | ||
| const statsReadingTimeMs = computed(() => bookStore.books.reduce((sum, b) => sum + (b.readingTimeMs || 0), 0)) |
There was a problem hiding this comment.
在新增的书架统计栏中,statsRead 当前是通过 b.lastReadAt 来过滤已读图书的。这意味着任何只要被打开过一次的书籍都会被计入“已读数”。\n\n由于本次 PR 引入了完善的“已读完”(finishedAt)标记功能,将“已读数”统计口径修改为“已读完的书籍数量”(即 b.finishedAt 不为空)会更加符合用户的直觉和行业通用标准。
const statsTotal = computed(() => bookStore.books.length)\nconst statsRead = computed(() => bookStore.books.filter(b => b.finishedAt).length)\nconst statsReadingTimeMs = computed(() => bookStore.books.reduce((sum, b) => sum + (b.readingTimeMs || 0), 0))
- feat: 初始化ZTools插件项目 - feat: 实现隐阅盒阅读器插件 - chore(gitignore): add ignore rules - chore: 相关标识重命名 - chore: add changelog - fix: 修复书架导入书籍异常未捕获、完善本地存储数据兼容逻辑 - docs: 更新README - docs: 重写README - feat: 发布1.1.0版本,新增多项功能并修复多个bug - fix(窗口拖动): 修复阅读窗口拖动拉伸卡顿问题 - chore: bump plugin version to 1.1.1 - chore: 重命名部分称呼 - feat(setting): add plain cover option, remove epub cache storage - feat: 新增MOBI电子书格式支持 - feat: 迁移封面和章节缓存到ztools.db - feat: 新增主题模式切换功能,优化深色主题样式 - chore: release v1.3.0, update plugin info and changelog - style: 优化窗口进度显示 - chore: 发布v1.3.1版本,更新功能与修复问题 - chore: bump plugin version to 1.3.1 - fix: 修复多个已知问题并优化部分功能 - style(settings): 优化设置项的封面提示文案 - fix(bookshelf): 修复纯色封面关闭后MOBI封面未恢复的问题 - docs: 更新README中的截图 - feat(bookshelf): 实现拖拽导入书籍功能 - feat(bookshelf): 增加快捷导入命令 - chore: update gitignore and changelog - feat(tips): 增加提示文本 ,更新版本标识 - feat: 新增多选批量操作、重载元数据和恢复封面功能 - style(plugin.json): 调整插件命令列表的排序顺序 - feat: 新增书籍信息弹窗与阅读数据追踪,优化元数据解析 - refactor: 完成bug修复 ### 详细变更 1. 修复MOBI封面解析:将Blob URL替换为可持久化的Base64格式,解决IndexedDB缓存失效问题 2. 优化书籍导入查重逻辑:通过文件名匹配避免重复导入同书籍 3. 重构阅读时长与速度统计:新增会话计时器避免跨会话闲置时间被计入,修复首页阅读时间丢失问题 4. 完善元数据加载容错:文件读取失败时不再静默清空原有元数据,抛出明确错误提示 5. 优化书籍信息编辑交互:新增分类批量添加、快捷键触发功能,修复分类选择UI缺陷 - docs: update README - release: bump version to 1.3.2 and update docs - feat: 发布v1.3.3版本,新增多项功能与优化 - release: bump version to 1.3.3 - refactor(settings): 分离配置和书籍的导入导出,修复配置导入问题 - fix: 修复多项问题并调整阅读定时器通知逻辑 - fix(Settings): 为新书籍数组添加类型声明,修复build错误 - feat: 发布v1.4.0版本,新增书签、已读标记等功能 - feat(bookshelf): 新增全文搜索跳转和批量设置分类功能 - feat(bookshelf): 新增视图切换按钮,增加书记格式标识,优化已读完标识的显示逻辑 - fix: 修复多个bug,重构搜索逻辑优化性能,修复XSS安全隐患、封面残留和多选布局重叠等问题,完善窗口销毁逻辑和书架统计规则

插件信息
本次变更
1.4.0 - 2026-06-23
Added
Changed
Fixed
截图 / 演示
自检清单
plugins/hushreader/目录此 PR 由 ztools-plugin-cli 自动管理:每次
ztools publish在分支上追加一个 commit,PR 链接保持不变。