From 65a3c879f15bbcf732e8225e9b0f4b37c96429b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 13 Jul 2026 10:21:36 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=E7=BD=91?= =?UTF-8?q?=E7=AB=99=E5=8C=B9=E9=85=8D/=E6=8E=92=E9=99=A4=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=E5=90=8E=E6=B8=85=E7=A9=BA=E5=88=97=E8=A1=A8(?= =?UTF-8?q?=E5=BA=94=E5=9B=9E=E8=90=BD=E8=84=9A=E6=9C=AC=E8=87=AA=E5=B8=A6?= =?UTF-8?q?=E8=A7=84=E5=88=99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScriptEditor/tabs/SettingsPane.test.tsx | 26 +++++++++++++++++++ .../routes/ScriptEditor/tabs/SettingsPane.tsx | 17 +++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx index 63f2b6f06..a5f9e7c4a 100644 --- a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx +++ b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx @@ -259,6 +259,32 @@ describe("SettingsPane 网站匹配/排除", () => { fireEvent.click(screen.getByText(t("confirm"), { selector: "button" })); expect(resetMatch).toHaveBeenCalledWith("u1", undefined); }); + + it("重置匹配后应恢复脚本自带规则而非清空列表", async () => { + render(); + await screen.findByText("*://user.com/*"); + fireEvent.click(screen.getAllByText(t("reset"), { selector: "button" })[0]); + fireEvent.click(screen.getByText(t("confirm"), { selector: "button" })); + // 用户添加的 user.com 被清除,脚本自带的 script.com 应保留并标记为「脚本」 + expect(screen.getByText("*://script.com/*")).toBeInTheDocument(); + expect(screen.queryByText("*://user.com/*")).toBeNull(); + expect(screen.getByText(t("editor:from_script"))).toBeInTheDocument(); + }); + + it("重置排除后应恢复脚本自带规则而非清空列表", async () => { + fetchScript.mockResolvedValue({ + ...sampleScript(), + metadata: { ...sampleScript().metadata, exclude: ["*://ads.script.com/*"] }, + selfMetadata: { ...sampleScript().selfMetadata, exclude: ["*://ads.script.com/*", "*://exclude.com/*"] }, + }); + render(); + await screen.findByText("*://exclude.com/*"); + fireEvent.click(screen.getAllByText(t("reset"), { selector: "button" })[1]); + fireEvent.click(screen.getByText(t("confirm"), { selector: "button" })); + // 用户添加的 exclude.com 被清除,脚本自带的 ads.script.com 应保留 + expect(screen.getByText("*://ads.script.com/*")).toBeInTheDocument(); + expect(screen.queryByText("*://exclude.com/*")).toBeNull(); + }); }); describe("SettingsPane 授权管理(CORS)", () => { diff --git a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx index 2b3ea1f7b..091a7d7ef 100644 --- a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx +++ b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx @@ -234,14 +234,25 @@ function SettingsPaneContent({ uuid, data }: SettingsPaneProps & { data: Setting // ===== 匹配 / 排除 ===== const setMatchList = (kind: "match" | "exclude", next: string[] | undefined) => { + // next 为 undefined 表示重置:删除用户覆盖,生效列表回落到脚本自带的 metadata(与后端 selfMetadataUpdate 一致) + const metaList = kind === "match" ? metaMatch : metaExclude; if (kind === "match") { void scriptClient.resetMatch(uuid, next); - setMatches(next ?? []); + setMatches(next ?? metaList); } else { void scriptClient.resetExclude(uuid, next); - setExcludes(next ?? []); + setExcludes(next ?? metaList); } - patchSelf({ [kind]: next ?? [] }); + setScript((prev) => { + if (!prev) return prev; + const selfMetadata = { ...prev.selfMetadata }; + if (next === undefined) { + delete selfMetadata[kind]; + } else { + selfMetadata[kind] = next; + } + return { ...prev, selfMetadata }; + }); }; const openBulkMatch = (kind: "match" | "exclude") => { setBulkMatchValue(""); From 6e40df369f8acd9ecc0391b6f5e9fbe4684e5576 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:34:32 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20=E5=88=A0=E9=99=A4=E6=9C=80?= =?UTF-8?q?=E5=90=8E=E4=B8=80=E9=A1=B9=E5=8C=B9=E9=85=8D/=E6=8E=92?= =?UTF-8?q?=E9=99=A4=E6=97=B6=E5=9B=9E=E8=90=BD=E8=84=9A=E6=9C=AC=E8=87=AA?= =?UTF-8?q?=E5=B8=A6=E8=A7=84=E5=88=99(=E5=AF=B9=E9=BD=90=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E7=A9=BA=E8=A6=86=E7=9B=96=E5=8D=B3=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E8=AF=AD=E4=B9=89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../ScriptEditor/tabs/SettingsPane.test.tsx | 15 +++++++++++++++ .../routes/ScriptEditor/tabs/SettingsPane.tsx | 12 +++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx index a5f9e7c4a..51ab7e726 100644 --- a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx +++ b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx @@ -285,6 +285,21 @@ describe("SettingsPane 网站匹配/排除", () => { expect(screen.getByText("*://ads.script.com/*")).toBeInTheDocument(); expect(screen.queryByText("*://exclude.com/*")).toBeNull(); }); + + it("删除最后一项匹配后应回落脚本自带规则而非清空列表", async () => { + fetchScript.mockResolvedValue({ + ...sampleScript(), + selfMetadata: { exclude: ["*://exclude.com/*"] }, + }); + render(); + await screen.findByText("*://script.com/*"); + fireEvent.click(screen.getByLabelText(`${t("delete")} *://script.com/*`)); + fireEvent.click(screen.getByText(t("confirm"), { selector: "button" })); + // 后端 selfMetadataUpdate 收到空集即删除覆盖,生效列表回落 metadata.match,脚本自带规则应保留 + expect(resetMatch).toHaveBeenCalledWith("u1", []); + expect(screen.getByText("*://script.com/*")).toBeInTheDocument(); + expect(screen.getByText(t("editor:from_script"))).toBeInTheDocument(); + }); }); describe("SettingsPane 授权管理(CORS)", () => { diff --git a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx index 091a7d7ef..d2200b79e 100644 --- a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx +++ b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx @@ -234,22 +234,24 @@ function SettingsPaneContent({ uuid, data }: SettingsPaneProps & { data: Setting // ===== 匹配 / 排除 ===== const setMatchList = (kind: "match" | "exclude", next: string[] | undefined) => { - // next 为 undefined 表示重置:删除用户覆盖,生效列表回落到脚本自带的 metadata(与后端 selfMetadataUpdate 一致) + // next 为 undefined(重置)或空数组(删除最后一项)时后端 selfMetadataUpdate 均删除用户覆盖, + // 生效列表回落到脚本自带的 metadata,前端乐观更新保持一致 + const override = next?.length ? next : undefined; const metaList = kind === "match" ? metaMatch : metaExclude; if (kind === "match") { void scriptClient.resetMatch(uuid, next); - setMatches(next ?? metaList); + setMatches(override ?? metaList); } else { void scriptClient.resetExclude(uuid, next); - setExcludes(next ?? metaList); + setExcludes(override ?? metaList); } setScript((prev) => { if (!prev) return prev; const selfMetadata = { ...prev.selfMetadata }; - if (next === undefined) { + if (override === undefined) { delete selfMetadata[kind]; } else { - selfMetadata[kind] = next; + selfMetadata[kind] = override; } return { ...prev, selfMetadata }; }); From d61759d6be9da7a5b28baa5fc9c57a6d9d4aa9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 16 Jul 2026 15:55:17 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20selfMetadata=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=A9=BA=E8=A6=86=E7=9B=96=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20tag/exclude/run-at=20=E7=9A=84=E5=88=A0=E9=99=A4=E5=8D=B3?= =?UTF-8?q?=E5=A4=8D=E6=B4=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:selfMetadataUpdate 把空集合一律当成「删除覆盖」,导致用户永远无法 显式清空脚本自带的 tag / match / exclude——删到空后覆盖被删、生效值回落 metadata,刷新即复活。改为区分 undefined(撤销覆盖) 与空集合(显式清空): - selfMetadataUpdate 接受 Set|undefined,空集合持久化为 [] - resetMatch/resetExclude/updateMetadata RPC 边界区分 undefined 与 [] (excludeUrl 由根因修复自动受益,popup 取消最后一条排除不再复活) - SettingsPane 前端对齐:重置传 undefined、编辑传数组;删除最后一项 = 显式清空 - 修复 onRunAt 选非 early-start 时下拉框回弹 early-start(空数组被当真值); run-in/run-at 选「默认」改传 undefined 撤销覆盖,保持行为不变 - 重写 8 个语言包 after_deleting_{match,exclude}_item 过时且自相矛盾的文案 对应 review 提出的 4 项遗漏路径(标签/popup 排除/文案/run-at) --- src/app/service/service_worker/client.ts | 3 +- src/app/service/service_worker/script.test.ts | 163 ++++++++++++++++++ src/app/service/service_worker/script.ts | 14 +- src/app/service/service_worker/utils.test.ts | 26 ++- src/app/service/service_worker/utils.ts | 11 +- src/locales/de-DE/editor.json | 4 +- src/locales/en-US/editor.json | 4 +- src/locales/ja-JP/editor.json | 4 +- src/locales/ru-RU/editor.json | 4 +- src/locales/tr-TR/editor.json | 4 +- src/locales/vi-VN/editor.json | 4 +- src/locales/zh-CN/editor.json | 4 +- src/locales/zh-TW/editor.json | 4 +- .../ScriptEditor/tabs/SettingsPane.test.tsx | 39 ++++- .../routes/ScriptEditor/tabs/SettingsPane.tsx | 46 ++--- 15 files changed, 278 insertions(+), 56 deletions(-) create mode 100644 src/app/service/service_worker/script.test.ts diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 8e874ce95..f11293570 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -118,7 +118,8 @@ export class ScriptClient extends Client { return this.do("setCheckUpdateUrl", { uuid, checkUpdate, checkUpdateUrl }); } - updateMetadata(uuid: string, key: string, value: string[]) { + // value 为 undefined 表示撤销用户覆盖,生效值回落脚本自带 metadata + updateMetadata(uuid: string, key: string, value: string[] | undefined) { return this.do("updateMetadata", { uuid, key, value }); } async getBatchUpdateRecordLite(i: number) { diff --git a/src/app/service/service_worker/script.test.ts b/src/app/service/service_worker/script.test.ts new file mode 100644 index 000000000..a69116b17 --- /dev/null +++ b/src/app/service/service_worker/script.test.ts @@ -0,0 +1,163 @@ +import { initTestEnv } from "@Tests/utils"; +import { ScriptService } from "./script"; +import { vi, describe, it, expect, beforeEach } from "vitest"; +import { randomUUID } from "crypto"; +import type { Script, ScriptDAO } from "@App/app/repo/scripts"; +import { SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts"; +import type { Group } from "@Packages/message/server"; +import { Server } from "@Packages/message/server"; +import type { IMessageQueue } from "@Packages/message/message_queue"; +import { MessageQueue } from "@Packages/message/message_queue"; +import { MockMessage } from "@Packages/message/mock_message"; +import EventEmitter from "eventemitter3"; +import type { SystemConfig } from "@App/pkg/config/config"; +import type { ValueService } from "./value"; +import type { ResourceService } from "./resource"; + +initTestEnv(); + +/** + * selfMetadata 是「用户对脚本自带 @metadata 的覆盖」。 + * undefined 表示撤销覆盖(生效值回落脚本自带 metadata),空数组表示用户显式清空,两者语义不同。 + */ +describe("ScriptService selfMetadata 用户覆盖", () => { + let scriptService: ScriptService; + let mockScriptDAO: ScriptDAO; + let mockGroup: Group; + let mockMessageQueue: IMessageQueue; + + const createMockScript = (overrides: Partial