旧稿曾为 thus-note 安排一段很顺的起源:使用现成工具遇到阻碍,于是决定做一个更轻的笔记应用;打开就写,写完就走,云同步安静地待在后面。公开仓库没有留下这些生活场景和使用效果的证据,这段叙述也把上游项目、后续维护和产品设想混在了一起。
按提交重新整理后,能够确认的起点更具体。thus-note 基于 Liubai 二次开发,继续使用 AGPL-3.0-or-later。首次公开提交完成代码导入和品牌替换,之后的修改逐步落到认证、空间、同步、导入、缓存与部署。仓库里仍保留一些 liu 前缀;它们记录着继承关系,也提醒我,改名不等于从零重写。
这次只沿着数据链复盘:一段内容写入浏览器以后,怎样在下次打开时出现,怎样迁出,怎样同步,应用升级以后又怎样避开旧缓存。输入框提供开始记录的入口,长期维护则要回答数据能否回来。
先把来源写在功能前面
项目的 README 和 NOTICE 都保留了 Liubai 的来源,根目录也包含 AGPL 许可证。数据模型、编辑器、同步框架和大量界面代码具有明确的继承关系。当前维护能够认领的是品牌迁移、自建后端、认证、部署和后续修复,不能把整套产品归为当前维护者的独立原创。
旧稿用一次公开提交承接了设计、开发和日常使用,结果既抹掉上游,也让维护过程失去边界。二次开发需要先理解既有概念,再决定哪些沿用、哪些迁移、哪些暂时不动。代码里没有彻底改掉的旧命名,本身就是来源证据,无需再用一段原创故事覆盖。
归属也会影响发布。AGPL 对通过网络提供修改版本有要求,仓库里的第三方组件、图片和服务各有许可。因此本文只讨论固定提交中的代码与维护判断,不复用仓库截图、GIF、Logo 或现有文章的泛化封面。来源明确后,每一项改动才有可核对的责任范围。
本地数据首先带来迁移责任
Web 端数据不是一组纯文本文件。固定提交使用 Dexie 封装 IndexedDB,数据库名为 ThusNoteDatabase,其中包含用户、空间、成员、草稿、内容、收藏、下载任务和上传任务等表。旧稿把浏览器数据库描述成普通文本工具可以直接读取的文件,这与当前实现不符。
数据库版本已经到 76。源码相邻注释明确提醒:新增表或修改索引时需要提升版本;曾经定义的表若不再出现在 stores() 中,升级时可能被删除。数据留在浏览器,并没有免去 schema 演进的责任。一次重命名、索引调整或旧表清理,都可能改变下次打开页面时还能看到什么。
表结构还说明,本地与云端没有被拆成两个独立模式。内容和草稿旁边保存上下行任务,索引覆盖状态、编辑时间、删除时间、提醒时间、标签与工作区。输入动作结束以后,数据已经进入一套需要迁移纪律的模型。
固定提交能够通过类型检查和构建,仓库却没有自动化用例回答版本 75 升到 76 会保留哪些记录、升级中途关闭页面会怎样、旧索引删除后能否回滚、附件与正文是否仍然对应。版本号只能证明结构多次变化,不能充当迁移质量证明。
导出文件是可检查的出口
应用提供 JSON 和 Markdown 两种导出。代码只读取当前工作区中状态正常的内容,按创建时间分页,再生成 ZIP。Markdown 路径会把编辑器内容转换成文本,并把图片和文件写入相邻的 assets 目录;JSON 保留内容字段和附件元数据,同时移除内存中的二进制数据与云端 URL。
这条路径至少让数据离开当前数据库,也为附件规定了落盘位置。限制同样清楚:导出范围只有当前工作区,数量受配置上限约束,附件只有在本地仍存在 arrayBuffer 时才会写进压缩包。若某个附件只剩云端地址,当前实现不会先下载再打包。
导入会校验 ID、空间和内容类型,再处理 ZIP 中的附件。内容进入当前账号时,归属会改为当前用户、成员和工作区;本地不存在的记录标记为新增,导入版本的 updatedStamp 更新时才进入覆盖分支,其余记录保持不变。确认后,新增与更新记录通过 bulkPut 写入 IndexedDB,搜索字段也会重建。
这些分支仍不等于完整恢复。源码保留 profile 上下文的 TODO,确认后的批量写入没有展示跨正文与附件的事务回滚,仓库也没有自动执行一次“导出、清空、导入、逐条比对”。出口存在,但另一台设备能否恢复相同工作区、附件和关系,仍缺一份脱敏的往返回执。
我过去更关注输入是否够快,现在需要先确认出口是否可验证。界面可以更换,数据迁出失败会直接破坏“本地优先”的承诺。
同步链路保留了尚未收口的折中
同步代码先把本地改动包装成上传任务。任务从 waiting 变为 syncing,请求失败后退回等待;下行请求会合并短时间内的任务,再按 taskId 分发各自结果。这些状态表明代码考虑了网络失败与批量请求,但没有证明重复提交、进程中断和冲突合并都能恢复。
5 月 6 日的一组连续提交把折中记录得更清楚。后端加入 client_key 候选存储:当前 key 与历史 key 在 Redis 中保留七天,最多取五个去重候选逐一尝试;全部无法解密时返回 401,要求客户端重新登录。随后,前端三个同步入口从加密封装改为直接发送 atoms,提交说明将原因写为绕过当时的加密同步问题。
这里不能据此判断传输层缺乏加密。应用层不再包装 payload,不代表 HTTPS 一定缺席。固定提交能支持的结论是:解密兼容路径与直接 atoms 路径同时存在,原先的应用层加密契约没有收口。后端根路由还兼容多种字段,另有独立的 /get 与 /set 路由;接口族越多,越需要用测试固定行为差异。
日志边界也需要处理。前端上行代码会把 atoms 和响应写到控制台;后端解密路径会打印 key 前缀、IV、用户标识与错误细节,部分结果也会整体输出。包含私人笔记的系统不应长期保留这些调试范围。后续需要先定义允许记录的字段,再移除 payload、密钥材料和内容。
让同步先恢复工作可以是一次临时选择,但临时路径需要退出条件。当前还缺少一套能够回答何时恢复加密封装、旧客户端怎样兼容、失败后怎样重试、日志允许留下什么的测试与迁移计划。
离线应用也要处理旧版本
本地数据之外,PWA 还会缓存应用文件。固定提交把 Service Worker 注册改为 autoUpdate,安装时执行 skipWaiting,激活时调用 clients.claim。Nginx 配置要求 index.html、Service Worker 和 manifest 不缓存,让入口及时获取新版本。
这些修改会与本地数据相遇。浏览器长期运行旧脚本时,新页面、旧 schema 和旧同步协议可能同时存在;更新过快,又可能在编辑期间接管页面。skipWaiting 和禁止缓存可以缩短旧版本停留时间,却没有回答编辑中的草稿怎样保存、迁移失败怎样提示、多个标签页是否同步切换。
8 月 9 日在固定 HEAD 的隔离副本中,前端完成 vue-tsc 和 Vite/PWA 构建,Service Worker 生成 264 项预缓存清单。构建也报告第三方调试库使用 eval、本地缓存模块同时静态与动态导入、主 chunk 超过 500 kB。它们没有阻断产物生成,但仍属于需要保留的构建结果。
绿色构建没有覆盖数据回程
同一固定提交的后端 TypeScript 编译通过。GitHub Actions 在 5 月 6 日成功构建前后端、打包并创建 Release;本轮使用冻结锁文件重新安装后,两个构建也再次通过。现有证据可以确认代码在 Node.js 22 环境中能够产出前端静态文件、Service Worker 和后端 JavaScript。
测试门禁没有同步建立。后端 package.json 声明 jest,依赖和锁文件却没有 Jest,执行 pnpm test 得到 jest: command not found。tests 目录中的脚本多数依赖数据库、账号或运行中的服务,TypeScript 配置又将该目录排除在编译范围外。远端工作流也只有安装、构建和发布,没有测试步骤。
因此当前只能确认构建链可重复,数据链还没有同等强度的回归保护。下一轮可以从一份不含私人内容的固定 fixture 开始,验证 IndexedDB 升级前后记录数量与附件哈希;再执行 JSON/Markdown 导出、清空、导入和逐字段比对;最后模拟一次 key 过期、401、重新登录与同步重试。PWA 还需要保留两个相邻版本,在真实浏览器中验证旧缓存怎样退出。这些都是待完成的测试计划,不是现有结果。
仓库还跟踪了几个 .env 文件。本轮没有打开它们,无法判断其中是否存在应撤销的值。公开发布项目前,需要先完成不暴露内容的独立 secret scan,并检查同步日志与 Release 产物。文章可以记录代码现状,不能替仓库安全审计签字。
写下以后还缺一张回执
“打开就写”仍可作为入口目标,但它不承担数据可靠性的结论。写下以后,IndexedDB 要能够升级,附件要跟随导出,旧内容要能够导入,网络失败要留下可重试状态,客户端更新也不能让旧数据失去解释方式。
固定提交已经包含本地表、导入分流、导出压缩包、上下行任务、key 候选和 PWA 更新。未完成项也同样明确:应用层加密被绕过,payload 日志范围过宽,测试命令不可运行,恢复与升级没有往返验证。
下一次整理 thus-note,应先让一份脱敏数据完整地导出、清空、导入,再逐条检查记录和附件。那份回执出现以前,这篇文章可以作为本地最终稿,生产状态仍是 CONDITIONAL;构建成功只说明软件能够产出,不能替已经写下的内容保证以后仍然找得到。