Skip to content

Commit 42f9516

Browse files
committed
✨ Popup 站点范围快捷操作常驻,关闭本站执行优先移出匹配
站点范围快捷操作不再需要在设置里开启:删除 popup_site_scope_actions 配置项与随之只在关闭态使用的 excludeUrl 全链路。 「排除在 $0 上执行」改为优先把网域与当前 host 完全相等的 @match 移出匹配 列表,只有移完仍命中当前网址(通配 @match@include、正则等移不动的 规则)才追加 @exclude。排除会冻结作者 @exclude 并让匹配与排除自相矛盾, 只在删不掉时才用。通配子域匹配不移除,否则会连带关掉兄弟子域。 匹配被移空的脚本此前会连原始规则一起从匹配器清掉,从 Popup 消失、无法 再用「允许在 X 执行」恢复;现在生效规则为空但原始规则还在时继续解析并 注册原始规则。相应地,注册前须挡下一条 inclusion 都不剩的脚本—— getApiMatchesAndGlobs 对这种规则集会退回 *://*/*,注册出去等于全站运行。 顺带删掉 exclude_on:#1646 把三态压成 exclude_off 单键后它就没有调用点, 本次移除 excludeUrl 后更不可能再被用到。
1 parent c6fede1 commit 42f9516

42 files changed

Lines changed: 229 additions & 334 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/references/terminology-zh-TW.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
| 目前詞彙 | 建議判斷 | 目前受影響 key |
5353
| --- | --- | --- |
5454
| `目錄` | 指 filesystem directory 的介面動作用 `資料夾`;文章或文件的內容目錄仍用 `目錄`| `open_backup_dir`, `open_directory`, `script_operation_description`, `get_backup_dir_url_failed` |
55-
| `恢復` | restore settings/default values 用 `還原`;resume operation 或 recover 依語意使用 `恢復` / `復原`| `exclude_on`, `restore_default_values` |
55+
| `恢復` | restore settings/default values 用 `還原`;resume operation 或 recover 依語意使用 `恢復` / `復原`| `restore_default_values` |
5656
| `拉取` | Git pull 可用 `拉取`;從雲端取得備份或資料的 UI 動作用 `下載` / `擷取` / `同步取得`| `pulling_data_from_cloud`, `pull_failed` |
5757
| `保存` / `儲存` | UI 的 save 動作用 `儲存`;保存期限、保存證據或一般敘述仍可使用 `保存`| `tools_backup_content` |
5858
| `設備` / `裝置` | 使用者持有、同步或連線的 device 介面用 `裝置`;equipment 或設備管理等語境仍可使用 `設備`| `setting_sync_content` |

e2e/popup-matching-regressions.spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ async function verifyExcludeRoundTrip(
3939
const script = initial.data.scriptList.find((item: { name: string }) => item.name === scriptName);
4040
if (!script) throw new Error(`script missing initially: ${JSON.stringify(initial.data.scriptList)}`);
4141

42+
const host = new URL(url).host;
4243
const exclude = await chrome.runtime.sendMessage({
43-
action: "serviceWorker/script/excludeUrl",
44-
data: { uuid: script.uuid, excludePattern: "*://sitea.test/*", remove: false },
44+
action: "serviceWorker/script/excludeFromMatch",
45+
data: { uuid: script.uuid, host, url },
4546
});
4647
if (exclude.code) throw new Error(`exclude failed: ${JSON.stringify(exclude)}`);
4748

@@ -50,8 +51,8 @@ async function verifyExcludeRoundTrip(
5051
if (!excluded) throw new Error("script disappeared from Popup after exclusion");
5152

5253
const unexclude = await chrome.runtime.sendMessage({
53-
action: "serviceWorker/script/excludeUrl",
54-
data: { uuid: script.uuid, excludePattern: "*://sitea.test/*", remove: true },
54+
action: "serviceWorker/script/allowUrl",
55+
data: { uuid: script.uuid, matchPattern: `*://${host}/*`, excludePattern: `*://${host}/*` },
5556
});
5657
if (unexclude.code) throw new Error(`unexclude failed: ${JSON.stringify(unexclude)}`);
5758

src/app/service/service_worker/client.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,6 @@ export class ScriptClient extends Client {
101101
return this.doThrow("getScriptRunResourceByUUID", uuid);
102102
}
103103

104-
excludeUrl(uuid: string, excludePattern: string, remove: boolean) {
105-
return this.do("excludeUrl", { uuid, excludePattern, remove });
106-
}
107-
108104
onlyRunOnUrl(uuid: string, matchPattern: string) {
109105
return this.do("onlyRunOnUrl", { uuid, matchPattern });
110106
}
@@ -113,8 +109,8 @@ export class ScriptClient extends Client {
113109
return this.do("allowUrl", { uuid, matchPattern, excludePattern });
114110
}
115111

116-
excludeFromMatch(uuid: string, matchPattern: string) {
117-
return this.do("excludeFromMatch", { uuid, matchPattern });
112+
excludeFromMatch(uuid: string, host: string, url: string) {
113+
return this.do("excludeFromMatch", { uuid, host, url });
118114
}
119115

120116
// 重置匹配项

src/app/service/service_worker/runtime.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹
288288
});
289289
});
290290

291-
it.concurrent("match 覆盖清空后不再保留此前的匹配规则", async () => {
291+
it.concurrent("match 覆盖清空后此前的匹配规则不再生效,但仍以未生效列出", async () => {
292292
const { runtime } = createRuntimeTestContext();
293293
const script = createMockScript({
294294
metadata: { match: ["https://www.example.com/*"] },
@@ -302,9 +302,24 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹
302302
...script,
303303
selfMetadata: { match: [] },
304304
});
305-
expect(await runtime.applyScriptMatchInfo(emptyMatchOverride)).toBeUndefined();
305+
await runtime.applyScriptMatchInfo(emptyMatchOverride);
306306

307-
expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/", true).has(script.uuid)).toBe(false);
307+
expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/").has(script.uuid)).toBe(false);
308+
// 原始规则仍在匹配器内,Popup 才能把它列为未生效并给出「允许在此执行」的恢复入口
309+
expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/", true).get(script.uuid)?.effective).toBe(
310+
false
311+
);
312+
});
313+
314+
it.concurrent("match 覆盖清空的脚本不应被注册(空规则会被 UserScripts API 退回成全站匹配)", async () => {
315+
const { runtime } = createRuntimeTestContext();
316+
(runtime as any).resource = { getScriptResourceValue: vi.fn().mockResolvedValue({}) };
317+
const script = createMockScript({
318+
metadata: { match: ["https://www.example.com/*"] },
319+
selfMetadata: { match: [] },
320+
});
321+
322+
expect(await runtime.buildAndSaveCompiledResourceFromScript(script)).toBeUndefined();
308323
});
309324

310325
it.concurrent("空匹配覆盖时应删除持久化 CompiledResource 并注销旧注册", async () => {

src/app/service/service_worker/runtime.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import Logger from "@App/app/logger/logger";
5252
import type { GMInfoEnv, ValueUpdateDataEncoded } from "../content/types";
5353
import { initLocalesPromise, localePath } from "@App/locales/locales";
5454
import { DocumentationSite } from "@App/app/const";
55-
import { extractUrlPatterns, RuleType, type URLRuleEntry } from "@App/pkg/utils/url_matcher";
55+
import { extractUrlPatterns, RuleType, RuleTypeBit, type URLRuleEntry } from "@App/pkg/utils/url_matcher";
5656
import { parseUserConfig } from "@App/pkg/utils/yaml";
5757
import type { CompiledResource, Resource, ResourceType } from "@App/app/repo/resource";
5858
import { CompiledResourceDAO, CompiledResourceNamespace } from "@App/app/repo/resource";
@@ -456,9 +456,9 @@ export class RuntimeService {
456456
// 安装,启用,或earlyStartScript的value更新
457457
const ret = await this.buildAndSaveCompiledResourceFromScript(script, true);
458458
if (!ret) {
459-
// 空匹配覆盖(match 与 include 均为空)时脚本不再匹配任何站点。
460-
// 内存 matcher 已由 applyScriptMatchInfo 清空,这里再清掉持久化的 CompiledResource
461-
// 并注销浏览器旧注册,否则 SW 重启后 waitInit 会信任旧资源、让旧范围复活。
459+
// 空匹配覆盖(match 与 include 均为空)时脚本不再匹配任何站点。内存 matcher 里只剩
460+
// 供 Popup 恢复用的原始规则,这里再清掉持久化的 CompiledResource 并注销浏览器旧注册,
461+
// 否则 SW 重启后 waitInit 会信任旧资源、让旧范围复活。
462462
await this.compiledResourceDAO.delete(script.uuid);
463463
await this.unregistryPageScripts([script.uuid]);
464464
return;
@@ -855,6 +855,9 @@ export class RuntimeService {
855855
const resourceUrls = (script.metadata["require"] || []).map((res) => resources[res]?.url).filter((res) => res);
856856
const scriptMatchInfo = await this.applyScriptMatchInfo(scriptRes);
857857
if (!scriptMatchInfo) return undefined;
858+
// 生效规则一条 inclusion 都不剩(用户把当前站点从匹配中移除后可能如此)时不能注册:
859+
// getApiMatchesAndGlobs 对没有 match pattern 的规则集会退回 *://*/*,注册出去等于全站运行。
860+
if (!scriptMatchInfo.scriptUrlPatterns.some((rule) => rule.ruleType & RuleTypeBit.INCLUSION)) return undefined;
858861

859862
const res = getUserScriptRegister(scriptMatchInfo);
860863
const registerScript = res.registerScript;

src/app/service/service_worker/script.test.ts

Lines changed: 76 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -723,11 +723,11 @@ describe("ScriptClient 站点范围消息", () => {
723723
const sendMessage = vi.fn().mockResolvedValue({ data: true });
724724
const client = new ScriptClient({ sendMessage } as unknown as MessageSend);
725725

726-
await client.excludeFromMatch("script-uuid", "*://current.example/*");
726+
await client.excludeFromMatch("script-uuid", "current.example", "https://current.example/page");
727727

728728
expect(sendMessage).toHaveBeenCalledWith({
729729
action: "serviceWorker/script/excludeFromMatch",
730-
data: { uuid: "script-uuid", matchPattern: "*://current.example/*" },
730+
data: { uuid: "script-uuid", host: "current.example", url: "https://current.example/page" },
731731
});
732732
});
733733
});
@@ -786,51 +786,10 @@ describe("ScriptService selfMetadata 用户覆盖", () => {
786786
);
787787
});
788788

789-
describe("excludeUrl - popup 排除/取消排除", () => {
790-
it("取消最后一条排除后应保存空覆盖,而不是回落脚本自带的 exclude", async () => {
791-
const script = createMockScript();
792-
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
793-
794-
await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://ads.script.com/*", remove: true });
795-
796-
expect(savedSelfMetadata()).toEqual({ exclude: [] });
797-
});
798-
799-
it("取消排除后仍有其他规则时应保存剩余规则", async () => {
800-
const script = createMockScript({
801-
selfMetadata: { exclude: ["*://ads.script.com/*", "*://user.com/*"] },
802-
});
803-
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
804-
805-
await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://user.com/*", remove: true });
806-
807-
expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*"] });
808-
});
809-
810-
it("排除新网站时应追加到覆盖中", async () => {
811-
const script = createMockScript();
812-
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
813-
814-
await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://user.com/*", remove: false });
815-
816-
expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*", "*://user.com/*"] });
817-
});
818-
819-
it("已有用户排除覆盖时新增排除应同时保留作者与用户规则", async () => {
820-
const script = createMockScript({
821-
selfMetadata: { exclude: ["*://user-blocked.example/*"] },
822-
});
823-
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
824-
825-
await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://new.example/*", remove: false });
826-
827-
expect(savedSelfMetadata()).toEqual({
828-
exclude: ["*://ads.script.com/*", "*://user-blocked.example/*", "*://new.example/*"],
829-
});
830-
});
831-
});
832-
833789
describe("popup 站点范围快捷操作", () => {
790+
const host = "current.example";
791+
const url = "https://current.example/page";
792+
834793
it("初始化时应注册排除已匹配站点操作", async () => {
835794
const alarmsDescriptor = Object.getOwnPropertyDescriptor(chrome, "alarms");
836795
Object.defineProperty(chrome, "alarms", {
@@ -894,17 +853,14 @@ describe("ScriptService selfMetadata 用户覆盖", () => {
894853

895854
const onlyRun = scriptService.onlyRunOnUrl({ uuid: stored.uuid, matchPattern: "*://current.example/*" });
896855
await firstUpdateStarted;
897-
const exclude = scriptService.excludeFromMatch({ uuid: stored.uuid, matchPattern: "*://current.example/*" });
856+
const exclude = scriptService.excludeFromMatch({ uuid: stored.uuid, host, url });
898857
await Promise.resolve();
899858
await Promise.resolve();
900859
releaseFirstUpdate();
901860
await Promise.all([onlyRun, exclude]);
902861

903-
expect(stored.selfMetadata).toEqual({
904-
match: [],
905-
include: [],
906-
exclude: ["*://ads.script.com/*", "*://current.example/*"],
907-
});
862+
// 串行执行才能让 excludeFromMatch 看到 onlyRunOnUrl 写入的匹配覆盖并把它移出
863+
expect(stored.selfMetadata).toEqual({ match: [], include: [] });
908864
});
909865

910866
it("并发 onlyRunOnUrl 与 resetMatch 应串行执行并保留两次读改写", async () => {
@@ -1012,86 +968,118 @@ describe("ScriptService selfMetadata 用户覆盖", () => {
1012968
});
1013969
});
1014970

1015-
it("排除已包含站点时应移出用户匹配并加入用户排除", async () => {
971+
it("匹配中有当前站点的专属规则时应只移出匹配,不写入排除", async () => {
1016972
const script = createMockScript({
1017-
selfMetadata: {
1018-
match: ["*://allowed.example/*", "*://current.example/*"],
1019-
exclude: ["*://blocked.example/*"],
1020-
},
973+
metadata: { match: ["*://current.example/*", "*://other.example/*"] },
974+
selfMetadata: { exclude: ["*://blocked.example/*"] },
1021975
});
1022976
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
1023977

1024-
await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
978+
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });
1025979

1026-
// 作者 @exclude(ads.script.com)并入用户覆盖,避免用户覆盖整体替换作者规则
980+
// 移出匹配后脚本已不在本站生效,无需再写排除,用户的排除列表保持原样
1027981
expect(savedSelfMetadata()).toEqual({
1028-
match: ["*://allowed.example/*"],
1029-
exclude: ["*://ads.script.com/*", "*://blocked.example/*", "*://current.example/*"],
982+
match: ["*://other.example/*"],
983+
exclude: ["*://blocked.example/*"],
1030984
});
1031985
});
1032986

1033-
it("排除最后一个用户匹配时应保留显式空匹配覆盖", async () => {
1034-
const script = createMockScript({ selfMetadata: { match: ["*://current.example/*"] } });
987+
it("同一站点的多条路径匹配应一并移出", async () => {
988+
const script = createMockScript({
989+
metadata: { match: ["https://current.example/a*", "http://current.example/b*", "*://other.example/*"] },
990+
});
1035991
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
1036992

1037-
await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
993+
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });
1038994

1039-
expect(savedSelfMetadata()).toEqual({
1040-
match: [],
1041-
exclude: ["*://ads.script.com/*", "*://current.example/*"],
1042-
});
995+
expect(savedSelfMetadata()).toEqual({ match: ["*://other.example/*"] });
1043996
});
1044997

1045-
it("没有用户匹配覆盖时排除站点不应创建匹配覆盖", async () => {
1046-
const script = createMockScript();
998+
it("移出的是最后一条匹配时应保留显式空匹配覆盖", async () => {
999+
const script = createMockScript({ metadata: { match: ["*://current.example/*"] } });
10471000
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
10481001

1049-
await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
1002+
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });
10501003

1051-
expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*", "*://current.example/*"] });
1004+
expect(savedSelfMetadata()).toEqual({ match: [] });
10521005
});
10531006

1054-
it("已有空匹配覆盖时排除站点应保留空覆盖", async () => {
1055-
const script = createMockScript({ selfMetadata: { match: [] } });
1007+
it("通配匹配移不掉当前站点时应回退为写入排除", async () => {
1008+
const script = createMockScript({ metadata: { match: ["*://*/*"] } });
10561009
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
10571010

1058-
await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
1011+
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });
10591012

1060-
expect(savedSelfMetadata()).toEqual({
1061-
match: [],
1062-
exclude: ["*://ads.script.com/*", "*://current.example/*"],
1013+
// 通配匹配删不掉单一站点,只有排除能真正关掉;同时不该创建匹配覆盖
1014+
expect(savedSelfMetadata()).toEqual({ exclude: ["*://current.example/*"] });
1015+
});
1016+
1017+
it("不应移除通配子域匹配,改以排除关掉当前子域", async () => {
1018+
const script = createMockScript({ metadata: { match: ["*://*.example.com/*"] } });
1019+
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
1020+
1021+
// 移除 *://*.example.com/* 会连兄弟子域一起关掉,超出「不在 www.example.com 执行」的范围
1022+
await scriptService.excludeFromMatch({
1023+
uuid: script.uuid,
1024+
host: "www.example.com",
1025+
url: "https://www.example.com/page",
10631026
});
1027+
1028+
expect(savedSelfMetadata()).toEqual({ exclude: ["*://www.example.com/*"] });
10641029
});
10651030

1066-
it("新增排除覆盖时应保留作者已有的排除规则", async () => {
1031+
it("@include 仍命中当前站点时应在移出匹配后补写排除", async () => {
10671032
const script = createMockScript({
1068-
metadata: { exclude: ["*://author-blocked.example/*"] },
1069-
selfMetadata: { match: ["*://current.example/*"] },
1033+
metadata: { match: ["*://current.example/*"], include: ["*://current.example/*"] },
10701034
});
10711035
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
10721036

1073-
await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
1037+
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });
10741038

10751039
expect(savedSelfMetadata()).toEqual({
10761040
match: [],
1077-
exclude: ["*://author-blocked.example/*", "*://current.example/*"],
1041+
exclude: ["*://current.example/*"],
10781042
});
10791043
});
10801044

1081-
it("已有用户排除覆盖时排除站点应同时保留作者与用户排除规则", async () => {
1045+
it("仅在当前站点执行后再关掉当前站点应清空匹配并撤销来源标记", async () => {
10821046
const script = createMockScript({
1083-
metadata: { exclude: ["*://author-blocked.example/*"] },
1084-
selfMetadata: { match: ["*://current.example/*"], exclude: ["*://user-blocked.example/*"] },
1047+
selfMetadata: {
1048+
match: ["*://current.example/*"],
1049+
include: [],
1050+
[SELF_METADATA_ONLY_RUN_ON_URL]: ["*://current.example/*"],
1051+
},
1052+
});
1053+
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
1054+
1055+
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });
1056+
1057+
expect(savedSelfMetadata()).toEqual({ match: [], include: [] });
1058+
});
1059+
1060+
it("写入排除时应同时保留作者与用户已有的排除规则", async () => {
1061+
const script = createMockScript({
1062+
metadata: { match: ["*://*/*"], exclude: ["*://author-blocked.example/*"] },
1063+
selfMetadata: { exclude: ["*://user-blocked.example/*"] },
10851064
});
10861065
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
10871066

1088-
await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
1067+
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });
10891068

1069+
// 用户覆盖整体替换作者规则,因此写排除时须并入作者 @exclude,避免丢作者规则
10901070
expect(savedSelfMetadata()).toEqual({
1091-
match: [],
10921071
exclude: ["*://author-blocked.example/*", "*://user-blocked.example/*", "*://current.example/*"],
10931072
});
10941073
});
1074+
1075+
it("当前站点本就不在匹配范围内时不应写入任何覆盖", async () => {
1076+
const script = createMockScript({ metadata: { match: ["*://other.example/*"] } });
1077+
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);
1078+
1079+
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });
1080+
1081+
expect(mockScriptDAO.update).not.toHaveBeenCalled();
1082+
});
10951083
});
10961084

10971085
describe("resetMatch / resetExclude - 编辑器匹配列表", () => {

0 commit comments

Comments
 (0)