Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5b631fc
feat(trash): 新增 TrashScriptDAO 与 TrashScript 类型
CodFrm Jul 14, 2026
eafd990
feat(trash): 新增 purgeScripts 彻底删除入口
CodFrm Jul 14, 2026
a7bc855
feat(trash): 删除脚本改为移入回收站,4 个订阅者迁移至 trashScripts
CodFrm Jul 15, 2026
f6d4512
test(trash): 断言回收站期间 value 冻结、彻底删除后才清理
CodFrm Jul 15, 2026
0e768ea
feat(trash): 新增 restoreScripts,含同名冲突拒绝与孤儿订阅清理
CodFrm Jul 15, 2026
c055036
fix(trash): installScript 落库前清除回收站同 uuid 条目,守住两表互斥不变量
CodFrm Jul 15, 2026
b8f6108
fix(trash): value 清理需同时检查回收站,避免误删共享 storagename 的数据
CodFrm Jul 15, 2026
ab6064e
feat(trash): 保留时间配置(默认30天/0=永不)与到期自动清理 alarm
CodFrm Jul 15, 2026
f882e8a
feat(trash): 新增 restores/purges/getTrashScripts 的 client 与 store 包装
CodFrm Jul 15, 2026
8b2263a
i18n(trash): 新增回收站相关文案 key(8 个 locale)
CodFrm Jul 15, 2026
9852f84
i18n(trash): 补充撤销 toast 与安装页还原态文案(script/install,8 个 locale)
CodFrm Jul 15, 2026
2ed47b5
feat(trash): 回收站 UI —— 脚本列表新增回收站 tab 与 TrashTable
CodFrm Jul 15, 2026
0e127df
feat(trash): 删除后提供撤销 toast
CodFrm Jul 15, 2026
c593bbb
feat(trash): 回收站支持按名称/命名空间搜索,补齐设计稿的搜索框
CodFrm Jul 15, 2026
2a5a343
feat(trash): 移动端回收站 —— TrashCardGrid 与移动端 tab
CodFrm Jul 15, 2026
36d5b1d
refactor(trash): 顶栏按设计稿对齐 —— tabs 取代标题槽位,数量改由 tab 角标承载
CodFrm Jul 15, 2026
32fadff
feat(trash): 安装页复用回收站脚本身份,按钮改为还原/还原并更新
CodFrm Jul 15, 2026
58c7704
i18n(trash): 新增回收站保留时间设置项文案(settings,8 个 locale)
CodFrm Jul 15, 2026
0909f66
feat(trash): 设置页新增回收站保留时间选项
CodFrm Jul 15, 2026
2623618
i18n(trash): 修正保留时间文案两处
CodFrm Jul 15, 2026
0650313
fix(trash): 页面侧订阅者漏迁移导致删除后列表不刷新
CodFrm Jul 15, 2026
60894e9
feat(trash): 移动端 tab 补上数量角标,与桌面端及设计稿一致
CodFrm Jul 15, 2026
21afc87
✨ 完善回收站清理设置与空状态交互
CodFrm Jul 15, 2026
d042192
perf(trash): 回收站 DAO 开启缓存,消除每次读取的全量存储扫描
CodFrm Jul 15, 2026
08a54da
feat(trash): 新增 trash_enabled 配置,关闭时删除直接销毁并发停用/销毁双事件
CodFrm Jul 15, 2026
83dc0cb
feat(trash): 回收站关闭时不再自动清理残留条目
CodFrm Jul 15, 2026
b5e38c5
fix(test): 补清 sync storage,消除 trash_enabled 跨用例泄漏
CodFrm Jul 15, 2026
c951ce3
i18n(trash): 新增回收站启用开关与自定义天数文案(8 个 locale)
CodFrm Jul 15, 2026
19372e8
feat(trash): 设置页新增回收站启用开关与自定义保留天数
CodFrm Jul 15, 2026
93f29d5
perf(trash): 角标计数改为订阅回收站相关事件,排序/开关不再重拉回收站
CodFrm Jul 15, 2026
b792070
test(trash): 覆盖 useTrashCount 的选择性订阅,钉住排序/开关不重拉回收站
CodFrm Jul 15, 2026
0a2d407
feat(trash): 回收站关闭且清空后隐藏回收站 tab,并回落到已安装
CodFrm Jul 15, 2026
14f4364
test(trash): 钉住回收站 tab 消失时回落到已安装
CodFrm Jul 15, 2026
bcc8e9c
test(trash): 护栏登记 useTrashCount 对 deleteScripts 的只读订阅
CodFrm Jul 15, 2026
432bd96
fix(trash): 关闭回收站后删除提示求真,兜底撤销失败的静默 rejection
CodFrm Jul 15, 2026
3b9ee34
test(trash): 补齐 DAO 缓存反向断言 + buildService 实例共享;fix(settings): a11y 重名…
CodFrm Jul 15, 2026
ac09779
refactor(trash): 抽取共享 SelectionBar,回收站与批量栏复用同一多选态外壳
CodFrm Jul 15, 2026
dd001a3
i18n(trash): 新增关闭态提示与开启态删除确认文案(8 个 locale)
CodFrm Jul 15, 2026
eec2f58
refactor(trash): 移除回收站内的保留时间弹窗,设置入口改为跳转设置页
CodFrm Jul 15, 2026
06e135f
🐛 完善回收站脚本信息与空标签显隐
CodFrm Jul 15, 2026
c5ee96e
🐛 空回收站恢复脚本列表原布局
CodFrm Jul 15, 2026
94f6e72
feat(trash): 关闭态删除文案不再承诺可还原,并修正开启态「无法撤销」的错误措辞
CodFrm Jul 15, 2026
03ee478
fix(settings): 自定义保留天数改为失焦提交,不再逐键写 sync 存储
CodFrm Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/app/repo/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
let loadCachePromise: Promise<Partial<Record<string, any>>> | undefined = undefined;
let cache: Partial<Record<string, any>> | undefined = undefined;

// 仅测试用:清除全局缓存,使后续读操作重新加载存储数据
export function clearCacheForTest(): void {
cache = undefined;
loadCachePromise = undefined;
}

// 加载数据到缓存
export function loadCache(): Promise<Partial<Record<string, any>>> {
if (cache) {
Expand Down
61 changes: 61 additions & 0 deletions src/app/repo/trash_script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, it, expect, beforeEach } from "vitest";
import { initTestEnv } from "@Tests/utils";
import { TrashScriptDAO, type TrashScript } from "./trash_script";
import { ScriptDAO, SCRIPT_TYPE_NORMAL, SCRIPT_STATUS_ENABLE, SCRIPT_RUN_STATUS_COMPLETE } from "./scripts";

initTestEnv();

const makeTrashScript = (overrides: Partial<TrashScript> = {}): TrashScript => ({
uuid: "uuid-trash-1",
name: "测试脚本",
namespace: "test-namespace",
type: SCRIPT_TYPE_NORMAL,
status: SCRIPT_STATUS_ENABLE,
sort: 0,
runStatus: SCRIPT_RUN_STATUS_COMPLETE,
createtime: Date.now(),
checktime: Date.now(),
metadata: {},
deleteTime: Date.now(),
deleteBy: "user",
...overrides,
});

describe("TrashScriptDAO", () => {
let dao: TrashScriptDAO;
let scriptDAO: ScriptDAO;

beforeEach(async () => {
await chrome.storage.local.clear();
dao = new TrashScriptDAO();
scriptDAO = new ScriptDAO();
});

it("应能保存回收站脚本并按 uuid 取回,且保留删除元数据", async () => {
await dao.save(makeTrashScript({ uuid: "u1", deleteTime: 1234, deleteBy: "sync" }));
const got = await dao.get("u1");
expect(got?.uuid).toBe("u1");
expect(got?.deleteTime).toBe(1234);
expect(got?.deleteBy).toBe("sync");
});

it("回收站脚本不得出现在 ScriptDAO 的查询结果中(前缀隔离)", async () => {
await dao.save(makeTrashScript({ uuid: "u2", name: "只在回收站" }));
const all = await scriptDAO.all();
expect(all.find((s) => s.uuid === "u2")).toBeUndefined();
expect(await scriptDAO.get("u2")).toBeUndefined();
});

it("应能按 name+namespace 查回收站脚本", async () => {
await dao.save(makeTrashScript({ uuid: "u3", name: "查我", namespace: "ns-a" }));
expect((await dao.findByNameAndNamespace("查我", "ns-a"))?.uuid).toBe("u3");
expect(await dao.findByNameAndNamespace("查我", "ns-b")).toBeUndefined();
});

it("应能批量删除回收站脚本", async () => {
await dao.save(makeTrashScript({ uuid: "u4" }));
await dao.save(makeTrashScript({ uuid: "u5" }));
await dao.deletes(["u4", "u5"]);
expect(await dao.all()).toHaveLength(0);
});
});
27 changes: 27 additions & 0 deletions src/app/repo/trash_script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Repo } from "./repo";
import type { Script } from "./scripts";
import type { InstallSource } from "../service/service_worker/types";

/** 回收站中的脚本:原脚本 + 删除元数据 */
export interface TrashScript extends Script {
/** 进入回收站的时间戳(毫秒) */
deleteTime: number;
/** 删除来源 */
deleteBy: InstallSource;
}

export class TrashScriptDAO extends Repo<TrashScript> {
constructor() {
super("trashScript");
}

public save(val: TrashScript) {
return super._save(val.uuid, val);
}

public findByNameAndNamespace(name: string, namespace: string) {
return this.findOne((key, value) => {
return value.name === name && (!namespace || value.namespace === namespace);
});
}
}
2 changes: 1 addition & 1 deletion src/app/service/offscreen/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class ScriptService {
disableScript(this.windowMessage, data.script.uuid);
}
});
this.messageQueue.subscribe<TDeleteScript[]>("deleteScripts", async (data) => {
this.messageQueue.subscribe<TDeleteScript[]>("trashScripts", async (data) => {
for (const { uuid, type } of data) {
// 只发送后台脚本和定时脚本
if (type === SCRIPT_TYPE_BACKGROUND || type === SCRIPT_TYPE_CRONTAB) {
Expand Down
14 changes: 14 additions & 0 deletions src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import type {
ScriptService,
TCheckScriptUpdateOption,
TOpenBatchUpdatePageOption,
TRestoreResult,
TScriptInstallParam,
TScriptInstallReturn,
} from "./script";
import type { TrashScript } from "@App/app/repo/trash_script";
import { encodeRValue, type TKeyValuePair } from "@App/pkg/utils/message_value";
import { type TSetValuesParams } from "./value";
import type { LocalBackupExport } from "./synchronize";
Expand Down Expand Up @@ -60,6 +62,18 @@ export class ScriptClient extends Client {
return this.do("deletes", uuids);
}

restores(uuids: string[]) {
return this.do<TRestoreResult>("restores", uuids);
}

purges(uuids: string[]) {
return this.do<boolean>("purges", uuids);
}

getTrashScripts() {
return this.do<TrashScript[]>("getTrashScripts");
}

enable(uuid: string, enable: boolean) {
return this.do("enable", { uuid, enable });
}
Expand Down
6 changes: 6 additions & 0 deletions src/app/service/service_worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ export default class ServiceWorkerManager {
// 避免浏览器打开时立即清除。先等tabs载入一下
setTimeout(cleanupStaleTempStorageEntries, needsWarmupDelay ? 45_000 : 100);
break;
case "cleanupTrash":
script.cleanupExpiredTrash();
break;
}
});
// 12小时检查一次扩展更新
Expand Down Expand Up @@ -246,6 +249,9 @@ export default class ServiceWorkerManager {
// 定期清理过期的临时安装信息
chrome.alarms.create("cleanupTempStorage", { periodInMinutes: 30 });

// 定期清理回收站中过期的脚本(30 天精度无需更细)
chrome.alarms.create("cleanupTrash", { periodInMinutes: 12 * 60 });

// 一些只需启动时运行一次的任务
cacheInstance.getOrSet("extension_initialized", () => {
// 启动一次云同步
Expand Down
6 changes: 4 additions & 2 deletions src/app/service/service_worker/popup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ describe("PopupService 删除脚本后 Popup 菜单残留清理", () => {
expect(scriptDAO.gets).toHaveBeenCalledWith([deletedUuid]);
});

it("deleteScripts 事件应从所有标签页缓存(包含后台 -1)清除已删脚本,并清理待处理菜单命令", async () => {
it("trashScripts 事件应从所有标签页缓存(包含后台 -1)清除已删脚本,并清理待处理菜单命令", async () => {
const deletedUuid = "deleted-script";
const liveUuid = "live-script";
await cacheInstance.set(`${CACHE_KEY_TAB_SCRIPT}${1}`, [createMenu(deletedUuid), createMenu(liveUuid)]);
Expand All @@ -163,7 +163,9 @@ describe("PopupService 删除脚本后 Popup 菜单残留清理", () => {
] as never);
service.dealBackgroundScriptInstall();

const [deleteHandler] = subscriptions.get("deleteScripts") || [];
// 删除脚本现在语义为“移入回收站”,该清理逻辑随 popup.ts 的订阅一并迁移到了 trashScripts 事件
// (事件名变更,handler 本身未变)。
const [deleteHandler] = subscriptions.get("trashScripts") || [];
expect(deleteHandler).toBeDefined();
await deleteHandler!([
{ uuid: deletedUuid, storageName: "deleted-script", type: SCRIPT_TYPE_NORMAL },
Expand Down
2 changes: 1 addition & 1 deletion src/app/service/service_worker/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ export class PopupService {
}
});
});
this.mq.subscribe<TDeleteScript[]>("deleteScripts", async (data) => {
this.mq.subscribe<TDeleteScript[]>("trashScripts", async (data) => {
const changed = await this.removeDeletedScriptsFromPopupCaches(data.map(({ uuid }) => uuid));
if (changed) {
this.updateBadgeIcon();
Expand Down
2 changes: 1 addition & 1 deletion src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ export class RuntimeService {
});

// 监听脚本删除
this.mq.subscribe<TDeleteScript[]>("deleteScripts", async (data) => {
this.mq.subscribe<TDeleteScript[]>("trashScripts", async (data) => {
// 显式提前失效,确保在任何 await 之前就清掉陈旧的 disabled 匹配器。
// 虽然下方 updateSorter() 内部也会调用 invalidateDisabledMatcher(),但此处与 installScript 保持一致:
// 由于 updateSorter() 位于 await 之后,必须先在最前面显式失效一次。
Expand Down
Loading
Loading