客户端改动同步记录

从作者 GitHub 原版 1.1.8 出发,到当前这个「能给家里人用」的版本。38 条改动 —— 每一次功能增加、bug 修复、安全加固都记在这里,方便回头看清「改了什么、为什么改、改完什么样」。

起点 R2-Cloud-Drive-Client 1.1.8 · 作者原版 当前 26.9.29 · 2026-09-29 共 38 条改动

关键指标

从「什么都存不进去」到「家里人日常能用」。

版本跨度
1.1.8 → 26.9.29
中间经过 10 个版本节点
改动条目
38 项
新增 10 · 修复 10 · 加固 5
清理 4 · 测试 4 · 其他 5
外部网络依赖
3 处 → 0 处
真实页面 http(s) 资源数 = 0
自动化验证
8 套 + 112 项
verify 8 套 · 真机冒烟 112 项
打包自检 29 项

版本总览

按版本节点看演进。1.2.4 / 1.2.5 / 1.2.6 三个号没单独发过,最后一起打成了 26.9.17。

阶段 版本 日期 主要内容 产物目录
起点 1.1.8 作者原版 GitHub 用户 HandsomeMJZ 的 Electron 客户端。只读参考。 —
一 1.1.8 2026-09-14 修 403(能用了)、便携版数据目录、搭测试底座 release-portable
二 1.1.9 2026-09-16 PDF/Word/Excel 预览 + 解除 32MB 上限、Office HTML 清洗、去作者痕迹 release-portable-final
三 1.2.0 2026-09-16 冒烟测试落地、corsEnabled 修复、外链防护 release-portable-r2
四 1.2.1 ~ 1.2.3 2026-09-16 图标字体本地化、正文改系统字体、真实应用启动验证、灰字提示统一模板 -r3 / -r4 / -r5
五 1.2.4 2026-09-16 同步丢文件修复、两条卡死路径、Office 锁文件、时钟偏差、PPTX 预览 未单独发布
六 1.2.5 ~ 1.2.6 2026-09-16 配置原子写、单实例锁、覆盖串行化、回收站与版本历史(接口层) 未单独发布
七 26.9.17 2026-09-17 首次正式发布(含前面全部 29 条改动),版本号改日期制 release-portable-26.9.17
八 26.9.17-2 2026-09-17 回收站进侧边栏、整目录打包下载、版本历史界面、断点续传、CSP -26.9.17-2b
九 26.9.27 2026-09-27 补齐缺口 36(大文件也有版本历史)+ 重新打包 release-portable 📌 固定
十 26.9.29 2026-09-29 回收站提示去掉空头承诺(改动 38)+ 重新打包 release-portable 📌 固定
拿来时的问题 → 现在还剩几个 致命(等于不能用) 1 → 0 高频痛点 4 → 1 剩下的 1 个是 PPT 版式还原(代价极高,不做) 体验 / 别扭 5 → 0 作者痕迹 4 → 0 安全兜底 0 → 3 新增了 Office 清洗 / 外链防护 / CSP 三道
红色 = 致命或高频,橙色 = 次要。最后一行的 0 → 3 是主动加的三道安全兜底。

改动清单

38 条全列在这里。「影响面」三列表示这个改动动了哪一层。

# 事项 类型 版本 渲染 主进程 测试
1修写操作 403
不修等于不能用
修复1.1.8—✅✅
2便携版数据目录
拷 exe 就带走配置
新增1.1.8—✅—
3测试底座
假服务端 + API/同步测试
测试1.1.8——✅
4同步稳定阈值 2 秒 → 5 秒调整1.1.9—✅—
5修「自己传的文件被当成云端更新」修复1.1.9—✅—
6429 限速提示翻译成人话体验1.1.9—✅—
7预览扩展 + 解除 32MB 上限
PDF/Word/Excel,走自定义协议
新增1.1.9✅✅✅
8Office 预览的 HTML 清洗
防文档里藏脚本
加固1.1.9✅—✅
9移除作者「关于」面板清理1.1.9✅——
10移除「软件更新」面板
会咬人:装回原版覆盖定制
清理1.1.9✅✅—
11冒烟测试落地
在真实 Electron 里跑
测试1.2.0✅✅✅
12修 corsEnabled 漏写
Word/Excel 预览整体失效
修复1.2.0—✅✅
13外链防护(两个导航守卫)加固1.2.0—✅✅
14图标字体本地化
不再等 Google Fonts
加固1.2.1✅—✅
15正文字体改走系统加固1.2.1✅——
16清作者域名残留清理1.2.1✅——
17真实应用启动验证
假 preload 跑真实界面
测试1.2.2✅✅✅
18地址灰字提示统一为模板清理1.2.3✅—✅
19修复同步会丢文件
唯一能真丢本地文件的路径
修复1.2.4—✅✅
20修两条「同步卡死但不报错」修复1.2.4—✅✅
21Office 锁文件检测新增1.2.4—✅✅
22时钟偏差修正新增1.2.4—✅✅
23同步跳过原因细分体验1.2.4✅✅—
24PPTX 预览
零依赖,不引 JSZip
新增1.2.4✅—✅
25配置原子写 + 备份恢复
配置坏了不再静默清空
修复1.2.5—✅✅
26单实例锁修复1.2.5—✅—
27云端覆盖本地串行化修复1.2.5—✅✅
28接上回收站新增1.2.6✅✅✅
29接上版本历史(接口层)新增1.2.6—✅✅
30版本号改日期制 + 打包发布流程26.9.17———
31回收站从弹窗搬进侧边栏改造26.9.17-2✅—✅
32整目录打包下载新增26.9.17-2✅✅✅
33版本历史接上界面 + 60MB + 只留 5 版新增26.9.17-2✅✅✅
34断点续传新增26.9.17-2—✅✅
35加 CSP加固26.9.17-2✅—✅
36补齐缺口 36
大文件(含教学 PPT)也有版本历史
修复26.9.27—✅✅
37修掉冒烟测试里两条失效断言测试26.9.17-2——✅
38回收站文案去掉空头承诺
原文案写「服务端保留 30 天…超过 30 天后由服务端自动清理」,实际既无定时任务、也无「清理 N 天前」入口 —— 改成实话
修复待打包✅——
「渲染 / 主进程 / 测试」是什么意思:渲染 = 你看到的界面(renderer.js、index.html); 主进程 = 管文件、网络、窗口的那一层(main.js、apiClient.js、backupManager.js); 测试 = tools/ 下的验证脚本。测试那一列打勾多,是因为很多改动是先写测试、再改代码。

改动详情

按时间顺序。每条都写了「问题是什么」「怎么改的」「改完什么样」。次要的条目合并在一条里讲。 「怎么改的」和「技术要点」默认收起 —— 想学细节,点标题就展开。

起
作者原版 · 1.1.8 起点

拿到的原版:能登录、能看目录,但什么都存不进去

GitHub 用户 HandsomeMJZ 的 Electron 桌面客户端。主界面、同步框架已经能用,所以决定「最小改造」而不是重写。

拿来时的问题(这是所有改动的出发点)
  • 写操作全部 403 —— 新建文件夹、上传、保存同步任务全被拒。等于不能用。
  • PDF / Word / Excel / PPT 一律「不支持预览」 —— 而教学资料全是这些格式。
  • 预览有 32MB 硬上限 —— 图片/视频/音频走 base64 内联,大视频根本打不开。
  • 便携版数据存在 %APPDATA% —— exe 拷到 U 盘后配置不跟着走,便携就没意义了。
  • 登录被限速时只显示 HTTP 429:{"ok":false,...} 原始文本,看不懂要等多久。
  • 刚保存的文件常被同步跳过(判定「仍在写入」的阈值只有 2 秒)。
  • 自己刚上传的文件,下一轮轮询被报成「云端更新」。
  • 带作者个人信息:头像、昵称、签名、GitHub 链接。
  • 带「软件更新」面板,更新源指向作者仓库 —— 会咬人的坑(见改动 10)。
  • 图标字体从 Google Fonts 取,正文字体依赖 Roboto / Noto Sans SC —— 国内启动慢、还漏 IP。
  • 应用没有设置 CSP。
为什么选它,而不是自己写的那两个

当时手上还有自己写的「阿鹏云盘」。那套有账本 + 删除同步 + 云端拉取目录三件套, 复杂、难维护,两套部署指向同一对目录时还会互相拉扯。这个 GitHub 客户端的主界面和同步框架已经能用, 最小改造比重写划算。

定下的边界 改之前先定原则:主界面不动(自绘界面是它的优点)、同步判断保持简单(不引入账本)、 能用服务端解决的就不在客户端造轮子。这条边界后来一直守住了 —— 回收站和版本历史, 客户端只做界面,快照逻辑全在服务端。
1
2026-09-14 修复 1.1.8

修掉「写操作全部 403」—— 一切的起点

客户端能登录、能列目录,但新建文件夹、上传文件、保存同步任务全部返回 403 CSRF check failed。 查下来不是客户端代码写错了,是服务端有防护、客户端没配合。

问题是什么

服务端对 /api/* 的非安全方法(POST / PUT / DELETE)做同源校验, 靠 Origin / Referer 判断请求是不是从自己的网页发出来的。 浏览器会自动带这两个头,但 Electron 主进程用 Node 发请求,不会带 —— 于是被当成跨站攻击拦掉。

而 /api/login 不做这个校验,所以症状特别有迷惑性: 能登录、能看目录,就是什么都存不进去。

同一个接口,两种发法,两种结果 浏览器发请求 Origin / Referer 自动带上 ✅ 通过 Electron 发请求 Node 发请求,两个头都不带 ❌ 403 补一个自定义头 X-R2Drive-CSRF: same-origin ✅ 通过 服务端在校验顺序的最后会回退看这个自定义头 —— 那是它给「非浏览器客户端」留的后门。
服务端一行都不用改,纯客户端修复。
怎么改的
  • 客户端把 cookieHeaders() 改名成 authHeaders(),统一在里面对同源请求附加 X-R2Drive-CSRF: same-origin
  • 跨域请求不带 —— 比如分布式上传要直连外部存储节点,带本站的 CSRF 头没意义
  • 重定向到别的域名时,headersForRedirect() 会把这个头去掉
改完什么样

上传、新建文件夹、保存同步任务全部恢复正常。新增回归测试 tools/verify-api-client.js: 内置一个假服务端复刻 401/403 规则,既测「修复后能过」,也测「还原成旧请求头确实会被 403 拦」(对照组)。

技术要点 ① 症状会骗人:「能登录、能看目录」让人以为网络是通的, 其实只有不做校验的那个接口通。「部分能用」比「完全不能用」更难查。
② 根因在服务端的防护,但修法在客户端:能不动服务端就不动 —— 少一处改动,少一处风险。
③ 这个修复对后来的 0915 服务端同样有效:0915 改了校验顺序(Origin → Referer → Sec-Fetch-Site), 但 Electron 三个都不带,仍然走同一个回退分支。
改动 1 处(apiClient.js) 新增回归 verify-api-client.js 服务端改动 0 行
2
2026-09-14 新增 测试 1.1.8

让便携版真的便携 + 搭起测试底座

两件事一起做:数据目录跟着 exe 走;同时建起一套能重复跑的自动化验证 —— 后面所有测试都挂在它上面。

问题是什么
  • 便携版的配置和登录状态存在 %APPDATA%,exe 拷到 U 盘后是「干净」的 —— 要重新登录、重新配同步任务,便携就没意义了
  • 客户端改坏了不好发现:没有测试,每次改完只能靠人肉点。而服务端有 CSRF 校验、有 401/403 规则, 靠真实服务端测既慢又不稳定
怎么改的

便携数据目录:electron-builder 打 portable 包时会注入 PORTABLE_EXECUTABLE_DIR(exe 所在目录)。 检测到它就 app.setPath('userData', path.join(dir, 'data'))。 留了开关 R2DRIVE_PORTABLE_DATA=0 可关闭;目录不可写时(比如只读 U 盘)自动回退到默认位置。

测试底座:新增 tools/mock-server.js(Node 原生 http 起的假服务端, 复刻真实的 CSRF 校验规则、401/403 响应、目录索引、上传/下载接口), 以及 verify-api-client.js、verify-backup-sync.js。

改完什么样

exe + data/ 一起拷走,配置跟着走,真正便携。测试这边有了可重复的自动化回归 —— 这个底座一直用到今天,后续所有验证脚本都挂在它上面。

技术要点 ① 假服务端比真服务端更好用:能造出真实环境很难复现的情况(超时、403、坏响应), 而且跑得快、不受网络影响。
② 写测试时踩的第一个坑:测试里要把文件的 mtime 回拨, 否则会被「仍在写入」的判断跳过 —— 这个坑后来每次写同步测试都要记得。
新增 3 个测试文件 便携数据目录 可随 U 盘迁移
7
2026-09-16 新增 加固 1.1.9

预览扩展 + 解除 32MB 上限 —— 用自定义协议替掉 base64

新增 PDF / Word / Excel 预览,同时取消预览的 32MB 硬上限,大视频能直接播、能拖进度条。 这两件事在技术上是同一个改法。

问题是什么
  • PDF / Word / Excel / PPT 一律「不支持预览」 —— 用户原话「你最痛的点」,教学资料全是这些格式
  • 图片/视频/音频原来走 base64 data URL(把整个文件读成字符串内联进页面), 内存翻倍,超 32MB 直接失败。大视频根本打不开,更别说拖进度条
同一个文件,两种给浏览器的方式 旧:base64 内联 整个文件 读进内存 · ×1.33 塞进页面 → 超 32MB 就崩 新:r2drive:// 自定义协议 <img src="…"> 主进程按需取流 边取边播 → 无上限 · 能拖进度条
base64 是「先全部搬进内存」,自定义协议是「用多少取多少」。
怎么改的

主进程:用 protocol.registerSchemesAsPrivileged() 把 r2drive 注册成特权协议 (standard / secure / supportFetchAPI / stream / bypassCSP), 必须在 app ready 之前调用。然后 protocol.handle('r2drive', ...) 把 URL 解出远端路径, 带上 Cookie 转发到服务端的 /api/download?inline=1, 原样透传状态码、Content-Type、Content-Range、Accept-Ranges。

渲染层:图片/视频/音频/PDF 一律直接引用协议 URL —— 不下载、不占内存、无上限、可拖进度条; 元素出错时自动退回老的 base64 预览(≤32MB)作为兜底。 Word 用 mammoth 解析 docx;Excel 用 SheetJS 解析,带多工作表页签; 文本/代码预览改成流式读取,最多读 2MB(原来无上限地读整个文件)。

顺手做的一件必须做的事(改动 8):新增 sanitizeHtml(), 对 mammoth / SheetJS 解析出来的 HTML 做白名单清洗。因为那段 HTML 来自用户上传的文档, 直接 innerHTML 等于把文档里藏的 <script>、onclick 搬进渲染进程执行 —— 而渲染进程能调 window.r2Drive 删你的文件。

清洗丢掉的:script / style / iframe / object / embed / form / input / link / meta / svg / math, 所有 on* 事件属性,以及 javascript: / vbscript: / data:text/html 开头的 URL。 保留的:正文、表格结构、data:image 内嵌图。
改完什么样
  • PDF 用 Chromium 内置查看器打开,自带翻页/缩放/搜索,省掉了约 4MB 的 pdf.js
  • 大视频能播、能拖进度条,不再占内存
  • 解析库从 CDN 改成内置本地文件(mammoth 627KB、xlsx 861KB),按需懒加载,离线可用
  • 清洗的验证方式:13 项断言 + 真的把清洗结果注入 DOM,确认 window.__PWNED__ 仍是 undefined
技术要点 ① 能用平台内置能力,就别引库:PDF 用 Chromium 自带的(省 4MB),ZIP 用 DecompressionStream(见改动 24)。
② 三处与方案的有意偏离(都在文档里记了原因):用 Chromium 内置 PDF 查看器而不内置 pdfjs-dist; csv 保持纯文本预览;只有 docx 走 mammoth,旧版二进制 .doc / .odt 降级为下载 —— mammoth 读不了它们,硬走会报错。
③ 安全这一条是方案里没写、但必须做的:「能预览」和「能安全预览」是两件事。 后来「应用没设 CSP」这个遗留项能暂时接受,理由正是文档 HTML 这条路径已经被兜住了。
新增 pptx 之外的全部 Office 预览 上限 32MB → 无 清洗断言 13 项 + 真注入 DOM
10
2026-09-16 清理 1.1.9

移除「软件更新」面板 —— 所有清理里最要紧的一条,因为它会咬人

作者留了个「检查更新」功能,更新源指向他自己的仓库。保留的话,它会提示你「有新版本」, 而你点下载,装回来的是官方原版。

问题是什么

装回官方原版意味着:预览扩展、403 修复、便携数据目录全部丢失, 直接退回那个「什么都传不上去」的状态。而且用户很难意识到「更新」是罪魁祸首 —— 他会以为是「新版本有 bug」。

怎么改的

整块移除,涉及 7 个文件:index.html(面板 + 弹窗)、 renderer.js(state.update、8 个元素引用、6 个事件监听、9 个函数)、 main.js(3 个常量、2 个变量、15 个函数、3 个 IPC 通道)、 preload.js(4 个接口)、apiClient.js(3 个配置项)、styles.css(.update-* 全部规则)。

删除时特别留意别误删名字近似的东西:.sync-update-* 和 #syncUpdateModal 是多端文件同步的弹窗,跟软件更新无关;updateSelectionVisuals / updateActionBar / updateFileViewModeTools 是「更新 UI 状态」的函数,也无关。这些全部保留。

改完什么样

定制版不会再被官方版覆盖。删除后做了全量扫描确认无残留。 顺带发现一个上游遗留问题:styles.css 全文括号差值是 -1(第 3924 行有个多余的 })—— 用打包好的 asar 里的旧文件对比确认,这是改动前就存在的,浏览器会忽略多余右括号,行为无变化,所以没动它。

技术要点 ① 「继承来的功能」要先问一句「它还指向谁」:一个看起来无害的更新按钮,指向的是别人的仓库 —— 这类东西的破坏力比 bug 还大,因为它是按设计正常工作的。
② 删代码比加代码危险:一次删 40 多个引用点,最容易留下的就是「悬空引用」(删了定义忘了删调用), 后果是打开应用一片空白。所以改动 17 专门补了一条真实启动验证来兜这件事。
涉及 7 个文件 移除 约 40 个引用点 残留检查 全量扫描
11
2026-09-16 测试 修复 1.2.0

冒烟测试落地,当场抓出一个「Word / Excel 预览完全失效」的 bug

前面所有测试都是纯 Node 跑的,碰不到真实浏览器环境。 而客户端最关键的两条链路(自定义协议、Office 解析)恰恰只在真实 Chromium 里才成立。于是补了 npm run smoke。

为什么要有这一层

「代码写对了」不等于「功能可用」。下面这个 bug 在源码层面完全看不出来, 是真实环境测试抓出来的。

抓到的 bug:corsEnabled 漏写

protocol.registerSchemesAsPrivileged() 里的 corsEnabled 在 Electron 里默认是 false,而原来没写。后果是渲染层 fetch('r2drive://file/xxx') 直接 TypeError: Failed to fetch, 请求连 protocol.handle 都到不了(协议层日志里一条记录都没有)。

症状极其隐蔽 —— <img> / <video> / <audio> / <embed> 用的是 no-cors 请求,完全不受影响。所以图片、视频、音频、PDF 预览全都正常, 只有 Word / Excel 预览整体失效(那条链路是 fetch → ArrayBuffer → mammoth/SheetJS,卡在第一步)。

换句话说:改动 7 加的 Office 预览,在打包版里其实是坏的。

缺 corsEnabled 时,同一份代码里的两种取法 <img> / <video> no-cors 请求,不受影响 ✅ 图片/视频/PDF 正常 fetch() → 解析 走 cors,被协议层挡下 ❌ Word / Excel 失效 修法:在 registerSchemesAsPrivileged 里补一行 corsEnabled: true。
「一半正常一半坏」是最难查的形态 —— 它让人以为协议本身是通的。
怎么改的

做了受控实验:同一份代码,只切换 corsEnabled 有无,在三种 origin(file:// / http://127.0.0.1 / r2drive://app)下测。结果一致:缺了就是 Failed to fetch、handler 命中 0 次; 补上就是 200、命中 2 次。修复本身就是补一行,同时加了源码守卫断言防止回归。

冒烟测试本身也踩了两个环境坑(都不是脚本的问题):

  • Electron 里连第 1 行都不执行 → 根因是这个 shell 环境里 Chromium 渲染进程的沙箱起不来, 不加 --no-sandbox 时连 about:blank 都是 ERR_FAILED。 所以「脚本不执行」其实是「页面根本没加载」
  • 含中文路径的页面加载失败 → loadFile() 不做百分号编码,改用 pathToFileURL()
技术要点 ① 关键设计:测生产代码本身,不是复制品。previewUrl() 和 sanitizeHtml() 是从 renderer.js 里按标记串切片抽出来再注入渲染层的 —— 测的就是用户真正在跑的那份代码。
② 默认值是最危险的:corsEnabled 默认 false、console-message 的回调签名会变 —— 「没写」和「写错」一样致命,而且没写的那个连报错都没有。
③ 顺手发现的另一个坑:测试里 销毁窗口后再新建窗口必定失败,所以全程复用同一个窗口; Windows 上 Electron 是 GUI 子系统程序,主进程 console.log 不进父进程输出,报告必须落盘再读。
覆盖 协议 / Range / 媒体 seek / Office / XSS 当场抓到 1 个真 bug 修复 1 行
19
2026-09-16 修复 1.2.4

修复同步会丢文件 —— 客户端唯一一条真能删掉你文件的路径

云端覆盖本地那段,原来的顺序是先 rm 删掉本地文件、再 rename 把新文件换上去。 只要 rename 失败,本地文件就永久消失了。

问题是什么

rename 会失败的情形一点都不罕见:杀毒软件锁着、磁盘满、路径被别的程序占用。 更糟的是失败分支里还把临时文件也删了 —— 等于连最后一份数据都不留。

同一个「覆盖」,两种做法的容错差别 旧:删了再写(两步) rm 原文件 rename 新的 rename 失败 → 文件没了 ✗ 不可恢复 新:先留档,再原子替换(一步) 原文件挪进留档 rename 新的 失败 → 留档放回原位 ✓ 有后悔药 同盘的 rename 是原子操作、几乎零成本 —— 大文件也不会拖慢同步。
核心思想:永远不要让「删掉旧的」和「写上新的」分成两步做。
怎么改的
  • 覆盖前先把原文件挪进留档目录 <同步根>/.r2sync-backup/<相对路径>.<时间戳>
  • 替换失败时把留档文件放回原位
  • 失败时不再删临时文件 —— 用户至少还能从 .r2sync-*.tmp 里把内容捞回来
  • 留档目录会被同步扫描主动跳过,不会传到云端去;每个文件保留最近 3 份留档
改完什么样

就算判断错了(时钟偏差、两台机器同时改),用户有后悔药。 这是「覆盖前留档 + 原子替换 + 失败回滚」这个模式的第一次应用,改动 22 又用了一次, 改动 25 把它用到了配置文件上。

技术要点 ① 改数据相关的东西,先问「如果这一步失败会怎样」。这个项目里两次数据丢失风险 (丢文件、丢配置)都是同一个病根:把「删旧的」和「写新的」分成了两步。
② 用 rename 而不是 copy 做留档:同盘 rename 是原子的、几乎零成本,大文件也不拖慢同步。
③ 判断逻辑的兜底和回滚机制是配套的:一个是「尽量别判断错」(改动 22 的时钟偏差修正), 一个是「判断错了能救回来」(这条)。两个都要有。
涉及 applyRemoteFileUpdates() 留档保留 最近 3 份 风险等级 唯一能丢本地文件的路径
24
2026-09-16 新增 1.2.4

PPTX 预览 —— 零依赖实现,不引 JSZip

用户原话:「现在客户端还没办法预览 pptx 吗?这个很重要平时用得最多。」 教学课件大量是 pptx。于是自己写了一个解析器(约 380 行),没有引入任何第三方库。

为什么不用现成的库

pptx 本质是个 ZIP 包,而 Chromium 已经内置了解压能力 (DecompressionStream('deflate-raw')),没必要为此引入 JSZip。

一个 .pptx 文件里面长什么样 pptx(= ZIP) [Content_Types].xml ppt/presentation.xml ppt/slides/slide1.xml ppt/media/image1.png ① 从文件末尾找 EOCD ② 读中央目录 ③ DecompressionStream 解压 ④ 按 sldIdLst 排幻灯片顺序 显示: 文字 · 图片 · 备注 不还原: 版式 / 母版
版式还原依赖字体度量、母版、占位符,纯前端还原代价极高且结果不可信 —— 界面上写明了这条边界。
怎么改的(踩过的几个细节)
  • 手写 ZIP 目录解析:从文件末尾找 EOCD(签名 0x06054b50,注释区最长 65535 字节),再读中央目录(0x02014b50)
  • 解析 OOXML 关系:.rels 里的 Target 是相对路径(如 ../media/image1.png), 要解析成 zip 内的绝对路径;TargetMode="External" 的外链图片要排除
  • 幻灯片顺序:优先读 presentation.xml 里的 sldIdLst, 读不到时按 slideN 的数字排序(保证 slide2 排在 slide10 前面)
  • 安全:pptx 里的文字来自他人文件,渲染一律用 textContent,绝不拼 innerHTML
  • blob URL 在切换文件/关闭预览时统一回收
改完什么样

pptx 能预览了,且不增加任何打包体积(没引入库)。旧版二进制 .ppt 仍然只能提示「另存为 .pptx」。 测试用手搓的 stored ZIP 验证解析器,含「文档里写的 <script> 只会是文字」和「slide2 排在 slide10 前」这类边界。

技术要点 ① 先问「平台有没有」再问「用哪个库」:DecompressionStream 已经能解 deflate, 省掉一个库 = 省掉一份体积、一份依赖风险。这套思路可以复用 —— 以后要做 docx 内嵌图、xlsx 图表,接着用。
② 排序要用数字,不要用字符串:'slide10' < 'slide2' 是 true —— 这类「字符串排序」的坑在文件列表、版本号上都会再遇到。
新增 pptx-viewer.js(约 380 行) 新增依赖 0 个 打包体积增加 0
25
2026-09-16 修复 1.2.5

配置文件原子写 + 自动备份恢复 —— 配置坏了不再静默清空

这是和改动 19 同一类的毛病,只是这次丢的是配置。原来的 writeFileSync(configFile, ...) 动作是「打开 → 清空 → 写」,中间断电、崩溃、磁盘满, config.json 就变成半截 JSON。

问题是什么

而 loadConfig() 解析失败时只 console.warn 一句, 然后静默退回全部默认值。后果比看起来严重:config.json 里装着 全部同步任务(本地目录 ↔ 云端目录的对应关系)、下载目录、会话、所有设置。 文件一坏,任务列表直接清空,同步悄悄停摆,界面上没有任何报错 —— 用户只会觉得「怎么不传了」。

怎么改的
  • 先写 config.json.tmp,再 renameSync 整体覆盖(同盘 rename 是原子的)
  • 覆盖前把上一份可用配置复制成 config.json.bak
  • 换不上去就把 .tmp 清掉,原配置保持不动
  • loadConfig() 依次尝试 config.json → config.json.bak
  • 解析结果必须是非数组的对象,null / [1,2,3] 一律视为坏文件
自己引入、又被测试当场抓出来的一个 bug:从 .bak 恢复后要回写主文件, 而「覆盖前先备份上一份」这一步会把当前那个损坏的主文件复制成 .bak, 把刚救回来的唯一好备份顶掉。 修法:备份前先确认原文件真的是可用配置(新增 isUsableConfigFile())。
技术要点 ① 「恢复」逻辑本身也会毁数据。修好「写坏」之后,新的风险转移到了「恢复」这一步 —— 这是写测试当场抓出来的。说明写测试是值得的:它会去走你没想过的分支。
② 校验要挑剔一点:「能 parse 成 JSON」不等于「是配置」—— null 和数组都能 parse,但都不是合法配置。格式校验要按语义,不按语法。
验证 verify-config-safety.js(29 项) 自动恢复 .bak 兜底 抓到自己引入的 1 个 bug
28
2026-09-16 → 09-17 新增 改造 1.2.6 → 26.9.17-2

回收站:先接上,再从弹窗搬进侧边栏

服务端早就有回收站(删除是「先把条目快照进回收站,R2 对象仍被引用着,保留 30 天」), 但客户端没有入口 —— 删掉的东西只能去网页端找回来。对老师来说,「误删了教学材料」是最要命的情况。

问题是什么

第一次接上时做的是「顶栏一个圆按钮 + 弹窗」。用户看了之后说: 「回收站很好用,需要直观一点,可以直接放左边侧边栏。」

这句话背后其实是一条放置原则,后来被固定下来:

「空间」进侧边栏,「属性」留右键菜单 是一个「地方」→ 侧边栏 你「去」回收站,像「去」相册 我的云盘 · 相册 · 快速访问 · … · 回收站 ✓ 删除的文件可随时还原,不会自动清理 ✓ 筛选、全选、批量还原/清除 是某个文件的「属性」→ 右键菜单 你「对某个文件」看它的过去 右键文件 → 版本历史…(N 个旧版本) ✓ 文件被改坏能退回旧版 ✓ 恢复 / 删单个版本
回收站管「没了」,版本历史管「变了」—— 两者不能互相替代,所以入口位置也按语义分开放。
怎么改的

接上服务端(1.2.6):契约从服务端源码读出来,不照猜 —— GET /api/trash 拿列表、POST /api/trash/restore 传 {ids}、 POST /api/trash/purge 支持 {ids} / {olderThanDays} / {all:true} 三种模式。 读源码读出一条关键语义:三种模式在服务端是按顺序判的,客户端绝不能把两种一起发出去 —— 这点单独写了断言钉住。

搬进侧边栏(26.9.17-2):侧边栏本来就是「视图切换」结构(button.nav-item[data-view]), 加第 7 项顺着现有做法走。新增 <section id="trashView">,内部结构照抄其他视图; 列表项复用原来的样式。旧入口全部删掉(圆按钮、弹窗、关闭按钮),不留两处入口。

顺带修掉一条已经过时的文案:删除确认框原来写「此操作无法在客户端撤销」—— 这在没有回收站的年代是对的,但会让用户以为删了就真没了,不敢删、删错了也不敢找。 改成「删除后会放进回收站保留 30 天,期间可以还原」。
技术要点 ① 接口契约要读源码,不能只看文档:「purge 三种模式不能混着发」这条, 光看接口文档根本不会知道 —— 但发错了会清掉不该清的东西。
② 列表要归一化 + 过滤脏数据:kind 统一成 file/folder、size 转数字, 过滤掉没有 id 的脏数据 —— 否则界面上会出现一行点不动也删不掉的幽灵项目。
③ 空选择在客户端就拦住,不把请求发出去。
④ 服务端来的文件名一律 textContent 渲染(可能是别人上传的文件,名字里可能带脚本)。
⑤ 用户可见的文案也是功能:「无法撤销」这四个字会让用户不敢用回收站 —— 功能做对了,文案没跟上,等于白做。
新增 3 个接口 验证 verify-trash-versions.js(60 项) 旧入口 全部删除(有反向断言)
29
2026-09-16 → 09-27 新增 修复 1.2.6 → 26.9.27

版本历史:接口 → 界面 → 把「空壳」补成真的

分三步走完,第三步最值得记:界面做完了、测试全绿、功能「看起来」好了, 但查服务端源码才发现 —— 大于 512KB 的文件压根不存版本。

为什么这条最要紧

家人共用 + 自动同步 = 最容易「内容被改坏」的组合。 文件被覆盖上传之后,文件本身还在、回收站里什么都没有 —— 这时候只有版本历史能救。 所以它比回收站更必要,不是更不必要。

三步怎么走的
  • 第一步(1.2.6,接口层):新增 listVersions / restoreVersion / deleteVersion。 契约同样从服务端源码读出。客户端再排一次序(按 ts 倒序),不依赖服务端返回顺序
  • 第二步(26.9.17-2,界面):右键文件 →「版本历史…」(菜单项上直接显示「N 个旧版本」)→ 弹窗列出旧版本,每行一个「恢复」和一个「删除」
  • 第三步(26.9.27,补缺口):见下面那个红色方框

上限怎么从 20MB 变成 60MB:worker 里 VERSION_MAX_MB = 20 是硬编码的, 但接口支持客户端传参覆盖。教学 PPT 大多超 20MB,走默认等于没有保护 → 客户端传 60,只改客户端,不用动服务端。

「只留 5 版」怎么实现的:worker 的 VERSION_KEEP_MAX = 10 同样硬编码且不可配。 于是客户端在上传完成后自己收:pruneVersions() 列出旧版本,slice(5) 之后的逐个删。 里面有个去重集合 versionPruneSeen:同一路径在一个 app 生命周期里只查一次,否则批量同步会把请求数翻倍。

⚠️ 缺口 36:功能做完了,但对教学 PPT 是「空壳」
客户端的大文件(>512KB)走的是分布式上传,而 worker 里 snapshotVersion()(覆盖前存旧版本)只在另外两条路径上挂了钩子,分布式上传那条没有。 意味着:大于 512KB 的文件根本没有版本历史,界面能开、能列出旧版本,但列表永远是空的。

这个坑修起来要三步、缺一步都不行:
① worker /api/distributed/init 接收 versionMaxMB 并存进 session
② worker /api/distributed/complete 在覆盖写入前调 snapshotVersion
③ 客户端上传时带上 versionMaxMB(普通上传 / multipart / distributed 三处统一 60MB)
已于 2026-09-27 全部补齐。
改完什么样

文件被改坏能自己救回来 —— 这是家人共用场景下最要紧的一条。 而且现在教学 PPT 那种大文件也真的有版本历史了。

另外一个刻意的设计:「恢复」直接执行,没有二次确认。 理由是「恢复」本身可逆(再恢复一次就行),弹确认框只会让人多点一次; 真要防误操作,防的是删除,不是恢复。

技术要点 ①「做了」和「有用」是两件事。界面、弹窗、恢复流程、60MB 上限、收 5 版,全都做完了、测试全绿 —— 但查服务端源码才发现大于 512KB 的文件压根不存版本。所以做完要回头问一句: 这个功能在我真正要用的场景里,真的会生效吗?
② 跨「客户端 / 服务端」的功能,要两边一起对:缺口 36 的三步里,① ② 在服务端、③ 在客户端, 缺任何一步都还是空转 —— 服务端有钩子但客户端不传,走的是默认值;客户端传了但服务端没钩子,参数被丢掉。
③ 硬编码的默认值要主动覆盖:服务端默认 20MB、只留 10 版,都是「对开发者合理、对使用者不合理」的值。
上限 20MB → 60MB 保留 10 版 → 5 版 缺口 36 三步全部补齐
32
2026-09-17 新增 26.9.17-2

整目录打包下载 —— 从「点几十次」变成「点一次」

选中文件夹(或一批文件)→ 右键「打包下载」,服务端流式吐一个 zip,客户端原样落盘。

问题是什么

家人下载整套资料是最高频的操作。原来只能一个一个点,几十个文件要等几十次。

怎么改的
  • 新接口 POST /api/download-zip,body { paths: [...], base: '父目录' }。 base 的作用是决定zip 内部的条目路径:base 是父目录时, 解压出来就是「文件夹名/里面的文件」;不传 base 会解出一堆散文件
  • apiClient.downloadZip() 走 await pipelineAsync(...)(node:stream/promises)
  • 三处入口:文件夹右键菜单、多选操作栏、视图空白处右键(根目录不显示)
  • 进度、速度、完成通知复用普通下载那一套(makeTransferId / makeSpeedTracker / makeProgressEmitter),不用新写
踩了两个 Node 流的坑:
① 一开始用 const source = Readable.fromWeb(response.body) 再 source.on('data', ...) 计数 —— 这会让流提前进入 flowing 模式,在 pipeline 挂上去之前到达的数据可能直接丢掉。 改成往 pipeline 里串一个 Transform,在 transform() 里计数。
② Node v22 下 pipeline(a, b, c) 三参数无回调会报 ERR_INVALID_ARG_TYPE(它把最后一个流当成了回调),必须用 promise 版。
改完什么样

家人下载整套资料从「点几十次」变成「点一次」。服务端有硬上限:80 个文件 / 150MB 总计, 超了报中文提示,客户端把中文说明原样透出来(不是「HTTP 400」这种看不懂的话)。 失败时会删掉半截文件,不在磁盘留个打不开的 zip。

技术要点 ① 要在流中间计数,就往 pipeline 里串一个 Transform,不要旁挂监听 —— 旁挂会改变流的模式,而且丢数据不报错。
② 复用现成的进度机制:打包下载和普通下载在用户眼里是同一件事, 进度/速度/通知走同一套,用户感受一致,代码也少写一半。
③ 服务端的错误提示要原样透出:「超过 80 个文件上限」比「HTTP 400」有用得多。
新增 verify-zip-download.js(18 项) 服务端上限 80 文件 / 150MB 三处入口
34
2026-09-17 新增 26.9.17-2

断点续传 —— 传到一半断了,下次只补没传完的分片

教学 PPT 几十上百 MB,传一半断掉要从头再来,家里人用着会崩。

问题是什么

上传传到一半断了(断网、关窗口、服务端抽风),原来只能整份重传。

同一个文件传两次:哪些分片真的要重传 第一次(传到第 4 片断了) ✗ 断了 第二次(只补缺口) ✓ 只传这 2 片 灰色 = 已传好,直接跳过;蓝色 = 这次要补的。若一片都没缺,直接进入合并,一片都不重传。 最关键的一条:catch 里故意不调 abort —— abort 会把「续传钥匙 → 分片」的映射一起删掉,等于亲手毁掉续传能力。
代价:中断的会话会占着服务端的记录,24 小时内不清理 —— 换来的是真的能续传,这个交换值。
怎么改的(两条上传路径各用各的办法)

① 分布式上传(>512KB 走的就是这条):服务端没有「续传」接口, 但 /api/distributed/reallocate 能给「还没传完的分片」重新发一套上传地址(顺带清掉旧节点上的残留)。 客户端自己用 distributedSessions 记「resumeKey → sessionId + 已传分片号」, 重传时算出 missing,调 reallocate 拿新地址只传这些。 reallocate 失败(会话过期,服务端 TTL 24 小时)→ 静默退化成全新上传,不给用户弹错。

② R2 分片上传(兜底路径):init 的 body 里带 resumeKey, 服务端认得上一次的钥匙时会把已传分片还回来(resumed: true)。 提交前 parts.sort() 按分片号排好 —— 顺序错了 R2 会把文件拼错,而且不报错。

③ 钥匙怎么算:makeResumeKey(路径, 文件大小, 修改时间) → sha256 截 32 位。 文件一改钥匙就变,服务端不会把「旧文件的分片」错当成这次上传的一部分。

技术要点 ① 清理逻辑会毁掉能力:「失败时清理现场」是本能反应,但这里清理掉的正是续传要用的东西。 加清理之前先问一句:这个状态以后还用不用得上?测试里专门钉了这条(两处 catch 里不许出现 abort)。
② 幂等的钥匙设计:把「文件身份」编码进钥匙(路径 + 大小 + 修改时间), 文件一改钥匙就变 —— 不用额外维护版本号,也天然避免了张冠李戴。
③ 降级要静默:续传不了就当作全新上传,不弹错 —— 用户要的是「传上去」, 不是「知道续传失败了」。
新增 verify-resume.js(49 项) 会话 TTL 24 小时 失败处理 静默降级为全新上传
35
2026-09-17 加固 26.9.17-2

加 CSP —— 以及自己踩进去的一个静默坑

原来全站零安全响应头。客户端是本地 file:// 页面 + 一个能执行 HTML 的预览器, 真被塞进恶意 HTML 也没有兜底。于是在 index.html 头部加了一条 meta 形式的 CSP。

问题是什么

没有 CSP 时,只要有一段 HTML 被塞进渲染进程,里面的 <script> 就能执行 —— 而渲染进程能调 window.r2Drive 删文件。改动 8 的 sanitizeHtml() 兜住了 Office 那条路径, 但那是「在某个入口处过滤」,CSP 是最后一道兜底:就算有路径漏了,脚本也跑不起来。

怎么写的(几条不能想当然的地方)
  • meta 形式的 CSP 在 file:// 下对 'self' 匹配不可靠 → 每条都显式写上 file:
  • connect-src 必须放 https: / http::文档来源是 file://, 写 'self' 对它毫无意义;而且手机端桥接要 fetch 用户自己填的 API 地址。 这一条没有收紧空间
  • style-src 必须给 'unsafe-inline':动态行内样式 + 自定义品牌色是靠 <style> 写的
  • script-src 不给 'unsafe-inline' / 'unsafe-eval': 这是这条 CSP 真正值钱的地方,渲染层也确实没有 eval / new Function
  • frame-src / base-uri / form-action 全关(全项目没有 <iframe>)
CSP 的各条指令分别管哪一类资源 script-src 脚本 —— 最要紧的一条,不给 inline style-src 样式 —— 要给 'unsafe-inline' img-src / media-src 图片 / 音视频 —— 要放 r2drive: 和 blob: object-src PDF 预览的 <embed> 归这条管 ← 踩过的坑 connect-src 网络请求 —— 这条没有收紧空间
第一版把 object-src 写成 'none',看起来「关得越干净越好」—— 结果 PDF 预览直接空白。
⚠️ 自己踩的坑:object-src 'none' 会把 PDF 预览静默拦掉。 <embed> 这个元素归 object-src 管,而 PDF 预览正是靠 document.createElement('embed') + src = r2drive://file/... 实现的。 所以 'none' 会让 PDF 预览直接空白,而且控制台不一定报得清楚 —— 正是 CSP 那个「静默失效」的特性。修正:改成 object-src r2drive:(只放行它真正要用的那一个协议)。
改完什么样(两层验证)
  • 运行时实测(tools/smoke/index.js):真的启动 Electron,挂 securitypolicyviolation 监听, 实测内联样式能不能用、blob: / data: 图片能不能加载、内联脚本有没有被拦
  • 离线核对(tools/verify-csp.js,30 项):把 index.html 和 styles.css 里出现的 每一条资源引用逐个拿去对白名单,判断会不会被拦
  • 另加两道守卫:只要源码里出现 <embed>/<object>,就断言 object-src 不是 'none', 并且从源码里读出协议名去核对它真的放行了那个协议(不在测试里另写一份)
技术要点 ① 安全加固最容易伤到自己人。「默认全拒」是安全的好默认,但一定要给自己明确要用的东西留显式通道 —— 否则收紧的那一刻就把自己的功能弄坏了。
② CSP 出错是静默的:图标、字体、预览会直接不显示,控制台还不一定报得清楚。 所以不能「写完就算」,必须实测。
③ 守卫要从源码推导,不要写死:测试里去读源码里的协议常量,而不是自己再写一遍 —— 否则改了源码、测试还是绿的(假绿)。
新增 verify-csp.js(30 项) 运行时实测 8 条 自查抓到自己的错 1 个
37
2026-09-17 测试 只改测试,不动客户端

修掉冒烟测试里两条失效断言 —— 顺便发现「这一层从来没被验证过」

打包完之后,用户第一次在真机上跑了 npm run smoke。 结果:111 项、失败 2 项。两项都是测试自己的问题,客户端代码一行没错。

这件事本身比两个失败项更重要:npm run smoke 是唯一会真的启动 Electron 的验证, 而它长期在开发机上跑不通(沙箱拦 .ssh)。等于这一层从来没被验证过 —— 里面藏了两条早就失效的断言,一直没人知道。
失败项 1:「扫描时会跳过留档目录」

原断言用 indexOf('SYNC_BACKUP_DIR') 找位置,再取后面 400 个字符来判断。 但实测发现:这个字符串第一次出现是文件顶部的常量定义(第 44 行), 真正的判断在第 377 行 —— 压根不在那个 400 字符的窗口里。

起因是改动 19 把留档目录名提取成了常量(「只有一处定义」), 这条断言的锚点就悄悄指错了地方 —— 而冒烟测试跑不起来,所以没人发现。 改成从真正的判断处起算。

失败项 2:「初始化期间渲染进程零报错」

这条是自己弄坏的。改动 35 加 CSP 时,为了验证「内联脚本真的被拦下了」, 加了一个故意制造 CSP 违规的探针。于是「内联脚本被拦下」通过了, 但那条 error 同时被算进了「渲染进程零报错」,把它顶成失败 —— 探针自己把另一条断言弄挂了。

改法:把反向验证挪到主探针之外,中间先给报错列表拍个快照,断言改成看快照。 语义反而更准了 —— 快照拿的是「做这个故意违规之前」的报错,那正是「初始化期间」。

改完什么样

冒烟测试从 111 项失败 2 项 → 111 项全过(用户重跑确认)。 exe 不用重新打包:tools/ 不在 build.files 里,这些改动不进包。

后续还补了一条:用户重跑时终端冒出 Electron 42 的弃用警告 (console-message 的回调签名改了)。危险的地方不只是那条警告 —— 而是如果监听哪天失效,报错列表会永远是空的,「零报错」就变成一条永远通过的空断言。 于是改用新签名,并加了一条自检:故意打一条 error,确认真的被收到了。断言总数 111 → 112。

技术要点 ① 用 indexOf 找锚点之前,先确认这个字符串的第一次出现就是你要找的地方。 把字面量提取成常量之后,「第一次出现」的位置会变 —— 这类断言会静默失效。
② 测试里「故意制造错误」的探针,必须跟「零错误」类断言隔离。
③「零错误」这类断言必须自证不是空断言。凡是靠「什么都没发生」来判定通过的测试, 都要问一句:我怎么知道它真的在监听?
④ 唯一能跑真机的那一层,如果长期跑不起来,那它就等于没有。 「跑不了」要当成待还的债,不能当常态。
断言 111 → 112 修掉失效断言 2 条 exe 不用重打
38
2026-09-29 修复 只改文案

回收站提示去掉空头承诺

蔡老师看网页端回收站文案时觉得不对,一查发现客户端这处也一样不实 —— 而且这句更危险。

原文案哪里不对

原文写「删除的文件会先放到这里,服务端保留 30 天…超过 30 天后由服务端自动清理」。 查下来两半都不成立:「清理 N 天前」按钮已删(没有手动入口)、worker 里也没有任何定时任务(不会自动清)。 最危险的地方是它让家人以为放着不管就会自己消失 —— 于是既不恢复也不清,空间一直占着。

改完什么样

「删除的文件会先放到这里,期间随时可以还原。回收站目前不会自动清理,放多久都在。」 后面保留原有的「『彻底删除』会立刻释放存储空间,不可恢复」—— 这句有用且真实,是操作警告。

技术要点 一个不实的说法很少是孤例。从这一句往回查(它引用了 worker 的 TRASH_KEEP_DAYS), 发现那个常量无人引用;再全项目 grep「自动清理 / 30 天后」,又挖出 3 处同样的说法 (网页端文案、常量注释、客户端开发文档),一起改掉。改完立刻 grep 同类说法,能一次挖干净。
改 1 处文案 ⚠️ 要重新打包才生效

实现方式小结:可复用的套路

38 条改动里反复出现的做法。下次遇到同类问题可以直接照用。整节默认收起,点标题展开。

01

原子写 + 留档 + 回滚

永远不要让「删掉旧的」和「写上新的」分成两步做。同盘的 rename 是原子操作, 用它把「替换」变成一步;并且在替换前把旧的挪走留档,失败时挪回来。

用在哪儿:云端覆盖本地(改动 19)、配置文件保存(改动 25)。 这是整个项目里最重要的一条工程模式 —— 两次数据丢失风险都靠它解决的。

02

接别人的接口,先读别人的源码

不要照猜、不要只看接口文档。契约从源码里读出来,尤其是边界语义 —— 比如「三种 purge 模式不能混着发」「不传参数时走的是哪个默认值」。

用在哪儿:回收站、版本历史的全部契约都是从 worker 源码读出来的。 「purge 模式不能混发」这条光看文档根本不会知道,但发错了会清掉不该清的东西。

03

测生产代码本身,不是复制品

把被测函数从源文件里按标记串切片抽出来再注入测试环境, 而不是在测试里重写一份「差不多的」实现。

用在哪儿:previewUrl() / sanitizeHtml() / PPTX 解析器 都是从 renderer.js、pptx-viewer.js 里抽出来测的。 复制品会随着源文件改动而过期,还照样绿。

04

测试要带对照组

不只测「修复后能过」,还要测「还原成旧写法确实会失败」。 否则你无法区分「修复生效了」和「这个断言本来就不会失败」。

用在哪儿:verify-api-client.js 里内置假服务端, 既测带 CSRF 头能过,也测不带头确实会被 403 拦。

05

源码守卫:把踩过的坑变成断言

每个「修过一次、但可能再犯」的坑,都写成一条源码检查。 这样它不是靠人记得,而是靠测试拦住。

用在哪儿:corsEnabled: true 在不在、两个导航守卫在不在、 假 preload 覆盖度、删除文案不再谎称「不可撤销」、object-src 不许写 'none'……

06

能用平台内置能力,就别引库

先问「平台有没有」,再问「用哪个库」。 省掉一个库 = 省掉一份体积、一份依赖风险、一份升级负担。

用在哪儿:PDF 用 Chromium 内置查看器(省掉约 4MB 的 pdf.js); ZIP 用 DecompressionStream('deflate-raw')(省掉 JSZip)。

07

用自定义协议替掉 base64

把「下载文件才能看」变成「直接流式引用」。 base64 要把整个文件读进内存、体积还膨胀 1/3,所以必然有上限。

用在哪儿:r2drive:// 协议(改动 7)。 注意三点:registerSchemesAsPrivileged 必须在 app ready 之前调、 corsEnabled: true 不能漏、要回写 Access-Control-Allow-Origin。

08

「零错误」断言要自证不是空断言

凡是靠「什么都没发生」来判定通过的测试,都要问一句: 我怎么知道它真的在监听?加一条自检,故意制造一次能被捕获的事件。

用在哪儿:改动 37 —— 故意 console.error() 一条, 断言它真的被收到了。收报能力坏了必须响亮地失败,不能静默变成永远通过。

09

服务端的默认值要主动覆盖

服务端的硬编码默认值,往往是「对开发者合理、对使用者不合理」。 客户端能传参覆盖的,就要按真实使用场景重设一遍。

用在哪儿:版本历史的大小门槛 20MB → 60MB(教学 PPT 大多超 20MB)、 保留版本数 10 → 5。不覆盖,功能就是空转的。

10

用户可见的文案也是功能

功能做对了、文案没跟上,等于白做 —— 甚至更糟,因为用户会因为文案而不敢用。

用在哪儿:删除确认框原来写「此操作无法在客户端撤销」, 让用户以为删了就真没了、不敢删也不敢找;接上回收站后改成「删除后会放进回收站保留 30 天」。 还有 429 的「请 0 分钟后再试」→ 钳到最少 1 分钟。

11

「做了」不等于「有用」

界面、弹窗、流程、测试全绿 —— 功能「看起来」好了。 但要回头问一句:这个功能在我真正要用的场景里,真的会生效吗?

用在哪儿:缺口 36 —— 版本历史全做完了,但大于 512KB 的文件(含教学 PPT) 压根不存版本,列表永远是空的。查服务端源码才发现。

12

假服务端 + 手搓最小样本

用假服务端复刻真实语义(不依赖真实部署),用手工拼出的最小合法文件做夹具(不依赖 Office、不依赖网络)。

用在哪儿:tools/mock-server.js 复刻 CSRF / 401 / 403 规则; 手搓 stored ZIP 拼出最小合法 docx / pptx;手搓 20 秒 WAV 验证媒体 seek。

当前状态

当前版本:26.9.29(2026-09-29 打包) —— 相比 26.9.27 多了一条:回收站提示去掉空头承诺(改动 38)。

产物:release-portable/R2-Cloud-Drive-portable.exe(约 88 MB, 2026-09-29 11:58 重打)。verify-packaged.js 29 项全绿; 8 套 npm run verify 也重跑全过。回滚点 backup/26.9.29/。

📌 输出目录与文件名从这一版起固定:release-portable/ + R2-Cloud-Drive-portable.exe,不再带版本号 —— 为的是做一个不用每次改的桌面快捷方式。 代价是从文件名读不出是哪一版,只能看 exe 的修改时间;版本号没丢,在 exe 属性里(鼠标悬停可见)。

🏷️ exe 属性里现在能看到:文件说明 R2 云盘客户端、 版本 26.9.29、公司 CZP、版权 Copyright © 2026 CZP —— 「去作者」补掉了最后一处漏网(以前公司名还挂着 Electron 模板自带的 GitHub, Inc.)。

🔧 打包也收敛成一条命令:npm run pack:release —— 自动「归档旧目录 → 把 data/ 挪出来 → 自检 → 打包 → data/ 放回 → 核对」。 「把 data/ 挪出来」这步不能省:里面有 config.json 和登录态, 把整个目录改名会把数据一起带走 → 新 exe 跑起来等于「从没配置过」。 归档那步若被外部进程占用(EPERM),脚本会自动降级:打到临时目录,再把新 exe 覆盖回来。

已具备的能力

相对作者原版,现在多出来的东西。

验证体系(三层)

改完一条命令就能验。第三层需要真机,跑不了就等于这一层没验证过。

层 命令 跑在哪儿 覆盖什么
语法 npm run check Node 7 个源文件能不能过解析
逻辑 npm run verify Node(带假服务端) 接口层、同步逻辑、配置安全、回收站/版本、预览解析 —— 8 个套件
真实环境 npm run smoke 真实 Electron 自定义协议、Range、媒体 seek、Office 解析、真实界面启动 —— 112 项
打包后 verify-packaged.js Node 包内源码与磁盘逐字节一致 + 新功能关键串真的进包 —— 29 项

为什么「打包后」要单独一层: check / verify 测的都是磁盘上的源码,不是打进 exe 的那份。 而「源码对」不等于「包里的对」—— 曾经出过 package.json 被误删、重建后才对得上的事。

一个反直觉但正常的事实:包内的 package.json 与磁盘本来就不同 —— electron-builder 会把 scripts / devDependencies / build 三段剥掉。 这不是出错,verify-packaged 里专门有一条断言把这件事钉住。

还没做 / 待验证

待办

优先级 事项 说明
高 实机验收 所有验证都是自动化的,还没在真机上完整用过一遍。重点试:docx / xlsx / PDF / pptx / 大视频 / 回收站还原 / 打包下载 / 版本历史 / 同步
高 重跑 npm run smoke 本机跑不通(沙箱拦 .ssh),需要自己开终端跑。 上一轮 112 项全过,但 CSP 动过 object-src,PDF 预览要重点看
中 清理旧 release 目录 release-portable-26.9.17、-2、-2b 等, 被文件锁占用的需先退出应用再手动删。 ⚠️ release-portable 是要留的(固定输出目录),别删
低 分享系列、站点设置 服务端已有接口,但要先判断是不是「网页端做更合适」 —— 能用网页端解决的不往客户端搬
低 全盘文件列举 /api/list-all 大账号下可能很慢,要评估是否值得
低 把 LICENSE 打进包 MIT 要求保留版权声明,而 build.files 只含 src/** / assets/** / package.json。 私用场景影响小,改起来是一行

遗留问题

  1. npm run smoke 在开发机上跑不通(沙箱拦 .ssh),需要用户自己开终端跑。 结果落在 tools/smoke/report.txt(Windows 上 Electron 是 GUI 程序, 主进程 console.log 不进父进程 stdout,所以只能落盘)。
  2. 它长期跑不起来这件事本身是个风险:它是唯一会真的启动 Electron 的验证层, 跑不了就等于这一层没验证过 —— 改动 37 那两条失效断言就是这么藏了很久的。
  3. 移动端没有版本历史 / 回收站(src/mobile/mobile.js 里没有)。 目前只做桌面端,未提需求。
  4. PPTX 不还原版式:只显示文字、图片、备注。真要还原得靠渲染引擎,代价极高, 大概率不做。
  5. 同一天多次打包的版本号会撞车:日期制(26.9.29)在同一天改两次就没法区分。 真遇到了就加后缀(26.9.29-2,注意不能写成四段式 26.9.29.1,那不是合法 semver)。

明确不做的事

这些不是「暂时不做」,是方向上就不做。列在这儿,是为了将来提议时能想起为什么当初不这么干。

不做 原因
❌ 引入同步账本(.sync-state.json 那套)这是另一套客户端难维护的主因;现在「远端时间 vs 本地时间」够用
❌ 做删除同步(本地删 → 云端删)一次误删就是灾难。要删去网页端,那儿有回收站
❌ 做「云端更新」拉取目录会堆积副本;现有「多端同步提示」够用
❌ 改主界面结构 / 套网页外壳自绘界面是它的优点
❌ 做「云端目录改名」流程风险高(会整目录重传),要用就用网页端搬移
❌ 用 CDN 加载任何资源离线可用 + 不依赖国外网络;目前真实页面里外部 http 资源数为 0
❌ 保留任何「指向作者仓库」的自动更新会把定制版覆盖回原版(见改动 10)
判断新需求要不要做的四条标准: ① 能用服务端解决吗?(能就别在客户端造轮子) ② 会引入「先删后写」这种两步操作吗?(会的话必须配留档 + 回滚) ③ 会依赖国外网络吗?(会的话先找本地替代) ④ 网页端已经能做吗?(能的话就不搬进客户端) —— 四条都过不了再考虑动手。