Worker 改动同步记录

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

起点 worker原版0915.js · 2026-09-15 当前 worker家人-0928.js · 2026-09-29

关键指标

两个阶段加起来的结果。字节数里含 231 KB 的内联图标字体。

代码体量
325 KB → 640 KB
净增 +315 KB(其中 231 KB 是字体)
代码行数
7,814 → 9,269
净增 +1,455 行
API 路由
35 → 43
净增 +8 条(另加 5 条 PWA)
完成事项
17 项
新增 5 · 修复 6 · 加固 2 · 调整 4
验证 8 组 103 项全过

阶段总览

每次改完另存为新文件,原文件保持不动 —— 出问题能拿原文件重新改。

阶段 日期 产出文件 体量 路由 主要内容
起点 2026-09-15 worker原版0915.js
作者 GitHub 发布的版本
325,550 B
7,814 行
35 只读参考,未改动
一 2026-09-26 worker家人-0926.js 344,678 B
7,975 行
35
+5 PWA
去掉作者痕迹、加 PWA 支持
二 2026-09-27 worker家人-0927.js 639,596 B
9,266 行
43
+5 PWA
回收站、版本历史、打包下载等 7 项
三 2026-09-28 worker家人-0928.js
当前版本
640,621 B
9,269 行
43
+5 PWA
壁纸空文件兜底、Office 文案、窄屏横滑、回收站简化、修壁纸遮挡、改回收站文案
代码体量(KB) 起点 0915 325 阶段一 345 +19 KB 阶段二 640 +295 KB 阶段三 640 +1 KB ↑ 这两段共 231 KB 是内联图标字体 扣除字体后,三个阶段真正写的代码约 80 KB。

改动清单

按「影响面」列排序,可以一眼看出哪几项是必须的。

# 事项 类型 服务端 网页端 客户端 规模
1 回收站
删掉的东西能捞回来
新增 ✅✅✅ ~205 行
2 版本历史
文件被改坏了能退回旧版
新增 ✅—✅ ~255 行
3 整目录打包下载
多选一次下成一个 zip
新增 ✅—✅ ~285 行
4 断点续传
大文件断了不用从头传
新增 ✅—✅ 75 行
5 安全响应头
9 处页面出口统一加防护头
加固 ✅✅— ~60 行
6 图标字体本地化
不再依赖外部 CDN
加固 ✅✅— ~25 行
7 目录改名批量实现
文件多了改名不再出错
修复 ✅✅✅ ~145 行
8 去作者痕迹 + PWA
去掉作者署名,加可安装到桌面
改造 ✅✅— +363 / −202
9 安全补丁验证
4 组反向测试,证明补丁不是白加的
验证 ——— 41 项断言
10 修 CSP 图片白名单
页头 logo 不再是破图
修复 ✅✅— ~25 行
11 修浏览器端调服务端函数
分享「有效期」一显示就报错(作者原版就有)
修复 —✅— ~14 行 + 1 检查脚本
12 壁纸空文件兜底
选到 0 字节图时如实报错,不再「提示已更新、壁纸却没变」
修复 —✅— 1 行
13 Office 文案指向客户端
网页端不做 Office 预览,把人引到客户端去打开
调整 —✅— 1 行
14 文件列表窄屏横滑
手机上「修改时间」不再被压扁,且仍能点表头按时间排序
新增 —✅— 2 行 CSS
15 回收站简化
去掉「清理 N 天前」按钮 + JS(蔡老师觉得多余,只留「恢复所选 / 彻底删除 / 清空回收站」)
调整 —✅— -18 行
16 修回收站被壁纸盖住
壁纸层 body::before 是定位元素,会盖在没定位的内容之上;作者的白名单漏了 .trash-page → 设了壁纸后标题和按钮被洗白。补上并加 body > * 兜底
修复 —✅— +5 行
17 回收站副标题去掉空头承诺
原文案「30 天后可手动清理」已不成立(「清理 N 天前」按钮上一轮删了,也没有任何定时任务)→ 改成「暂未启用自动清理」,只说实话
修复 —✅— 改 1 行文案
「服务端 / 网页端 / 客户端」是什么意思:服务端 = Worker 里的接口(改这里全家都能用); 网页端 = 用浏览器打开时看到的界面;客户端 = 装在电脑上的那个 exe。 有些功能只做了服务端 + 客户端(比如版本历史),是因为网页端用的次数不多,故意不加。

改动详情

按时间顺序。每条都写了「为什么改」「怎么改的」「改完什么样」。 「怎么改的」和「技术要点」默认收起 —— 想学细节,点标题就展开。

起
2026-09-15 起点

作者发布了新版 0915

比我们原来用的 0630 多了几样东西。这是我们的出发点 —— 一个字都不改,只当参考。

为什么从这里开始

0915 是作者自己更新的版本,里面有几样我们后来一直想要的:纯文本在线编辑、上传速度显示、自定义分享后缀、复制下载直链、主题切换 + 云盘背景图。从它出发,就不用把这些再自己写一遍。

作者这次新增的东西
  • 纯文本在线编辑 —— 新增 3 条路由 /api/text-file、/api/ui-background、/api/ui-settings
  • 主题切换 + 云盘背景图 —— 可在「界面设置」里换
  • 上传速度显示、自定义分享后缀、复制下载直链
文件 325,550 B 代码 7,814 行 API 路由 35 条
1
2026-09-26 改造 阶段一

去掉作者痕迹 + 加 PWA

把作者的署名、版本检查、页脚链接都清掉,同时让它能「安装」到手机/电脑桌面上像 App 一样用。

要解决的问题

作者在页面里留了署名(页脚的 GitHub 链接)、版本徽章,以及一个会去访问作者仓库检查更新的脚本。自己家里人用,这些既没必要,又会让页面多一次外部请求。

怎么改的

先拿 0630 那次的改动当「配方」(7 处改动),再逐条套到 0915 上。PWA 的代码块直接从既有文件里抽出来插入,保证逐字节一致。

  • 删头部注释里的 By HandsomeMJZ
  • 删页脚整块(含 GitHub 链接和版本号)、删版本徽章 CSS
  • 删版本检查脚本(CURRENT_VERSION / checkVersionUpdate)—— 那个地址指向作者仓库
  • 插入 PWA 常量块:两个内嵌 PNG 图标(192 / 512)、Service Worker 源码
  • 主处理函数加 5 条 PWA 路由:/pwa-manifest.json、/pwa-icon.png、/pwa-icon-192.png、/favicon.png、/pwa-sw.js
浏览器 首次访问 拿到 manifest → 可「安装到桌面」 Worker 提供的 5 条 PWA 路由 /pwa-manifest.json /pwa-icon.png /pwa-icon-192.png /favicon.png  /pwa-sw.js 图标是内嵌的 PNG,不依赖任何外部图床
PWA 让云盘可以像 App 一样装到桌面 —— 图标和 Service Worker 全部由 Worker 自己提供
一个很强的自洽信号:这次改动是 9 个 hunk、+363 / −202 行。对照 0630 那次的 +363 / −201 —— 正好差 1 行,就是 viewport-fit=cover 那一行:0915 的作者自己已经加过了,我们不用再加。说明配方套得准。
技术要点 「配方化移植」:先把两版旧文件做一次对比,得出「改哪几处」的配方(9 处改动),再照着配方逐条套到新版本上。 代码块直接原样抽出插入而不是手敲 —— 这样能保证逐字节一致,不会因为手误引入新 bug。 最后用「改动行数跟上次几乎一样」来自查(差 1 行,正好是作者已经自己加过的一行)。
文件 325,550 → 344,678 B 代码 7,814 → 7,975 行 新增路由 5 条 PWA 验证 34 项全过
2
2026-09-27 阶段二 7 项

把客户端缺的接口补齐 + 日常体验加固

客户端本身已经有回收站、版本历史的界面了,但 Worker 这边没有对应的接口 —— 点了就报错。这次把缺的补上,顺带把两个「会卡住人」的地方加固掉。

先搞清楚缺什么

把客户端源码里调用的所有接口提取出来,跟 Worker 的路由求差集 —— 客户端要的 31 个接口里,有 8 条路由在 Worker 里不存在:回收站 3 条、版本历史 3 条、打包下载 1 条、断点续传 1 条。对应的就是「点回收站报错」「点版本历史报错」「多选下载只能下一个」。

补齐前 客户端调 31 个接口 · Worker 提供 35 条路由 缺 8 条 → 回收站 / 版本历史 / 打包下载 / 断点续传 全失效 补齐后 客户端调 31 个接口 · Worker 提供 43 条路由 缺 0 条 ✓
用一个脚本自动比对两边,结论是「客户端要而 Worker 没有的:0 条」
技术要点 「差集法找缺口」:不靠印象判断缺什么,而是写个脚本把两边的清单各自提取出来(一边是 Worker 的路由判断, 一边是客户端代码里调用的地址),排序后求差集。 这样得到的是可复现的结论而不是「我觉得应该差不多」。补完之后再跑一次同一个脚本,缺口归零就是完成了。
1
2026-09-27 新增

回收站

删掉的东西先放进回收站,能恢复回原来的位置;确认不要了才真正释放空间。

为什么需要它

家里人共用一份空间,谁手滑删了东西都可能。原来的删除是直接抹掉,没有后悔药。而「回收站管『没了』、版本历史管『变了』」是两件不同的事 —— 文件被改坏时文件还在,回收站里什么都没有,那要靠版本历史;文件被删了才归回收站。

怎么改的

关键顺序:删除时先把条目快照进回收站,再摘掉当前条目。R2 里的文件本体因为还被回收站这条记录引用着,就不会被清理掉。

删除 → 恢复 / 彻底清除 的完整链路 ① 用户删除文件 /api/delete ② 快照进回收站 连 storageKey 一起存 ③ 摘掉当前条目 文件从列表消失 文件本体 保 留 恢复 → 回到原来的位置 先恢复目录、再恢复文件(文件要挂回目录索引,目录得先在) /api/trash/restore 彻底清除 → 才真正释放空间 先删回收站记录,再让清理程序去判断「还有没有人引用」 /api/trash/purge
顺序不能反:先摘条目再快照,就查不到文件本体了;清除时先删记录再清理,否则永远删不掉
最关键的一处补丁(不补就白做):删除流程最后会问一句「这个文件还有没有人引用?没人引用就删掉」。 原来的代码只看文件列表,不看回收站 —— 结果就是:文件一删,R2 里的本体当场被当垃圾清掉, 回收站里只剩一条指向空气的记录,根本恢复不回来。 修法:让这一步同时扫「文件列表」和「回收站」两个地方。 另外「清理孤儿」那个功能也要一起改,否则点一次清理就把回收站永久清了。
技术要点 顺序依赖(最容易踩的一类坑):快照必须在摘条目之前,清记录必须在清理之前。 代码本身没写错、逻辑也想得通,但顺序反了就会「静默失效」—— 不报错,只是永远恢复不回来。 另外「新增一类数据」时,必须把它登记到所有会问『谁在引用它』的地方(这次是两处扫描函数), 漏一处就等于埋了一颗定时炸弹。
服务端 3 条路由 + 6 个函数 网页端 工具栏加了入口 保留期 30 天 规模 ~205 行
2
2026-09-27 新增

版本历史

每次覆盖上传前,自动把旧的那一份存起来。文件被改坏了,能退回任意一个旧版本。

为什么它比回收站更重要

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

怎么改的

在「覆盖写入之前」先把旧对象复制一份。难点是「哪里算覆盖」要一处不落地找出来 —— 一共挂了 6 个钩子:

  • 普通上传 /api/upload
  • 分片上传完成 /api/multipart/complete
  • 分布式上传完成 /api/distributed/complete
  • 在线编辑保存 —— 这个最容易漏,它是原地覆盖同一个存储对象
  • 分片上传初始化(接收「多大以上的文件不存历史」这个参数)
  • 分布式上传初始化(同上)
覆盖上传时的时间顺序(顺序反了就存不到旧版本) ① 文件已存在 旧对象还在原处 ② 快照旧对象 复制到「历史版本」区 ③ 写入新对象 这时才覆盖 ④ 索引 只留最近 5 版 大小上限:60 MB 教学 PPT 动辄几十上百 MB,不限制会把空间吃满 恢复时不产生新版本 否则「恢复」本身又被存一版,越恢复版本越多
快照必须在覆盖之前发生 —— 反过来的话旧内容已经被冲掉了,想存也存不到
第二处关键补丁:分布式上传的大文件,「旧本体」其实只是一份分片清单, 真正的数据分片还散在别处。覆盖上传时清理程序会把旧分片删掉 —— 于是历史版本变成「能看见、但下载不了」的坏文件。 修法:删分片之前先查一下「这份数据还有版本在用吗」,有就只删清单、留分片。
技术要点 「把一个概念找全」:「覆盖写入」这件事散落在 6 个不同的入口(普通上传、两种分片上传、在线编辑保存、两处初始化)。 做法是搜索所有写数据的调用点,而不是等用户报错才发现漏了一个。 另外两个细节:判断大小要按文件声明的真实大小(大文件的清单本身只有几 KB,按清单大小判断会永远拦不住); 「恢复旧版本」这个动作故意不产生新版本,否则越恢复版本越多、自我放大。
服务端 3 条路由 + 6 个函数 覆盖钩子 6 处 保留 最近 5 版 / 上限 60 MB 规模 ~255 行
3
2026-09-27 新增

整目录打包下载

多选一批文件,一次下载成一个 zip 压缩包 —— 而不是「选了 10 个只下来 1 个」。

原来的毛病

原来多选下载是逐个文件触发下载。但浏览器只放行第一个,后面的全部静默拦掉 —— 用户看到的现象是「我选了 10 个,只下来 1 个」,很难理解是为什么。

怎么改的

选 1 个:还是普通下载。选多个:走新接口,服务端边读边打包、流式吐出来(用不压缩的 store 模式,省 CPU),前端边收边显示进度。

  • 压缩包名字取当前目录名(在根目录就叫「云盘文件.zip」),比 files.zip 好认
  • 设了上限:最多 80 个文件、合计 150 MB —— 避免 Worker 算超时
  • 天然支持分片/分布式的大文件(统一走同一个读取入口)
原来:逐个触发下载 文件1 ✓ 文件2 ✗ 文件3 ✗ 文件4 ✗ 浏览器只放行第一个,后面的静默拦掉 结果:选了 10 个,只下来 1 个 现在:打包成一个 zip 文件1+2+3+4 → 一个 .zip ✓ 服务端边读边打包、流式输出,前端显示进度 结果:选几个就下来几个
同样的「多选下载」,从「只下一个」变成「一个压缩包全给你」
技术要点 两条通用经验:① 浏览器的「隐性规矩」——连续触发多次下载只会放行第一个,这类限制不会报错,只会静默失败, 遇到「做了但没效果」先怀疑它。② 流式输出:服务端边读边吐、前端边收边显示进度, 而不是「全部攒在内存里再一次性给你」——这样几百 MB 的包也不会把内存撑爆。 代价是 ZIP 每条记录要算校验值,只能边读边算,所以设了文件数和体积上限来兜住处理时间。
服务端 1 条路由 + ZIP 打包代码 上限 80 个文件 / 150 MB 模式 store(不压缩,省 CPU) 规模 ~285 行
4
2026-09-27 新增

断点续传

大文件传到一半断了,下次接着传没传完的部分 —— 不用整个文件从头再来。

为什么需要它

教学资料动辄几百 MB。原来一旦中途失败(网络抖一下、关了个窗口),整个文件白传。客户端的同步任务尤其怕这个 —— 每次跑都得从头来。

怎么改的

客户端把「哪个文件、传到第几个分片」记在本地。重传时先问服务端要一份「还没传完的那些分片」的新上传地址,只补缺的。
这个接口就是 /api/distributed/reallocate —— 顺带把旧节点上的残留对象清掉。

技术要点 「进度记在客户端」:服务端不去记「谁传到哪了」——那样要维护会话状态、还要处理过期。 改成客户端把进度写在本地(哪个文件、传到第几个分片),重传时拿着这个信息来问服务端要新地址。 服务端因此不用保存任何长期状态,重启/过期都不影响,实现也更短。
服务端 1 条路由 规模 75 行
5
2026-09-27 加固

安全响应头

给每一个页面出口统一加上安全头,并且用一处函数收口 —— 以后新加页面不会漏。

原来的状况

全站 一个安全响应头都没有。这意味着:别人可以用别的网站把我们的页面嵌进 iframe 里(钓鱼/伪装),浏览器也不知道该怎么防 XSS。

怎么改的

写了一个 securityHeaders(),再把所有返回 HTML 的地方统一改走 htmlResponse()。改完实测:裸写 text/html 的地方 = 0 处,全部走统一出口。

9 处页面出口 → 1 个统一函数 / /login /trash(本次新增) /shared /s/… 共 9 处 htmlResponse() 一处统一加头 新页面自动继承 实际发出的防护头 Content-Security-Policy X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff Referrer-Policy / Permissions-Policy / HSTS
一句实话:CSP 这条拦不住「已经混进页面的脚本」—— 因为全站大量用的是内联 onclick, script-src 必须带 'unsafe-inline' 才能正常跑。 它真正有用的地方是:frame-ancestors(别人不能嵌我们的页面)、 object-src、base-uri,以及限制外部域。别把它当万能药。
技术要点 「收口」:与其在 9 个出口各写一遍安全头(将来加第 10 个页面必定会忘),不如做一个统一函数, 让所有出口都从它过。这样「漏掉」在结构上就不可能发生。 改完再用一条可数的验证确认:搜索裸写 text/html 的地方,计数必须是 0。
覆盖出口 9 处 → 0 遗漏 规模 ~60 行
6
2026-09-27 加固

图标字体本地化

全站 164 个图标原来靠一个外部 CDN 提供字体 —— 那个 CDN 一旦连不上,整个界面全是「文字」而不是图标。

原来的风险

页面里只有 1 个外部链接,但全站 164 处图标、48 种不同图标全都靠它。这个 CDN 在国内访问不稳 —— 一旦失败,所有图标位置会显示成「home」「upload」这样的文字名,界面直接不能用。

怎么改的

把那 1 个外链换掉,改成把字体文件直接内嵌进页面(base64)。代价是文件大了 231 KB,换来的是零外部依赖。

  • 先试过「只保留用到的 48 个图标」来压缩体积 —— 行不通:这是连字字体,压缩工具会把近 2000 个字形全拉进来,只压到 97.6%,等于没压
  • 最终直接内嵌完整字体(173,620 字节的 woff2)
  • 顺手补了一条兜底样式,防止字体万一没生效时图标名把布局撑宽
  • 内嵌后已核对:还原出来的字体文件与原文件校验值一致
原来:1 个外部 CDN 链接 cdn.jsdelivr.net/npm/material-icons… 164 处图标全靠它 · 连不上就全变文字 外部请求:1 次(但每次打开页面都要) 现在:字体内嵌进页面 @font-face { src: url(data:font/woff2;base64,…) } 164 处图标自带 · 断网也正常显示 外部请求:0 次 · 代价是文件 +231 KB
技术要点 「先量,再决定要不要优化」:原本想只保留用到的 48 个图标来省体积,动手前先实测了一下 —— 只压到 97.6%, 等于没压,于是果断放弃优化路线,改用最简单的「直接内嵌完整字体」。 及时止损比硬啃一个不划算的优化更省事。 改完还做了一步核对:把内嵌的数据解回来,跟原文件比对校验值,确认没被改坏。
去掉外链 1 → 0 体积代价 +231 KB 规模 ~25 行
7
2026-09-27 修复 会丢数据

目录改名:文件多了不再出错

给文件夹改名,背后其实是「把里面所有文件一个个搬到新位置」。文件一多就中途超时,留下半成品 —— 看起来就是「莫名其妙多了副本」。

问题是怎么来的

给文件夹改名,服务端走的不是「改个名字」,而是通用的「搬移」逻辑:逐条复制 + 逐条删除。

  • 逐个读 N 个文件的记录
  • 逐个写入新位置的记录
  • 逐个删除旧位置的记录 + 逐个改父目录的索引

算下来,一个 200 个文件的目录,要来回问数据库约 800~1000 次,而且全是排队一个个来、没有事务。

后果

问到一半超时了 —— 这时候的状态是:一部分文件在新位置、一部分还在旧位置,旧目录的索引删了一半。用户看到的就是「改个名,怎么多出一堆重复文件」。

怎么改的

核心思路:别再一条条问了,把要写的操作先攒起来,再一次提交。

  • 读:用一次「范围扫描」列出所有记录,再按 100 条一批取回来 —— 取代 N 次单独读
  • 写:把「新记录写入 + 旧记录删除」全部攒成待执行语句,再按 100 条一批提交
  • 数据库的批量提交单批内是原子的 —— 不会再出现「改一半」
  • 目标位置已经有东西时,主动退回原来的逐条逻辑(覆盖的语义交给它处理,避免误删)
200 个文件的目录改名 原来:逐条 读 200 次 写 200 次 删 200 次 改索引 200+ 次 ≈ 800~1000 次排队往返 中途超时 → 新旧位置同时有文件(像多副本) 现在:攒起来批量提交 扫描 1 次 批量取 1 次 405 条语句 → 分成 5 批 每批内原子,不会改一半 200 文件 → 5 次提交 ✓(实测)
实测:200 个文件产生 405 条语句,分成 5 批(每批 ≤100 条)
一个坦白:「复制文件夹」这个操作没有同样的批量优化。 因为它不删东西 —— 万一中途失败,只是多留了些副本,不会丢数据,危害小得多。所以这次没动它。
技术要点 「攒起来批量提交」+「快速路径与保守回退」:把 N 次单独请求换成「先全部攒好、再分批提交」, 是处理「批量操作」的通用套路;单批内的原子性顺带解决了「改一半」的问题。 同时新逻辑只在条件满足时启用(数据库可用、目标位置是空的),不满足就退回原来的实现 —— 这样新增的快路径不会破坏任何既有行为。 验证方式也值得一提:用一个假的数据库把它跑起来,检查它到底生成了哪些语句、分了几个批次。
往返次数 ~1000 → 5 规模 ~145 行 验证 24 项全过
8
2026-09-27 新增

网页端的回收站入口

用浏览器打开云盘时,工具栏多了一个回收站图标 —— 否则「从网页删掉的东西」会静默进回收站,看不见也捞不回来。

为什么必须补

回收站接口做好之后,网页端删除就变成「软删除」了 —— 但网页上没有回收站页面。结果就是:从浏览器删一个文件,它消失了,你也没法确认它到底删没删、更没法恢复。这比没有回收站还让人不安。

怎么改的

在顶部工具栏「分享管理」和「界面设置」之间加了一个回收站图标,点进去是一个完整页面:全选、恢复所选、彻底删除所选、清理 30 天前、清空回收站。

实现上做了简化:参考实现里有一套「窄屏点表头切换显示大小/时间列」的响应式机关,要连带搬一大堆样式。我们改成窄屏直接把「大小」列藏掉 —— 够用,也不容易出错。

版本历史故意没做网页端。用的次数不多,只在客户端体现就够了 —— 这是有意的,不是漏了。
技术要点 「搬运不是照抄」:参考实现里有大量为它自己的页面结构服务的细节,搬过来要按自己的情况砍掉, 留核心、去机关。另外这次写验证脚本时,第一次的断言本身写错了(去搜一个词,而那词在别处也出现过,导致误报)—— 所以「测试报错时先怀疑测试」是个值得记住的习惯。 最后:「有意不做」也是一个决定,写清楚理由,免得以后被当成遗漏又补一遍。
网页端 工具栏入口 + 完整页面 验证 27 项全过
9
2026-09-27 验证

把四个「不补就白做」的安全补丁逐个验证

回收站和版本历史能跑起来,靠的不是那几个新页面,而是四处「顺手补上的判断」。这一轮专门给它们做证据 —— 而且是能证明「旧代码确实会坏」的那种证据。

为什么必须验证

新增了一类数据(回收站条目、历史版本),就等于多了一个「谁引用着 R2 对象」的答案。而清理程序判断「这个对象没人要了」时,只认它知道的那几个地方。少登记一处,后果不是"少个功能",而是: 一次「清理空间」把用户的回收站和历史版本整批永久删掉 —— 没有二次确认,也没有回收站兜底(回收站自己就是被删的那个)。

这种补丁的特点是:不补也能跑、测试也不会红、只有真出事了才知道。所以必须主动造证据。

怎么验证的

写 4 个验证脚本,每个都配一组反向测试:把同一套流程换成改动前的真实旧代码再跑一遍,断言它确实会失败。 只有「旧代码确实坏 + 新代码确实好」同时成立,才说明这个补丁不是多余的。

  • 回收站数据流 —— 删除 → 恢复 → 彻底清除走一遍,盯住「删完之后 R2 对象还被认为有人要吗」。
  • 版本历史分片保护 —— 覆盖上传后清理旧本体,盯住「历史版本依赖的分片有没有被顺手删掉」。
  • 清理孤儿保护 —— 提交 3 个 key,看谁被拦、谁该删,两层保护各戳一次。
  • 全量引用扫描 —— 造一份数据,确认回收站条目和版本索引都被算进「还有人要」的清单。
四组共 41 项断言全过。加上之前的页面渲染(27)和目录批量搬移(24),现在一条命令 node tools/run-all-tests.mjs 能跑完全部 6 组 92 项。
技术要点 ① 反向测试要「只换一个变量」:不是整份换成旧文件,而是只把被测的那一个函数换成旧实现,其余全用新版。这样两次运行的唯一差别就是补丁本身,结论才干净 —— 整份换文件的话,跑出的差异可能来自别的地方。
② 假的存储要跟真实结构一致:第一版我把「KV」和「数据库」做成两套独立存储,结果测出「新代码也失效」的假结论。真实情况下它们是同一张表(KV 是映射在数据库上的),拆成两套就把被测代码的视野也拆没了。
③ 抽函数源码的扫描器必须跳过注释:英文注释里的 We'll、don't 那个撇号会被当成「字符串开始了」,后面整个函数都被当字符串,配对直接错位。这个坑是跑测试时才发现的 —— 说明测试本身也在被测。
④ 断言失败时先怀疑断言:这轮又出现一次(断言写错了而不是代码错了)。能证明"旧代码会坏"的测试,才值得信。
新增验证 4 组 41 项 累计 7 组 101 项全过 反向测试 4 组
10
2026-09-27 修复

修一个自己引入的回归:页头 logo 变成破图

蔡老师报:「图标显示有问题」—— 页头那个小图标变成了浏览器的「图片加载失败」占位符。查下来是加安全响应头时自己踩的坑。

问题是什么

安全响应头里的 CSP 写了 img-src 'self' data: blob:,意思是「只允许加载本站自己的图片」。 本意是好的:别人分享的页面里如果有外链图片,会被浏览器直接拦掉,不会「看一眼就把你的 IP 暴露给图片服务器」。

但站点自己的 logo 恰恰是站主自己填的外链(CLOUD_ICON_URL 指向另一个域名)。 规则不认这个区别,一视同仁地拦掉 → 页头当场变破图。图片地址本身没问题,是浏览器按 CSP 拒绝加载它。

怎么改的

让 CSP 的 img-src 自动放行「站主自己填过的那几个图片域名」: 从 CLOUD_ICON_URL / CLOUD_PWA_ICON_URL / CLOUD_FAVICON_URL / LOGIN_BACKGROUND_URL 四个环境变量里各取一次域名,加进白名单。

其余外域仍然一律拦掉 —— 安全和可用两头都占住。一个都没配时,白名单为空,CSP 回到最严。

顺手发现一个更严重的:线上那版 CSP 里 frame-src 'none' / frame-ancestors 'none', 这会把 PDF 预览用的 iframe 一起拦掉(预览是把下载地址塞进 iframe)。 本地文件已经改成 'self' 了,部署新版本时会一并解决。
技术要点 ① 安全加固最容易伤到自己人:「默认全拒」是安全的好默认,但一定要给自己明确要用的东西留一条显式通道 —— 否则收紧的那一刻就把自己的功能弄坏了,而且报错信息(浏览器控制台)普通人根本看不到。
② 白名单要从配置推导,不要写死:写死成某个域名,下次换图床又坏一次;从环境变量读,改了配置立刻生效,不用改代码。
③ 只放行 origin,不放行整条 URL:CSP 只认域名,顺便还能挡住「填了 javascript: 这种危险协议」的情况。
④ 这次仍然配了反向测试:拿改动前的版本跑同样的配置,断言它确实不含 logo 域名 —— 证明这个修复不是多余的。
改动 3 处 · 约 25 行 新增验证 9 项(含反向) 累计 8 组全过
11
2026-09-27 修复

作者原版就有的坑:分享列表里「有效期」一显示就报错

另一个 IDE 那边报了个「版本历史弹窗永远打不开」的 bug,蔡老师问我们这边有没有同样的问题。查下来:我们也中了一个,而且是作者原版就带来的。

那边踩的是什么坑

他们前端函数里直接调了 jsAttr() —— 而那函数只定义在 Worker(服务端)。点开弹窗就报 jsAttr is not defined,而且藏了 21 天:没历史版本时函数会提前 return,永远走不到那一行;语法检查、部署自检也全都抓不到(语法完全合法)。

根子在于:单文件 Worker 里「服务端」和「浏览器端」的代码住在同一个文件里,肉眼看着是一家人,运行时却是两个世界。

我们这边查出什么

写了个静态检查脚本,把 <script> 块(浏览器执行)和内联事件属性里的代码抠出来, 看它们调用了哪些函数、其中哪些在浏览器端根本没定义。结果抓到 1 个真的:

formatDate —— 分享管理面板里的 shareMetaText() 调它,而它只在服务端定义。 和那边一模一样:只有分享设了有效期才会触发(没设就短路跳过),所以一直没被发现。

影响比那边还大一点:报错发生在渲染循环里,一抛就把整个循环打断 —— 从那条分享起,后面所有分享都不再渲染(而列表开头已经被清空), 表现为「分享管理面板一片空白」。分享管理是常用功能,不像版本历史那样偶尔才点一次。

先分清两件事,别混了:那边报的 bug 在 版本历史 里(作者没有的功能,他们自己加的); 我们中的这个在 分享管理 里(作者原版就有的功能)。是两个不同的 bug,只是同一类病。 所以「作者没有的功能,怎么会是原版的问题」—— 答案是:原版的问题在分享管理,不在版本历史。
而版本历史那个坑我们不可能有:我们的网页端根本没有版本历史界面(按蔡老师要求「只在客户端体现」), 所以不存在那个前端函数,也就无从踩坑。
追查来源:这是作者原版 0915 就有的 bug —— 原版第 2379 行、去作者版第 2573 行、我们第 2683 行,同一个位置。 不是我们改出来的,但既然发现了就修掉:在浏览器端补了一份同名 formatDate(和服务端格式保持一致)。
顺带查清的两件事
  • 那边第二个坑(中文路径 404)对我们没影响。他们把路径塞进了 URL 的 路径部分(不自动解码); 我们全程用 ?path=... 查询参数传,服务端用 searchParams.get() 取,会自动解码。已逐个接口确认过。
  • 顺便拿到一份「同名双份」名单:escapeHtml / formatDate / formatSize 在服务端和浏览器端各有一份。这是正常的,但改动时必须两边一起改,只改一边就会出现「有的地方显示对了、有的地方不对」。
技术要点 ① 这类 bug 的共同特征:藏在「条件分支的深处」。没设有效期就永远不触发 —— 所以测试要专门去踩那条分支,光看"页面能打开"没用。
② 判断标准要精确,否则噪声淹没真问题:一开始我只看「浏览器端调用了未定义的函数」,结果满屏都是 circle(、translate( 这种 CSS 字符串里的词。 换成「服务端有定义 + 浏览器端没有 + 浏览器端在调用」三条同时成立,噪声立刻归零 —— 因为杂词根本不在服务端名单里。
③ 检查脚本自己也会错:中途试过"把字符串和注释剥掉再分析",结果它不认识正则字面量,把后面一大段代码也当字符串剥了,凭空造出 4 个假问题。工具报错时先怀疑工具。
④ 一定要做反向验证:拿作者原版跑同一个脚本,它确实报出 formatDate —— 这才证明脚本有效、而且这个坑是原版带来的。
修 1 个真 bug 新增检查 1 个脚本 累计 8 组全过
12
2026-09-28 ~ 29 调整

阶段三:把真机上用出来的问题逐个修掉

前面几项是「加功能」,这一段反过来 —— 全是蔡老师在手机和电脑上真用出来的问题。 改动都很小(1 行、2 行 CSS、改个文案),但每条都对着一个真实的不顺手。

六件事
  • 壁纸选到空文件时如实报错 —— 安卓 PWA 桌面模式下部分机型从相册选图会拿到 0 字节文件, 原来的表现是「提示壁纸已更新,但壁纸没变」(静默失败)。现在拿到空文件直接说「没读到图片内容,请重选一张」。
  • Office 文案指向客户端 —— 网页端有意不做 Office 预览,文案里直接写清「请用云盘客户端打开」,别让人在网页上白等。
  • 文件列表窄屏横滑 —— 手机上「大小 / 修改时间」两列挤得看不清。做法跟已有的快捷按钮栏一样: 屏宽撑不住时保住表格 560px 最小宽度,底部出横向滚动条。「修改时间」列刻意不隐藏 —— 蔡老师要按时间排序。
  • 回收站简化 —— 去掉「清理 N 天前」按钮,只留「恢复所选 / 彻底删除 / 清空回收站」。
  • 修:回收站被壁纸盖住 —— 见下面技术要点,这是本段最值得记的一个。
  • 回收站副标题去掉空头承诺 —— 原文案写「30 天后可手动清理」,但按钮已删、worker 里也没有任何定时任务, 这句话既不手动也不自动。改成「暂未启用自动清理」。
为什么「刚点开正常、一秒后消失」

回收站页面的标题和按钮点开时正常,不到一秒就变白,只剩表格。根因是壁纸层: body.has-custom-bg::before 是 position:fixed; z-index:0 的伪元素, 而按 CSS 绘制顺序,定位元素画在所有「没设 position」的普通内容之后 —— 它天然压在页面内容上面。

作者早知道这事,写了一条白名单把内容抬到 z-index:1: .app-bar, .layout, body > main, .login-wrap, .foot-bar —— 名单里漏了 .trash-page。 补上,并加 body > * 兜底,以后新增页面不会再踩。

时间线也对得上:壁纸是异步加载的(loadUiSettings() 去 fetch,回来才加 class), 所以「点开那一瞬间没有壁纸层、一切正常;fetch 一回来就被洗白」。

技术要点 ① 「一秒后变样」先怀疑异步,别先翻 DOM。这次先通读了回收站的内嵌脚本(46 行)找「谁隐藏了元素」, 其实一行隐藏代码都没有 —— 真正的原因是异步 fetch 回来后加了个 class。
② 没实测过的场景,别提前弹提示吓唬人。壁纸那件事我先加过「PWA 下先提示可能选不到图」, 蔡老师一测是误报(他那儿正常)。改成「失败后才诊断」:真读到 0 字节才说话。
③ 一个不实的说法很少是孤例。从一句可疑文案里的 ${TRASH_KEEP_DAYS} 往回查 → 发现这个常量无人引用 → 全项目 grep「自动清理 / 30 天后」→ 又挖出 3 处(客户端界面、客户端开发文档、常量注释),一起改掉。
新增 1 项 修复 3 项 调整 2 项

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

这一节是专门给「学习参考」用的 —— 把这次改动里反复出现的做法抽出来。 它们不限于这个项目,换个场景也成立。整节默认收起,点标题展开。

01

先核实依赖,再整块搬运

要搬一段别人的代码过来,先列出它用到的所有东西(函数、常量、工具), 逐个确认目标文件里有没有。有一个缺的就不能搬 —— 否则搬过去是一片报错。

这次用在哪:搬回收站和版本历史前,先逐项核对依赖清单, 结果发现「版本历史」背后整套机制在原版里根本不存在(不是缺几条路由,是整个底座都没有)。 如果没核这一步,会以为搬两条路由就完事。

02

顺序依赖:先写后删

「备份一份,再删原件」这类操作,顺序反了会静默失效 —— 不报错、不崩溃, 只是备份出来的东西永远恢复不回来。这类 bug 最难发现,因为一切看起来都正常。

这次用在哪:回收站必须先「快照」再「摘条目」; 彻底清除必须先「删记录」再「清理数据」。两处顺序反了都会导致数据找不回来。

03

新增一类数据,要登记到所有「谁引用它」的地方

系统里往往有「清理没人用的东西」的逻辑。你新增一种新的数据种类时, 必须让所有这类清理逻辑都认识它 —— 漏一处,你的数据就会被当垃圾清掉。

这次用在哪:回收站和版本历史都是「新的数据种类」, 各自要登记到两处扫描函数里。这是本次最大的坑,也是最有价值的一条经验。

04

再加一层「物理兜底」

逻辑上已经保护好了,但再加一道不依赖逻辑的保险: 用命名前缀做白名单,这类数据永远不允许被自动删除。将来逻辑改错了,也删不掉。

这次用在哪:历史版本的对象名有固定前缀,加进「永不自动删除」的白名单, 并且在删除前再反查一遍引用关系。两道锁。

05

攒起来批量提交,别一条条问

处理大批量数据时,N 次单独请求换成「先把要做的操作攒在内存里,再分批提交」。 速度快几十倍,而且单批内是原子的 —— 顺带解决了「做一半失败了」的问题。

这次用在哪:文件夹改名。200 个文件从约 1000 次排队往返,降到 5 次提交。

06

快速路径 + 保守回退

给老逻辑加一条新快路时,让新路只在条件完全满足时启用, 只要有一点不确定(依赖不可用、目标有冲突),就退回原来的实现。 这样新增的东西不可能破坏既有行为。

这次用在哪:批量搬移只在「数据库可用 + 目标位置是空的」时生效, 否则原样走老的逐条逻辑。

07

统一出口收口

要保证「每处都做同一件事」(加安全头、记日志、统一格式), 别去每处各写一遍 —— 做个统一函数让所有地方都从它过。 这样「漏掉」在结构上就不可能发生。

这次用在哪:安全响应头。9 个页面出口全部改走同一个函数, 以后新加页面自动继承。

08

把结论变成「可数的验证」

不说「应该没问题了」,而是给出一个能数出来的数字: 缺几条、剩几处、多少项通过。结论可复现、可复查,也能在下一次改动后重跑一遍。

这次用在哪:写脚本比对两边的接口清单求差集(缺口 8 → 0); 搜「裸写页面类型的地方」计数必须为 0;把函数抽出来单独跑,逐项断言。

09

先量,再决定要不要优化

想优化体积/性能之前,先测一下能优化多少。 如果收益很小,就果断放弃、改用最简单可靠的方案 —— 别硬啃一个不划算的优化。

这次用在哪:想让图标字体只保留用到的部分来省体积,实测只压到 97.6%, 于是直接放弃,改成内嵌完整字体(代价 231 KB,换来零外部依赖)。

10

改之前先备份,改完另存新名

每次改动不覆盖原文件,而是另存成一个名字里带「这次加了什么」的新文件。 出问题能拿原文件重新改,也能一眼看出每个文件的来历。

这次用在哪:贯穿全程。 从 worker原版0915.js → …改版-去作者+PWA.js → …加回收站版本历史打包下载.js, 每一版都在,都能回退。

如果只记一条:上面 03 那条 ——「新增一类数据,要登记到所有『谁引用它』的地方」。 这次的回收站和版本历史,功能代码全写对了,但如果漏了这一步,做出来的就是个假的回收站: 界面能看、点了能删,就是恢复不回来。而且它不报错。

当前状态

当前文件 worker家人-0928.js
体量 640,621 字节 / 9,269 行(其中 231 KB 是内联字体)
API 路由 43 条 (+ 5 条 PWA)
客户端兼容性 客户端调用的 31 个接口 —— 一条不缺 ✓
语法校验 通过 ✓
页面出口收口 10 处全走统一函数,裸写 text/html 的 0 处 ✓
原文件备份 worker家人-0927.js 保持不动

还没做 / 待验证

✅ 已经在本地验证过的(不依赖真实环境)
⏳ 只能到真机上才能确认的