Skip to content

✨ 增加脚本回收站功能#1585

Open
CodFrm wants to merge 47 commits into
mainfrom
recycle-bin
Open

✨ 增加脚本回收站功能#1585
CodFrm wants to merge 47 commits into
mainfrom
recycle-bin

Conversation

@CodFrm

@CodFrm CodFrm commented Jul 15, 2026

Copy link
Copy Markdown
Member

检查清单

  • 修复已提及的问题
  • 代码通过人工检查
  • 已完成测试

描述

  • 增加脚本回收站、还原、彻底删除、自动到期清理及相关数据保留机制
  • 桌面端与移动端统一提供回收站数量、搜索、来源筛选和清空操作
  • 在回收站内增加自动清理时间弹窗,可选择 7 天、30 天、90 天或永不清理
  • 空回收站仅替换列表内容区,保留 tabs、搜索、筛选、设置入口和表头
  • 桌面批量操作与筛选共用固定高度行,并提供平滑状态动画
  • 移除回收站脚本名称删除线,完善移动端明暗主题与禁用状态

测试

  • 相关 Vitest:5 个文件、12 个用例通过
  • pnpm run typecheck 通过
  • 相关文件 ESLint 通过
  • Rspack development build 通过
  • 真实扩展 Playwright E2E:桌面和移动端 2 个场景通过

截图

trash-ui-ux.zip

desktop-batch-replaces-filter

备注

此前的 #1584 曾被误操作直接合并;main 已恢复到合并前提交,本 PR 用于重新进行正常审阅。

关联 Issue

CodFrm added 23 commits July 15, 2026 00:45
从回收站(trashScript:)彻底销毁脚本及其关联的 scriptCode:/compiled_resource:
数据,并广播既有的 deleteScripts 事件供下游 4 个清理订阅者(value/permission/
resource/favicon)消费。作为回收站功能的销毁终点,暂无生产调用方,由后续任务
接入。
- Toolbar 新增 leading 槽位,tabs 传入后取代「已安装脚本 + 数量」标题
- 回收站顶栏 = tabs + 搜索 + 清空回收站,与已安装 tab 共用同一条 h-14 顶栏
- 保留提示移到 chips 行右侧;批量栏改为独立条件行
- 脚本数量不再丢失:改由 tab 角标显示(已安装 = scriptList.length,回收站 = trashCount)
- 回收站为空时仍渲染带 tabs 的顶栏,避免用户被困在回收站 tab
- prepareScriptByCode 在所有活跃表查找之后增查回收站,命中则复用其 uuid 并置 oldInTrash
- 复用 uuid 即保住 storageName → value 与权限不丢;移出回收站由 installScript 的 upsert 不变量保证
- InstallView.inTrash 透出;主按钮改为 还原 / 还原并更新(按版本是否变化)
- 安装页新增 in_trash_hint 提示条,告知用户还原会保留原有数据
- zh-CN「7 天」→「7天」:对齐同文件既有的「6小时」体例(空格是我在 plan 里带进去的,zh-TW 自由翻译时就没这个问题)
- ja-JP「なし」→「無期限」:该行标签是「ゴミ箱の保存期間」,值为「なし」会被读成「不保存/立刻删」,与「永不清理」语义相反
端到端验证真实扩展时发现:删除脚本后「已安装」列表的行不消失、popup 也不更新,
再点一次删除会报「删除失败: scripts not found」。

根因:Task 3 把删除的广播从 deleteScripts 改为 trashScripts 时,只迁移了 SW 侧的
4 个订阅者(spec 统计的「8 个订阅者」只涵盖 SW 侧),漏了两个页面侧的:
- ScriptList/hooks.ts —— 负责从「已安装」列表移除该行
- popup/usePopupData.ts —— popup 列表

单元测试全部 mock 掉消息层,跨上下文的 topic 不匹配跑不出来,3165 个用例无一发现。

同时新增静态护栏 trash_event_partition.test.ts 钉死事件二分:
- trashScripts(6) = 脚本不再活跃:注销/停 cron/刷 badge/云端删除/列表移行/popup 移行
- deleteScripts(4) = 销毁数据:value/权限/资源/图标(只在彻底删除时触发)
护栏已验证有牙:把任一订阅者订错边,测试立即失败。
@CodFrm CodFrm changed the title 完善回收站功能与清理设置交互 ✨ 增加脚本回收站功能 Jul 15, 2026
@cyfung1031

Copy link
Copy Markdown
Collaborator

1584 不用退回嗎

@cyfung1031

Copy link
Copy Markdown
Collaborator

1584 不用退回嗎

呀好像有force push退回?

@CodFrm

CodFrm commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

1584 不用退回嗎

呀好像有force push退回?

是的,退回了

@cyfung1031

Copy link
Copy Markdown
Collaborator

@CodFrm 刪掉的記錄,特別是代碼部份,可以改用OPFS儲存嗎??
想減少 chrome.storage.local.get 所有的耗時

@CodFrm

CodFrm commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

@CodFrm 刪掉的記錄,特別是代碼部份,可以改用OPFS儲存嗎?? 想減少 chrome.storage.local.get 所有的耗時

确实可以,我调整一下

@cyfung1031

Copy link
Copy Markdown
Collaborator

@CodFrm 刪掉的記錄,特別是代碼部份,可以改用OPFS儲存嗎?? 想減少 chrome.storage.local.get 所有的耗時

确实可以,我调整一下

唉,本來的scriptdao那個代碼我都不知道怎麼migrate了
移到opfs的話 轉不回舊版本。。。
這個處理太麻煩了不想搞。。。

@CodFrm

CodFrm commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

@CodFrm 刪掉的記錄,特別是代碼部份,可以改用OPFS儲存嗎?? 想減少 chrome.storage.local.get 所有的耗時

确实可以,我调整一下

唉,本來的scriptdao那個代碼我都不知道怎麼migrate了 移到opfs的話 轉不回舊版本。。。 這個處理太麻煩了不想搞。。。

这个影响太大了,不要动吧

CodFrm added 16 commits July 15, 2026 17:54
showTrashTab = trashEnabled || trashCount > 0:关闭只影响以后,站内残留条目仍可见可清,
清空后 tab 才消失。桌面/移动两份重复实现各自补上;回落用渲染期状态比较模式(同
Logger/hooks.ts 写法)而非 effect 内同步 setState,避免 react-hooks/set-state-in-effect。
trashEnabled 为 false 时 requestDeleteScripts 已彻底销毁脚本,之前仍复用
「已移入回收站」文案 + 撤销按钮;点击撤销会因 SW 侧找不到回收站记录而
reject,onClick 里的 void undo() 又把这个 rejection 悄悄吞掉,用户毫无
反馈。改为关闭态只提示删除成功、不给撤销入口;顺手给 undo() 补上
try/catch,异常也会走既有的 trash_undo_failed 提示,不再是无声失败。
…与自定义天数边界

- script.test.ts:钉住 TrashScriptDAO 自身不得默认开缓存(否则安装页/编辑器/
  导入页 new 出来的实例会把整个 storage 常驻内存);buildService() 改为直接
  返回 service.trashScriptDAO / service.scriptCodeDAO,不再另 new 一份未缓存
  实例,避免测试今后按“先写后读”之外的顺序断言时命中冷热缓存不一致的假红
- GeneralSection:自定义天数输入框与保留时间下拉的 aria-label 曾同为
  「回收站保留时间」,读屏会报两个同名控件;改用有区分度的
  settings:trash_retention_custom(自定义)
- GeneralSection:切到自定义时用 String(days) 播种草稿,存储值为 0(永不)
  时会播种出非法的 "0"(低于 Input min=1,UI 显示自相矛盾且永远提交不了),
  改为 String(days || 30)
- 测试补齐:自定义天数 1/3650 边界必须接受、3651 必须拒绝;「永不」切自定义
  时草稿播种为合法默认值
把多选态操作栏从 BatchActionsBar 里抽成独立的 SelectionBar + SelectionBarButton,
回收站表格改为复用它,两边不再各写一套。随之删掉重复的 script:trash_selected —
计数文案改由 SelectionBar 内部统一承载。
单条/批量删除确认框按 trash_enabled 分支:开启态改用可还原文案(修正此前
误用「无法撤销」的既有 bug),关闭态保留原「无法撤销」文案。回收站视图的
清理提示与倒计时同样按 trash_enabled 分支:关闭时天数归零、hint 换成
「回收站已关闭」,避免倒数一个 cleanupExpiredTrash 不会再触发的期限。
@cyfung1031

Copy link
Copy Markdown
Collaborator

审查结论

做了一轮完整审查,覆盖了 Service Worker 服务层、消息事件划分、DAO 与缓存、安装、导入、订阅、同步链路、桌面与移动端回收站界面、8 个语言文件,以及相关测试和护栏。

整体设计是扎实的。两阶段删除流程、trashScripts / deleteScripts 的事件划分,以及“先写入回收站、再删除原记录”的落库顺序都比较合理,现有护栏测试也能有效防止事件语义被意外改坏。

审查过程中发现 4 个实际问题,均已在本地修复并补充测试,共拆分为 4 个独立 commit,暂未 push。

已修复问题

1. cleanupTrash 闹钟会在每次 Service Worker 冷启动时重置

原实现每次初始化都会直接调用 chrome.alarms.create。对于同名 alarm,Chrome 会重新创建并重置倒计时。

由于 Service Worker 会被消息、标签页事件等频繁唤醒,12 小时的清理闹钟可能不断重新计时。对于活跃用户,过期脚本可能长期得不到清理。

现已改为先调用 alarms.get,仅在闹钟不存在时创建,与仓库中 checkUpdateagentTaskScheduler 的既有写法保持一致。

此项未新增单元测试。当前测试环境无法直接构造 ServiceWorkerManager 的初始化流程,仓库中另外两处相同模式也暂时没有测试覆盖,相关原因已写入 commit message。

2. 订阅链路删除的脚本没有记录为“订阅删除”

取消订阅和订阅更新移除脚本时,原代码都直接调用:

deleteScript(uuid)

由于没有传入 deleteBy,进入回收站后的来源会默认记为 user,也就是“本机删除”。

结果是回收站中的“订阅”来源筛选实际上永远筛不出这些脚本。

现已在以下两条路径中补传 deleteBy: "subscribe"

  • 删除订阅时移除脚本
  • 更新订阅时移除已经不存在的脚本

同时新增了 2 个测试,分别锁定这两条链路。

3. 回收站操作失败时没有反馈,且“永不清理”的空状态文案不正确

桌面端和移动端回收站都存在相同问题。

当脚本已经被其他窗口、撤销操作或到期清理提前处理时,再点击“还原”或“彻底删除”,Service Worker 会抛出 trash scripts not found

原来的 onRestoreonPurge 没有处理异常,因此会出现:

  • Promise rejection 未被捕获
  • 用户看不到任何错误提示
  • 已失效的条目继续留在列表中

现在操作失败时会显示对应错误提示,并重新拉取回收站列表,使界面恢复到真实状态。

另外,当保留时间设置为“永不清理”,也就是 0 时,原空状态会显示“删除的脚本会保留 0 天”,语义不正确。回收站关闭时继续显示“脚本会先移到这里”也不符合实际行为。

现已新增 trash_empty_desc_never,并同步更新 8 个 locale;回收站关闭时则改用既有的关闭状态提示。

TrashViews.test.tsx 共新增 5 个用例,覆盖桌面端和移动端的失败处理及空状态文案。

4. prepareScriptByCode 查询回收站时没有继承 DAO 缓存设置

备份导入流程会主动启用 ScriptDAO 缓存,然后在循环中逐个处理脚本。

但新加入的回收站查询使用了独立的 TrashScriptDAO,且没有继承 dao.useCache。因此每处理一个脚本,都可能触发一次不带 key 的 chrome.storage.local.get(),重新反序列化整份 storage。

导入脚本数量较多时,会形成明显的重复开销。

现在回收站 DAO 会跟随原 ScriptDAO 的缓存开关,共用模块级缓存。同时新增了 spy 断言,确保该路径不会退化成全量 storage 扫描。

设计层面的观察

以下问题没有在本次审查中修改,建议根据产品语义和 PR 范围决定是否后续处理。

1. 编辑器新建同名脚本可能继承回收站中的旧身份

当前活跃脚本的 name + namespace 查找带有 (!uuid || override) 限制,因此编辑器允许创建同名脚本并作为新脚本保存。

但回收站查找没有相同限制。

用户删除脚本 A 后,如果在编辑器中新建一个相同 @name@namespace 的脚本,保存时可能复用 A 的 UUID,并继承其:

  • status
  • sort
  • selfMetadata
  • subscribeUrl
  • value
  • 权限配置

对于安装、导入和同步流程,这种行为可以理解为“恢复原脚本”;但对于编辑器新建,它会破坏原先允许同名脚本并存的语义。

可以考虑在 byEditor && uuid && !override 时跳过回收站回退。

2. 通过重新安装恢复脚本时,不会清理已经失效的 subscribeUrl

restoreScripts 在恢复脚本时,会检查对应订阅是否仍然存在;若订阅已不存在,会移除 subscribeUrl

但通过安装页恢复时,prepareScriptByCode 命中回收站后会直接继承旧的 subscribeUrl,没有执行相同清理。

两条恢复路径目前行为不一致。

3. 回收站列表不会响应其他窗口的变更

TrashTableTrashCardGrid 只会在挂载及自身操作后重新加载数据,没有订阅消息队列。

因此在多个窗口同时打开时,回收站数量角标会更新,但当前列表可能继续显示旧数据。

本次增加的失败重拉可以在用户操作时修正状态。若要彻底解决,可以让两个视图监听:

  • trashScripts
  • deleteScripts
  • installScript

如果增加订阅,需要同步更新 trash_event_partition.test.ts 中的护栏清单。

4. 到期清理与手动清空之间存在窄竞态

cleanupExpiredTrash 会先调用 all() 取得过期条目,再调用 purgeScripts(expired)

如果用户恰好在两步之间清空回收站,purgeScripts 会因为找不到任何条目而抛出 trash scripts not found,alarm 回调中也没有 catch,最终形成未处理 rejection。

发生窗口较小,可以考虑让自动清理路径在目标已经不存在时直接返回。

5. 备份文件不包含回收站条目

目前备份导出只包含活跃脚本,回收站内容不会随备份迁移。

这意味着换机或恢复备份后,原设备上的回收站条目不会保留。如果这是有意设计,建议在 PR 描述或相关文档中说明。

6. 配置加载完成前,UI 可能短暂误判回收站已开启

前端使用:

trashEnabled ?? true

在异步配置尚未加载完成时,会暂时按“回收站开启”展示删除确认文案和撤销 toast。

实际删除行为由 Service Worker 中的真实配置决定。因此在回收站关闭的情况下,界面可能短暂承诺“可以撤销”,但用户点击后只能得到还原失败。

该窗口通常很短,影响有限。

7. 移动端的已安装列表和回收站共用搜索关键字

移动端切换到回收站后,已安装列表中的搜索内容会继续保留;桌面端回收站则有独立的搜索状态。

这属于交互一致性问题,可以根据预期体验决定是否调整。


整体来看,PR 的核心设计没有问题。上述 4 个修复主要补齐了闹钟初始化、删除来源、异常反馈和导入性能上的边界情况。

@cyfung1031

Copy link
Copy Markdown
Collaborator

@CodFrm 设计层面的观察 的7点你看看需不需要处理吧

cyfung1031 and others added 4 commits July 16, 2026 00:57
chrome.alarms.create 对同名 alarm 会取消重建并重置倒计时;SW 在活跃使用时
频繁冷启动,无条件 create 会让 cleanupTrash 永远数不满 12 小时,到期脚本
一直不被清理。对齐 checkUpdate / agentTaskScheduler 已有的 get 守卫模式。

此改动无单元测试:ServiceWorkerManager 的初始化无法在单测中构造,
同模式的 checkUpdate/agentTaskScheduler 守卫亦无测试覆盖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
取消订阅与订阅更新移除脚本都未传 deleteBy,回收站里这些条目被记成
「本机删除」,来源筛选的「订阅」chip 永远筛不出任何东西。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 还原/彻底删除的条目可能已被其他窗口或到期清理抢先处理,SW 抛
  trash scripts not found 时此前是未捕获的 rejection:界面毫无反馈,
  陈旧行还留在列表里。现在失败会提示错误并重拉列表对齐真实状态。
- 保留时间为「永不」(0) 时空状态说明会插值出「保留 0 天」;关闭态下
  「删除的脚本会先移到这里」也不再成立。分别改用 trash_empty_desc_never
  (8 语言新增)与既有的关闭态提示文案。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
备份导入会给 ScriptDAO 开缓存后在循环里逐脚本调用 prepareScriptByCode;
回收站查找固定走非缓存路径时,每个导入脚本都触发一次全量 storage
反序列化,大备份导入被拖垮。dao 开了缓存时回收站查找也走同一份模块级缓存。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants