diff --git a/docs/references/terminology-zh-TW.md b/docs/references/terminology-zh-TW.md index 26068cbb3..1bae09058 100644 --- a/docs/references/terminology-zh-TW.md +++ b/docs/references/terminology-zh-TW.md @@ -52,7 +52,7 @@ | 目前詞彙 | 建議判斷 | 目前受影響 key | | --- | --- | --- | | `目錄` | 指 filesystem directory 的介面動作用 `資料夾`;文章或文件的內容目錄仍用 `目錄`。 | `open_backup_dir`, `open_directory`, `script_operation_description`, `get_backup_dir_url_failed` | -| `恢復` | restore settings/default values 用 `還原`;resume operation 或 recover 依語意使用 `恢復` / `復原`。 | `exclude_on`, `restore_default_values` | +| `恢復` | restore settings/default values 用 `還原`;resume operation 或 recover 依語意使用 `恢復` / `復原`。 | `restore_default_values` | | `拉取` | Git pull 可用 `拉取`;從雲端取得備份或資料的 UI 動作用 `下載` / `擷取` / `同步取得`。 | `pulling_data_from_cloud`, `pull_failed` | | `保存` / `儲存` | UI 的 save 動作用 `儲存`;保存期限、保存證據或一般敘述仍可使用 `保存`。 | `tools_backup_content` | | `設備` / `裝置` | 使用者持有、同步或連線的 device 介面用 `裝置`;equipment 或設備管理等語境仍可使用 `設備`。 | `setting_sync_content` | diff --git a/e2e/popup-matching-regressions.spec.ts b/e2e/popup-matching-regressions.spec.ts index a47982a89..d65ccc016 100644 --- a/e2e/popup-matching-regressions.spec.ts +++ b/e2e/popup-matching-regressions.spec.ts @@ -39,9 +39,10 @@ async function verifyExcludeRoundTrip( const script = initial.data.scriptList.find((item: { name: string }) => item.name === scriptName); if (!script) throw new Error(`script missing initially: ${JSON.stringify(initial.data.scriptList)}`); + const host = new URL(url).host; const exclude = await chrome.runtime.sendMessage({ - action: "serviceWorker/script/excludeUrl", - data: { uuid: script.uuid, excludePattern: "*://sitea.test/*", remove: false }, + action: "serviceWorker/script/excludeFromMatch", + data: { uuid: script.uuid, host, url }, }); if (exclude.code) throw new Error(`exclude failed: ${JSON.stringify(exclude)}`); @@ -50,8 +51,8 @@ async function verifyExcludeRoundTrip( if (!excluded) throw new Error("script disappeared from Popup after exclusion"); const unexclude = await chrome.runtime.sendMessage({ - action: "serviceWorker/script/excludeUrl", - data: { uuid: script.uuid, excludePattern: "*://sitea.test/*", remove: true }, + action: "serviceWorker/script/allowUrl", + data: { uuid: script.uuid, matchPattern: `*://${host}/*`, excludePattern: `*://${host}/*` }, }); if (unexclude.code) throw new Error(`unexclude failed: ${JSON.stringify(unexclude)}`); diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 61984c9c2..aad7eb5c7 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -101,10 +101,6 @@ export class ScriptClient extends Client { return this.doThrow("getScriptRunResourceByUUID", uuid); } - excludeUrl(uuid: string, excludePattern: string, remove: boolean) { - return this.do("excludeUrl", { uuid, excludePattern, remove }); - } - onlyRunOnUrl(uuid: string, matchPattern: string) { return this.do("onlyRunOnUrl", { uuid, matchPattern }); } @@ -113,8 +109,8 @@ export class ScriptClient extends Client { return this.do("allowUrl", { uuid, matchPattern, excludePattern }); } - excludeFromMatch(uuid: string, matchPattern: string) { - return this.do("excludeFromMatch", { uuid, matchPattern }); + excludeFromMatch(uuid: string, host: string, url: string) { + return this.do("excludeFromMatch", { uuid, host, url }); } // 重置匹配项 diff --git a/src/app/service/service_worker/runtime.test.ts b/src/app/service/service_worker/runtime.test.ts index 9170755ed..27103b47c 100644 --- a/src/app/service/service_worker/runtime.test.ts +++ b/src/app/service/service_worker/runtime.test.ts @@ -288,7 +288,7 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹 }); }); - it.concurrent("match 覆盖清空后不再保留此前的匹配规则", async () => { + it.concurrent("match 覆盖清空后此前的匹配规则不再生效,但仍以未生效列出", async () => { const { runtime } = createRuntimeTestContext(); const script = createMockScript({ metadata: { match: ["https://www.example.com/*"] }, @@ -302,9 +302,24 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹 ...script, selfMetadata: { match: [] }, }); - expect(await runtime.applyScriptMatchInfo(emptyMatchOverride)).toBeUndefined(); + await runtime.applyScriptMatchInfo(emptyMatchOverride); - expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/", true).has(script.uuid)).toBe(false); + expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/").has(script.uuid)).toBe(false); + // 原始规则仍在匹配器内,Popup 才能把它列为未生效并给出「允许在此执行」的恢复入口 + expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/", true).get(script.uuid)?.effective).toBe( + false + ); + }); + + it.concurrent("match 覆盖清空的脚本不应被注册(空规则会被 UserScripts API 退回成全站匹配)", async () => { + const { runtime } = createRuntimeTestContext(); + (runtime as any).resource = { getScriptResourceValue: vi.fn().mockResolvedValue({}) }; + const script = createMockScript({ + metadata: { match: ["https://www.example.com/*"] }, + selfMetadata: { match: [] }, + }); + + expect(await runtime.buildAndSaveCompiledResourceFromScript(script)).toBeUndefined(); }); it.concurrent("空匹配覆盖时应删除持久化 CompiledResource 并注销旧注册", async () => { diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index daad01b51..ca06143f1 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -52,7 +52,7 @@ import Logger from "@App/app/logger/logger"; import type { GMInfoEnv, ValueUpdateDataEncoded } from "../content/types"; import { initLocalesPromise, localePath } from "@App/locales/locales"; import { DocumentationSite } from "@App/app/const"; -import { extractUrlPatterns, RuleType, type URLRuleEntry } from "@App/pkg/utils/url_matcher"; +import { extractUrlPatterns, RuleType, RuleTypeBit, type URLRuleEntry } from "@App/pkg/utils/url_matcher"; import { parseUserConfig } from "@App/pkg/utils/yaml"; import type { CompiledResource, Resource, ResourceType } from "@App/app/repo/resource"; import { CompiledResourceDAO, CompiledResourceNamespace } from "@App/app/repo/resource"; @@ -456,9 +456,9 @@ export class RuntimeService { // 安装,启用,或earlyStartScript的value更新 const ret = await this.buildAndSaveCompiledResourceFromScript(script, true); if (!ret) { - // 空匹配覆盖(match 与 include 均为空)时脚本不再匹配任何站点。 - // 内存 matcher 已由 applyScriptMatchInfo 清空,这里再清掉持久化的 CompiledResource - // 并注销浏览器旧注册,否则 SW 重启后 waitInit 会信任旧资源、让旧范围复活。 + // 空匹配覆盖(match 与 include 均为空)时脚本不再匹配任何站点。内存 matcher 里只剩 + // 供 Popup 恢复用的原始规则,这里再清掉持久化的 CompiledResource 并注销浏览器旧注册, + // 否则 SW 重启后 waitInit 会信任旧资源、让旧范围复活。 await this.compiledResourceDAO.delete(script.uuid); await this.unregistryPageScripts([script.uuid]); return; @@ -855,6 +855,9 @@ export class RuntimeService { const resourceUrls = (script.metadata["require"] || []).map((res) => resources[res]?.url).filter((res) => res); const scriptMatchInfo = await this.applyScriptMatchInfo(scriptRes); if (!scriptMatchInfo) return undefined; + // 生效规则一条 inclusion 都不剩(用户把当前站点从匹配中移除后可能如此)时不能注册: + // getApiMatchesAndGlobs 对没有 match pattern 的规则集会退回 *://*/*,注册出去等于全站运行。 + if (!scriptMatchInfo.scriptUrlPatterns.some((rule) => rule.ruleType & RuleTypeBit.INCLUSION)) return undefined; const res = getUserScriptRegister(scriptMatchInfo); const registerScript = res.registerScript; diff --git a/src/app/service/service_worker/script.test.ts b/src/app/service/service_worker/script.test.ts index 84b7ba909..cb9574649 100644 --- a/src/app/service/service_worker/script.test.ts +++ b/src/app/service/service_worker/script.test.ts @@ -723,11 +723,11 @@ describe("ScriptClient 站点范围消息", () => { const sendMessage = vi.fn().mockResolvedValue({ data: true }); const client = new ScriptClient({ sendMessage } as unknown as MessageSend); - await client.excludeFromMatch("script-uuid", "*://current.example/*"); + await client.excludeFromMatch("script-uuid", "current.example", "https://current.example/page"); expect(sendMessage).toHaveBeenCalledWith({ action: "serviceWorker/script/excludeFromMatch", - data: { uuid: "script-uuid", matchPattern: "*://current.example/*" }, + data: { uuid: "script-uuid", host: "current.example", url: "https://current.example/page" }, }); }); }); @@ -786,51 +786,10 @@ describe("ScriptService selfMetadata 用户覆盖", () => { ); }); - describe("excludeUrl - popup 排除/取消排除", () => { - it("取消最后一条排除后应保存空覆盖,而不是回落脚本自带的 exclude", async () => { - const script = createMockScript(); - vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - - await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://ads.script.com/*", remove: true }); - - expect(savedSelfMetadata()).toEqual({ exclude: [] }); - }); - - it("取消排除后仍有其他规则时应保存剩余规则", async () => { - const script = createMockScript({ - selfMetadata: { exclude: ["*://ads.script.com/*", "*://user.com/*"] }, - }); - vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - - await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://user.com/*", remove: true }); - - expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*"] }); - }); - - it("排除新网站时应追加到覆盖中", async () => { - const script = createMockScript(); - vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - - await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://user.com/*", remove: false }); - - expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*", "*://user.com/*"] }); - }); - - it("已有用户排除覆盖时新增排除应同时保留作者与用户规则", async () => { - const script = createMockScript({ - selfMetadata: { exclude: ["*://user-blocked.example/*"] }, - }); - vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - - await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://new.example/*", remove: false }); - - expect(savedSelfMetadata()).toEqual({ - exclude: ["*://ads.script.com/*", "*://user-blocked.example/*", "*://new.example/*"], - }); - }); - }); - describe("popup 站点范围快捷操作", () => { + const host = "current.example"; + const url = "https://current.example/page"; + it("初始化时应注册排除已匹配站点操作", async () => { const alarmsDescriptor = Object.getOwnPropertyDescriptor(chrome, "alarms"); Object.defineProperty(chrome, "alarms", { @@ -894,17 +853,14 @@ describe("ScriptService selfMetadata 用户覆盖", () => { const onlyRun = scriptService.onlyRunOnUrl({ uuid: stored.uuid, matchPattern: "*://current.example/*" }); await firstUpdateStarted; - const exclude = scriptService.excludeFromMatch({ uuid: stored.uuid, matchPattern: "*://current.example/*" }); + const exclude = scriptService.excludeFromMatch({ uuid: stored.uuid, host, url }); await Promise.resolve(); await Promise.resolve(); releaseFirstUpdate(); await Promise.all([onlyRun, exclude]); - expect(stored.selfMetadata).toEqual({ - match: [], - include: [], - exclude: ["*://ads.script.com/*", "*://current.example/*"], - }); + // 串行执行才能让 excludeFromMatch 看到 onlyRunOnUrl 写入的匹配覆盖并把它移出 + expect(stored.selfMetadata).toEqual({ match: [], include: [] }); }); it("并发 onlyRunOnUrl 与 resetMatch 应串行执行并保留两次读改写", async () => { @@ -1012,86 +968,118 @@ describe("ScriptService selfMetadata 用户覆盖", () => { }); }); - it("排除已包含站点时应移出用户匹配并加入用户排除", async () => { + it("匹配中有当前站点的专属规则时应只移出匹配,不写入排除", async () => { const script = createMockScript({ - selfMetadata: { - match: ["*://allowed.example/*", "*://current.example/*"], - exclude: ["*://blocked.example/*"], - }, + metadata: { match: ["*://current.example/*", "*://other.example/*"] }, + selfMetadata: { exclude: ["*://blocked.example/*"] }, }); vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" }); + await scriptService.excludeFromMatch({ uuid: script.uuid, host, url }); - // 作者 @exclude(ads.script.com)并入用户覆盖,避免用户覆盖整体替换作者规则 + // 移出匹配后脚本已不在本站生效,无需再写排除,用户的排除列表保持原样 expect(savedSelfMetadata()).toEqual({ - match: ["*://allowed.example/*"], - exclude: ["*://ads.script.com/*", "*://blocked.example/*", "*://current.example/*"], + match: ["*://other.example/*"], + exclude: ["*://blocked.example/*"], }); }); - it("排除最后一个用户匹配时应保留显式空匹配覆盖", async () => { - const script = createMockScript({ selfMetadata: { match: ["*://current.example/*"] } }); + it("同一站点的多条路径匹配应一并移出", async () => { + const script = createMockScript({ + metadata: { match: ["https://current.example/a*", "http://current.example/b*", "*://other.example/*"] }, + }); vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" }); + await scriptService.excludeFromMatch({ uuid: script.uuid, host, url }); - expect(savedSelfMetadata()).toEqual({ - match: [], - exclude: ["*://ads.script.com/*", "*://current.example/*"], - }); + expect(savedSelfMetadata()).toEqual({ match: ["*://other.example/*"] }); }); - it("没有用户匹配覆盖时排除站点不应创建匹配覆盖", async () => { - const script = createMockScript(); + it("移出的是最后一条匹配时应保留显式空匹配覆盖", async () => { + const script = createMockScript({ metadata: { match: ["*://current.example/*"] } }); vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" }); + await scriptService.excludeFromMatch({ uuid: script.uuid, host, url }); - expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*", "*://current.example/*"] }); + expect(savedSelfMetadata()).toEqual({ match: [] }); }); - it("已有空匹配覆盖时排除站点应保留空覆盖", async () => { - const script = createMockScript({ selfMetadata: { match: [] } }); + it("通配匹配移不掉当前站点时应回退为写入排除", async () => { + const script = createMockScript({ metadata: { match: ["*://*/*"] } }); vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" }); + await scriptService.excludeFromMatch({ uuid: script.uuid, host, url }); - expect(savedSelfMetadata()).toEqual({ - match: [], - exclude: ["*://ads.script.com/*", "*://current.example/*"], + // 通配匹配删不掉单一站点,只有排除能真正关掉;同时不该创建匹配覆盖 + expect(savedSelfMetadata()).toEqual({ exclude: ["*://current.example/*"] }); + }); + + it("不应移除通配子域匹配,改以排除关掉当前子域", async () => { + const script = createMockScript({ metadata: { match: ["*://*.example.com/*"] } }); + vi.mocked(mockScriptDAO.get).mockResolvedValue(script); + + // 移除 *://*.example.com/* 会连兄弟子域一起关掉,超出「不在 www.example.com 执行」的范围 + await scriptService.excludeFromMatch({ + uuid: script.uuid, + host: "www.example.com", + url: "https://www.example.com/page", }); + + expect(savedSelfMetadata()).toEqual({ exclude: ["*://www.example.com/*"] }); }); - it("新增排除覆盖时应保留作者已有的排除规则", async () => { + it("@include 仍命中当前站点时应在移出匹配后补写排除", async () => { const script = createMockScript({ - metadata: { exclude: ["*://author-blocked.example/*"] }, - selfMetadata: { match: ["*://current.example/*"] }, + metadata: { match: ["*://current.example/*"], include: ["*://current.example/*"] }, }); vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" }); + await scriptService.excludeFromMatch({ uuid: script.uuid, host, url }); expect(savedSelfMetadata()).toEqual({ match: [], - exclude: ["*://author-blocked.example/*", "*://current.example/*"], + exclude: ["*://current.example/*"], }); }); - it("已有用户排除覆盖时排除站点应同时保留作者与用户排除规则", async () => { + it("仅在当前站点执行后再关掉当前站点应清空匹配并撤销来源标记", async () => { const script = createMockScript({ - metadata: { exclude: ["*://author-blocked.example/*"] }, - selfMetadata: { match: ["*://current.example/*"], exclude: ["*://user-blocked.example/*"] }, + selfMetadata: { + match: ["*://current.example/*"], + include: [], + [SELF_METADATA_ONLY_RUN_ON_URL]: ["*://current.example/*"], + }, + }); + vi.mocked(mockScriptDAO.get).mockResolvedValue(script); + + await scriptService.excludeFromMatch({ uuid: script.uuid, host, url }); + + expect(savedSelfMetadata()).toEqual({ match: [], include: [] }); + }); + + it("写入排除时应同时保留作者与用户已有的排除规则", async () => { + const script = createMockScript({ + metadata: { match: ["*://*/*"], exclude: ["*://author-blocked.example/*"] }, + selfMetadata: { exclude: ["*://user-blocked.example/*"] }, }); vi.mocked(mockScriptDAO.get).mockResolvedValue(script); - await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" }); + await scriptService.excludeFromMatch({ uuid: script.uuid, host, url }); + // 用户覆盖整体替换作者规则,因此写排除时须并入作者 @exclude,避免丢作者规则 expect(savedSelfMetadata()).toEqual({ - match: [], exclude: ["*://author-blocked.example/*", "*://user-blocked.example/*", "*://current.example/*"], }); }); + + it("当前站点本就不在匹配范围内时不应写入任何覆盖", async () => { + const script = createMockScript({ metadata: { match: ["*://other.example/*"] } }); + vi.mocked(mockScriptDAO.get).mockResolvedValue(script); + + await scriptService.excludeFromMatch({ uuid: script.uuid, host, url }); + + expect(mockScriptDAO.update).not.toHaveBeenCalled(); + }); }); describe("resetMatch / resetExclude - 编辑器匹配列表", () => { diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 5044dcda4..23e4cfe67 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -37,6 +37,8 @@ import type { } from "../queue"; import { CLOUD_SYNC_QUEUE_KEY } from "../queue"; import { buildScriptRunResourceBasic, selfMetadataUpdate } from "./utils"; +import { extractUrlPatterns, getMatchPatternHost } from "@App/pkg/utils/url_matcher"; +import { isUrlIncluded } from "@App/pkg/utils/match"; import { BatchUpdateListActionCode, type TBatchUpdateListAction, @@ -901,38 +903,6 @@ export class ScriptService { }); } - // ScriptMenuList 的 excludeUrl - 排除或回复 - async excludeUrl({ uuid, excludePattern, remove }: { uuid: string; excludePattern: string; remove: boolean }) { - return stackAsyncTask("script-site-scope", async () => { - let script = await this.scriptDAO.get(uuid); - if (!script) { - throw new Error("script not found"); - } - // 建立Set去掉重复(如有);用户覆盖整体替换作者规则,因此须把作者 @exclude 一并并入, - // 否则用户已有排除覆盖时会丢作者规则 - const excludeSet = new Set([...(script.metadata?.exclude || []), ...(script.selfMetadata?.exclude || [])]); - if (remove) { - const deleted = excludeSet.delete(excludePattern); - if (!deleted) { - return; // scriptDAO 不用更新 - } - } else { - excludeSet.add(excludePattern); - } - // 更新 script.selfMetadata.exclude - script = selfMetadataUpdate(script, "exclude", excludeSet); - try { - await this.scriptDAO.update(uuid, script); - // 广播一下 - this.publishInstallScript(script, { update: true }); - return true; - } catch (e) { - this.logger.error("exclude url error", Logger.E(e)); - throw e; - } - }); - } - async onlyRunOnUrl({ uuid, matchPattern }: { uuid: string; matchPattern: string }) { return stackAsyncTask("script-site-scope", async () => { let script = await this.scriptDAO.get(uuid); @@ -974,20 +944,39 @@ export class ScriptService { }); } - async excludeFromMatch({ uuid, matchPattern }: { uuid: string; matchPattern: string }) { + /** + * 关掉脚本在 host 上的执行:优先把只服务于该站点的 @match 移出匹配列表,只有在移完仍会命中 + * 当前网址时(通配 @match、@include、正则等移不动的规则)才追加 @exclude。排除会冻结作者的 + * @exclude 并让匹配与排除自相矛盾,因此只在删不掉的情况下使用。 + */ + async excludeFromMatch({ uuid, host, url }: { uuid: string; host: string; url: string }) { return stackAsyncTask("script-site-scope", async () => { let script = await this.scriptDAO.get(uuid); if (!script) throw new Error("script not found"); - if (script.selfMetadata?.match !== undefined) { - const matchSet = new Set(script.selfMetadata.match); - matchSet.delete(matchPattern); - script = selfMetadataUpdate(script, "match", matchSet); + // 用户覆盖存在时整体替换作者规则,生效值须按项取 + const match = script.selfMetadata?.match ?? script.metadata?.match ?? []; + const include = script.selfMetadata?.include ?? script.metadata?.include ?? []; + const exclude = script.selfMetadata?.exclude ?? script.metadata?.exclude ?? []; + // 通配网域(`*://*/*`、`*://*.example.com/*`)不只服务于当前站点,移除会连带关掉其他站点 + const keptMatch = match.filter((pattern) => getMatchPatternHost(pattern) !== host); + const removedFromMatch = keptMatch.length !== match.length; + if (removedFromMatch) { + script = selfMetadataUpdate(script, "match", new Set(keptMatch)); script = selfMetadataUpdate(script, SELF_METADATA_ONLY_RUN_ON_URL, undefined); } - // 用户覆盖整体替换作者规则,因此把作者 @exclude 一并并入用户覆盖,避免丢作者规则 - const excludeSet = new Set([...(script.metadata?.exclude || []), ...(script.selfMetadata?.exclude || [])]); - excludeSet.add(matchPattern); - script = selfMetadataUpdate(script, "exclude", excludeSet); + const rules = extractUrlPatterns([ + ...keptMatch.map((e) => `@match ${e}`), + ...include.map((e) => `@include ${e}`), + ...exclude.map((e) => `@exclude ${e}`), + ]); + if (isUrlIncluded(url, rules)) { + // 用户覆盖整体替换作者规则,因此把作者 @exclude 一并并入用户覆盖,避免丢作者规则 + const excludeSet = new Set([...(script.metadata?.exclude || []), ...(script.selfMetadata?.exclude || [])]); + excludeSet.add(`*://${host}/*`); + script = selfMetadataUpdate(script, "exclude", excludeSet); + } else if (!removedFromMatch) { + return; // 当前站点本就不在匹配范围内,没有要改的覆盖 + } await this.scriptDAO.update(uuid, script); this.publishInstallScript(script, { update: true }); return true; @@ -1797,7 +1786,6 @@ export class ScriptService { this.group.on("updateRunStatus", this.updateRunStatus.bind(this)); this.group.on("getFilterResult", this.getFilterResult.bind(this)); this.group.on("getScriptRunResourceByUUID", this.getScriptRunResourceByUUID.bind(this)); - this.group.on("excludeUrl", this.excludeUrl.bind(this)); this.group.on("onlyRunOnUrl", this.onlyRunOnUrl.bind(this)); this.group.on("allowUrl", this.allowUrl.bind(this)); this.group.on("excludeFromMatch", this.excludeFromMatch.bind(this)); diff --git a/src/app/service/service_worker/utils.test.ts b/src/app/service/service_worker/utils.test.ts index cd39524fe..94c31b627 100644 --- a/src/app/service/service_worker/utils.test.ts +++ b/src/app/service/service_worker/utils.test.ts @@ -7,12 +7,13 @@ import { getUserScriptRegister, compileInjectionCode, shouldAutoOpenChangelog, + scriptURLPatternResults, } from "./utils"; import type { SCMetadata, Script, ScriptRunResource } from "@App/app/repo/scripts"; import { SELF_METADATA_ONLY_RUN_ON_URL } from "@App/app/repo/metadata"; import { SCRIPT_TYPE_NORMAL, SCRIPT_STATUS_ENABLE, SCRIPT_RUN_STATUS_COMPLETE } from "@App/app/repo/scripts"; import type { ScriptMatchInfo } from "./types"; -import { extractUrlPatterns } from "@App/pkg/utils/url_matcher"; +import { extractUrlPatterns, RuleTypeBit } from "@App/pkg/utils/url_matcher"; describe.concurrent("parseUrlSRI", () => { it.concurrent("should parse URL SRI", () => { @@ -358,3 +359,24 @@ describe.concurrent("compileInjectionCode", () => { expect(result).toContain("window['#-test-uuid']"); }); }); + +describe.concurrent("scriptURLPatternResults", () => { + const createScriptRes = (metadata: SCMetadata, selfMetadata?: SCMetadata) => ({ + metadata: selfMetadata ? getCombinedMeta(metadata, selfMetadata) : metadata, + originalMetadata: metadata, + selfMetadata, + }); + + it.concurrent("无任何匹配规则的脚本应返回 null", () => { + expect(scriptURLPatternResults(createScriptRes({ name: ["no match"] }))).toBeNull(); + }); + + it.concurrent("用户把匹配清空后应保留原始规则,使脚本仍能在 Popup 中被恢复", () => { + const result = scriptURLPatternResults(createScriptRes({ match: ["*://example.com/*"] }, { match: [] })); + + // 生效规则不含任何 inclusion => 不匹配任何站点; + // 原始规则仍在 => Popup 仍以「未生效」列出该脚本,可点「允许在此执行」恢复 + expect(result!.scriptUrlPatterns.some((rule) => rule.ruleType & RuleTypeBit.INCLUSION)).toBe(false); + expect(result!.originalUrlPatterns.map((rule) => rule.patternString)).toEqual(["*://example.com/*"]); + }); +}); diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts index 4dbc3ae28..753befa28 100644 --- a/src/app/service/service_worker/utils.ts +++ b/src/app/service/service_worker/utils.ts @@ -273,7 +273,13 @@ export function scriptURLPatternResults(scriptRes: { const metaMatch = metadata.match; const metaInclude = metadata.include; const metaExclude = metadata.exclude; - if ((metaMatch?.length ?? 0) + (metaInclude?.length ?? 0) === 0) { + // 生效规则为空时仍要解析原始规则:用户把当前站点从匹配中移除后可能一条不剩, + // 此时脚本不匹配任何站点,但 Popup 要靠原始规则继续列出它(未生效),用户才有恢复入口。 + // 只有连原始规则都没有的脚本才真的无从匹配。 + if ( + (metaMatch?.length ?? 0) + (metaInclude?.length ?? 0) === 0 && + (originalMetadata.match?.length ?? 0) + (originalMetadata.include?.length ?? 0) === 0 + ) { return null; } diff --git a/src/locales/de-DE/common.json b/src/locales/de-DE/common.json index 730106c29..991a01f86 100644 --- a/src/locales/de-DE/common.json +++ b/src/locales/de-DE/common.json @@ -58,7 +58,6 @@ "stop": "Stoppen", "edit": "Bearbeiten", "copy": "Kopieren", - "exclude_on": "Wiederherstellen auf $0 zur Ausführung", "exclude_off": "Ausschließen auf $0 zur Ausführung", "only_on_site": "Nur auf $0 ausführen", "confirm_only_run_on_site": "Die bestehenden Website-Abgleichsregeln des Skripts werden gelöscht. Nur auf dieser Website ausführen?", diff --git a/src/locales/de-DE/settings.json b/src/locales/de-DE/settings.json index 9b9733498..87cd2095a 100644 --- a/src/locales/de-DE/settings.json +++ b/src/locales/de-DE/settings.json @@ -72,8 +72,6 @@ "popup_layout": "Popup-Layout", "compact_popup_layout": "Kompaktes Popup-Layout", "compact_popup_layout_desc": "Verringert die Abstände zwischen Bereichen und Skriptzeilen im Popup", - "popup_site_scope_actions": "Aktionen für den Website-Bereich", - "popup_site_scope_actions_desc": "Zeigt im Popup Schnellaktionen an, um ein Skript auf der aktuellen Website einzuschränken oder zuzulassen", "script_list_expand_count": "Angezeigte Skripte in der Liste", "script_list_expand_count_desc": "Anzahl der Skripte, die je Popup-Bereich ausgeklappt werden; der Rest wird eingeklappt. Bei 0 werden alle angezeigt", "script_update_check_frequency": "Häufigkeit der Skript-Aktualisierungsprüfung", diff --git a/src/locales/en-US/common.json b/src/locales/en-US/common.json index 3a27ae70f..c10ef8504 100644 --- a/src/locales/en-US/common.json +++ b/src/locales/en-US/common.json @@ -58,7 +58,6 @@ "stop": "Stop", "edit": "Edit", "copy": "Copy", - "exclude_on": "Reinstate $0's execution", "exclude_off": "Exclude $0's execution", "only_on_site": "Run only on $0", "confirm_only_run_on_site": "This will clear the script's existing site match rules. Run only on this site?", diff --git a/src/locales/en-US/settings.json b/src/locales/en-US/settings.json index ab3a97fc0..13bec0644 100644 --- a/src/locales/en-US/settings.json +++ b/src/locales/en-US/settings.json @@ -72,8 +72,6 @@ "popup_layout": "Popup Layout", "compact_popup_layout": "Compact Popup Layout", "compact_popup_layout_desc": "Reduce spacing between popup sections and script rows", - "popup_site_scope_actions": "Site scope actions", - "popup_site_scope_actions_desc": "Show quick actions in the popup to restrict or allow a script on the current site", "script_list_expand_count": "Script List Expand Count", "script_list_expand_count_desc": "Scripts shown in each popup section; the rest are collapsed. 0 shows all of them", "script_update_check_frequency": "Script Update Check Frequency", diff --git a/src/locales/ja-JP/common.json b/src/locales/ja-JP/common.json index 782c6860d..fc87703d5 100644 --- a/src/locales/ja-JP/common.json +++ b/src/locales/ja-JP/common.json @@ -58,7 +58,6 @@ "stop": "中止", "edit": "編集", "copy": "コピー", - "exclude_on": "$0の実行を復元", "exclude_off": "$0の実行を除外", "only_on_site": "$0 でのみ実行", "confirm_only_run_on_site": "スクリプトの既存のサイト対象ルールが消去されます。このサイトでのみ実行しますか?", diff --git a/src/locales/ja-JP/settings.json b/src/locales/ja-JP/settings.json index 763c66f74..5f2e5b3e5 100644 --- a/src/locales/ja-JP/settings.json +++ b/src/locales/ja-JP/settings.json @@ -72,8 +72,6 @@ "popup_layout": "ポップアップレイアウト", "compact_popup_layout": "コンパクトなポップアップ", "compact_popup_layout_desc": "ポップアップのセクションとスクリプト行の間隔を狭くします", - "popup_site_scope_actions": "サイト範囲の操作", - "popup_site_scope_actions_desc": "現在のサイトでスクリプトの実行を制限または許可するクイック操作をポップアップに表示します", "script_list_expand_count": "スクリプト一覧の展開数", "script_list_expand_count_desc": "ポップアップの各セクションで展開表示するスクリプト数です。超えた分は折りたたまれ、0 の場合はすべて表示します", "script_update_check_frequency": "スクリプト更新の確認頻度", diff --git a/src/locales/ko-KR/common.json b/src/locales/ko-KR/common.json index 6bea732aa..eccf7319e 100644 --- a/src/locales/ko-KR/common.json +++ b/src/locales/ko-KR/common.json @@ -58,7 +58,6 @@ "stop": "중지", "edit": "편집", "copy": "복사", - "exclude_on": "$0에서 다시 실행", "exclude_off": "$0에서 실행 제외", "only_on_site": "$0에서만 실행", "confirm_only_run_on_site": "스크립트의 기존 사이트 일치 규칙이 모두 삭제됩니다. 이 사이트에서만 실행하시겠습니까?", diff --git a/src/locales/ko-KR/settings.json b/src/locales/ko-KR/settings.json index 237280752..f1bd00e49 100644 --- a/src/locales/ko-KR/settings.json +++ b/src/locales/ko-KR/settings.json @@ -72,8 +72,6 @@ "popup_layout": "팝업 레이아웃", "compact_popup_layout": "간격을 줄인 팝업 레이아웃", "compact_popup_layout_desc": "팝업 섹션과 스크립트 행 사이의 간격을 줄입니다", - "popup_site_scope_actions": "사이트 범위 작업", - "popup_site_scope_actions_desc": "현재 사이트에서 스크립트를 제한하거나 허용하는 빠른 작업을 팝업에 표시합니다", "script_list_expand_count": "스크립트 목록 펼침 개수", "script_list_expand_count_desc": "팝업의 각 섹션에서 펼쳐 표시할 스크립트 개수이며, 초과분은 접힙니다. 0이면 모두 표시합니다", "script_update_check_frequency": "스크립트 업데이트 확인 주기", diff --git a/src/locales/pt-BR/common.json b/src/locales/pt-BR/common.json index 6c7605b1a..06eab562a 100644 --- a/src/locales/pt-BR/common.json +++ b/src/locales/pt-BR/common.json @@ -58,7 +58,6 @@ "stop": "Parar", "edit": "Editar", "copy": "Copiar", - "exclude_on": "Restaurar a execução de $0", "exclude_off": "Impedir a execução em $0", "only_on_site": "Executar somente em $0", "confirm_only_run_on_site": "Isso limpará as regras de correspondência de sites existentes do script. Executar somente neste site?", diff --git a/src/locales/pt-BR/settings.json b/src/locales/pt-BR/settings.json index ba5ed3cb0..827141920 100644 --- a/src/locales/pt-BR/settings.json +++ b/src/locales/pt-BR/settings.json @@ -72,8 +72,6 @@ "popup_layout": "Layout do popup", "compact_popup_layout": "Layout compacto do popup", "compact_popup_layout_desc": "Reduzir o espaçamento entre as seções do popup e as linhas de scripts", - "popup_site_scope_actions": "Ações de escopo do site", - "popup_site_scope_actions_desc": "Mostrar no popup ações rápidas para restringir ou permitir um script no site atual", "script_list_expand_count": "Scripts visíveis na lista", "script_list_expand_count_desc": "Quantidade de scripts expandidos em cada seção do popup; o restante fica recolhido. Com 0, todos são exibidos", "script_update_check_frequency": "Frequência de verificação de atualização de scripts", diff --git a/src/locales/ru-RU/common.json b/src/locales/ru-RU/common.json index 80b612b4c..18380cd8d 100644 --- a/src/locales/ru-RU/common.json +++ b/src/locales/ru-RU/common.json @@ -58,7 +58,6 @@ "stop": "Остановить", "edit": "Редактировать", "copy": "Копировать", - "exclude_on": "Восстановить в $0 выполнении", "exclude_off": "Исключить в $0 выполнении", "only_on_site": "Выполнять только на $0", "confirm_only_run_on_site": "Существующие правила соответствия сайтов для скрипта будут очищены. Выполнять только на этом сайте?", diff --git a/src/locales/ru-RU/settings.json b/src/locales/ru-RU/settings.json index 548fa4c4e..e9bfc5fc3 100644 --- a/src/locales/ru-RU/settings.json +++ b/src/locales/ru-RU/settings.json @@ -72,8 +72,6 @@ "popup_layout": "Макет всплывающего окна", "compact_popup_layout": "Компактное всплывающее окно", "compact_popup_layout_desc": "Уменьшает отступы между разделами и строками скриптов во всплывающем окне", - "popup_site_scope_actions": "Действия для области сайтов", - "popup_site_scope_actions_desc": "Показывать во всплывающем окне быстрые действия для ограничения или разрешения скрипта на текущем сайте", "script_list_expand_count": "Количество скриптов в списке", "script_list_expand_count_desc": "Сколько скриптов разворачивать в каждом разделе всплывающего окна; остальные сворачиваются. При 0 отображаются все", "script_update_check_frequency": "Частота проверки обновления скрипта", diff --git a/src/locales/tr-TR/common.json b/src/locales/tr-TR/common.json index 93b11c7f0..d2f1d1ba0 100644 --- a/src/locales/tr-TR/common.json +++ b/src/locales/tr-TR/common.json @@ -58,7 +58,6 @@ "stop": "Durdur", "edit": "Düzenle", "copy": "Kopyala", - "exclude_on": "$0 yürütmesini yeniden etkinleştir", "exclude_off": "$0 yürütmesini dışla", "only_on_site": "Yalnızca $0 üzerinde çalıştır", "confirm_only_run_on_site": "Betiğin mevcut site eşleştirme kuralları temizlenecek. Yalnızca bu sitede çalıştırılsın mı?", diff --git a/src/locales/tr-TR/settings.json b/src/locales/tr-TR/settings.json index ca21c2cd6..2abd2c47d 100644 --- a/src/locales/tr-TR/settings.json +++ b/src/locales/tr-TR/settings.json @@ -72,8 +72,6 @@ "popup_layout": "Açılır Pencere Düzeni", "compact_popup_layout": "Kompakt Açılır Pencere Düzeni", "compact_popup_layout_desc": "Açılır penceredeki bölümler ve betik satırları arasındaki boşluğu azaltır", - "popup_site_scope_actions": "Site kapsamı işlemleri", - "popup_site_scope_actions_desc": "Geçerli sitede bir betiği kısıtlamak veya çalışmasına izin vermek için açılır pencerede hızlı işlemler gösterir", "script_list_expand_count": "Listede Gösterilen Betik Sayısı", "script_list_expand_count_desc": "Açılır penceredeki her bölümde genişletilerek gösterilecek betik sayısıdır; fazlası daraltılır. 0 olduğunda tümü gösterilir", "script_update_check_frequency": "Betik Güncelleme Denetimi Sıklığı", diff --git a/src/locales/vi-VN/common.json b/src/locales/vi-VN/common.json index 82fecd08a..29986ebf5 100644 --- a/src/locales/vi-VN/common.json +++ b/src/locales/vi-VN/common.json @@ -58,7 +58,6 @@ "stop": "Dừng", "edit": "Sửa", "copy": "Sao chép", - "exclude_on": "Cho phép chạy lại $0", "exclude_off": "Loại trừ chạy $0", "only_on_site": "Chỉ chạy trên $0", "confirm_only_run_on_site": "Thao tác này sẽ xóa các quy tắc khớp trang web hiện có của script. Chỉ chạy trên trang web này?", diff --git a/src/locales/vi-VN/settings.json b/src/locales/vi-VN/settings.json index 932f2b4fe..ed380f717 100644 --- a/src/locales/vi-VN/settings.json +++ b/src/locales/vi-VN/settings.json @@ -72,8 +72,6 @@ "popup_layout": "Bố cục cửa sổ bật lên", "compact_popup_layout": "Bố cục cửa sổ bật lên thu gọn", "compact_popup_layout_desc": "Giảm khoảng cách giữa các mục và hàng script trong cửa sổ bật lên", - "popup_site_scope_actions": "Thao tác phạm vi trang", - "popup_site_scope_actions_desc": "Hiển thị thao tác nhanh trong cửa sổ bật lên để giới hạn hoặc cho phép script trên trang hiện tại", "script_list_expand_count": "Số script hiển thị trong danh sách", "script_list_expand_count_desc": "Số script mở rộng trong mỗi mục của cửa sổ bật lên, phần còn lại sẽ thu gọn; đặt 0 thì hiển thị tất cả", "script_update_check_frequency": "Tần suất kiểm tra cập nhật tập lệnh", diff --git a/src/locales/zh-CN/common.json b/src/locales/zh-CN/common.json index 7764553c0..2fd596a96 100644 --- a/src/locales/zh-CN/common.json +++ b/src/locales/zh-CN/common.json @@ -58,7 +58,6 @@ "stop": "停止", "edit": "编辑", "copy": "复制", - "exclude_on": "恢复在 $0 上执行", "exclude_off": "排除在 $0 上执行", "only_on_site": "仅在 $0 执行", "confirm_only_run_on_site": "将清空脚本原有的网站匹配规则,确认仅在此网站运行?", diff --git a/src/locales/zh-CN/settings.json b/src/locales/zh-CN/settings.json index 518f1361c..625d943c7 100644 --- a/src/locales/zh-CN/settings.json +++ b/src/locales/zh-CN/settings.json @@ -72,8 +72,6 @@ "popup_layout": "弹窗布局", "compact_popup_layout": "紧凑弹窗布局", "compact_popup_layout_desc": "减少弹窗分组与脚本行之间的间距", - "popup_site_scope_actions": "站点范围快捷操作", - "popup_site_scope_actions_desc": "在弹窗中显示仅在当前站点执行或允许当前站点执行的快捷操作", "script_list_expand_count": "脚本列表展开数量", "script_list_expand_count_desc": "弹窗中每个分组展开显示的脚本数量,超出部分折叠;填 0 表示全部显示", "script_update_check_frequency": "脚本更新检查频率", diff --git a/src/locales/zh-TW/common.json b/src/locales/zh-TW/common.json index 4ca26967d..c5bac2d90 100644 --- a/src/locales/zh-TW/common.json +++ b/src/locales/zh-TW/common.json @@ -58,7 +58,6 @@ "stop": "停止", "edit": "編輯", "copy": "複製", - "exclude_on": "恢復 $0 的執行", "exclude_off": "排除 $0 的執行", "only_on_site": "僅在 $0 執行", "confirm_only_run_on_site": "這將清除腳本原有的網站匹配規則,確定只在此網站執行嗎?", diff --git a/src/locales/zh-TW/settings.json b/src/locales/zh-TW/settings.json index bb0dc5e99..19a26817e 100644 --- a/src/locales/zh-TW/settings.json +++ b/src/locales/zh-TW/settings.json @@ -72,8 +72,6 @@ "popup_layout": "彈出視窗版面", "compact_popup_layout": "緊湊彈出視窗版面", "compact_popup_layout_desc": "縮小彈出視窗區段與腳本列的間距", - "popup_site_scope_actions": "站點範圍快速操作", - "popup_site_scope_actions_desc": "在彈出視窗中顯示僅在目前站點執行或允許目前站點執行的快速操作", "script_list_expand_count": "腳本清單展開數量", "script_list_expand_count_desc": "彈出視窗中每個區段展開顯示的腳本數量,超出部分摺疊;填 0 表示全部顯示", "script_update_check_frequency": "腳本更新檢查頻率", diff --git a/src/pages/options/routes/Setting/sections/InterfaceSection.test.tsx b/src/pages/options/routes/Setting/sections/InterfaceSection.test.tsx index 14e1d23eb..729cfb7c9 100644 --- a/src/pages/options/routes/Setting/sections/InterfaceSection.test.tsx +++ b/src/pages/options/routes/Setting/sections/InterfaceSection.test.tsx @@ -55,17 +55,6 @@ describe("界面分区-popup 布局", () => { fireEvent.click(compactSwitch); expect(set).toHaveBeenCalledWith("popup_compact_layout", false); }); - - it("站点范围快捷操作应默认关闭并保存开启结果", async () => { - get.mockResolvedValue(undefined); - render( () => {}} />); - - const siteScopeSwitch = await screen.findByRole("switch", { name: "站点范围快捷操作" }); - expect(siteScopeSwitch).not.toBeChecked(); - - fireEvent.click(siteScopeSwitch); - expect(set).toHaveBeenCalledWith("popup_site_scope_actions", true); - }); }); // 两个展开数量此前共用「展开数量 / 超过此数量时自动折叠」文案,用户误以为它管脚本列表(#1558) diff --git a/src/pages/options/routes/Setting/sections/InterfaceSection.tsx b/src/pages/options/routes/Setting/sections/InterfaceSection.tsx index 487886479..e852a1a24 100644 --- a/src/pages/options/routes/Setting/sections/InterfaceSection.tsx +++ b/src/pages/options/routes/Setting/sections/InterfaceSection.tsx @@ -17,7 +17,6 @@ export function InterfaceSection({ register }: { register: (id: string) => (el: const [scriptListExpandNum, setScriptListExpandNum] = useSystemConfig("script_list_expand_num"); const [favicon, setFavicon] = useSystemConfig("favicon_service"); const [popupCompactLayout, setPopupCompactLayout] = useSystemConfig("popup_compact_layout"); - const [popupSiteScopeActions, setPopupSiteScopeActions] = useSystemConfig("popup_site_scope_actions"); return ( @@ -58,16 +57,6 @@ export function InterfaceSection({ register }: { register: (id: string) => (el: onCheckedChange={setPopupCompactLayout} /> - - - = {}) { showAlert: false, menuExpandNum: 5, popupCompactLayout: false, - popupSiteScopeActions: false, defaultScriptProvider: "scriptcat", currentUrl: "https://example.com", handleToggleScript: vi.fn(), @@ -55,7 +54,6 @@ function makeData(overrides: Record = {}) { handleOpenEditor: vi.fn(), handleOpenScriptSettings: vi.fn(), handleOpenUserConfig: vi.fn(), - handleExcludeUrl: vi.fn(), handleExcludeFromMatch: vi.fn(), handleOnlyRunOnUrl: vi.fn(), handleAllowUrl: vi.fn(), @@ -187,34 +185,10 @@ describe("Popup 当前页状态提示(脚本猫触及不到的页面)", () = }); describe("Popup 脚本快捷设置与站点范围操作", () => { - it.each([false, true])( - "开关关闭时有效脚本始终保留排除并回落黑名单动作(hasMatchOverride=%s)", - (hasMatchOverride) => { - const handleExcludeUrl = vi.fn(); - mockData = makeData({ - scriptList: [makeScriptMenu({ isEffective: true, hasMatchOverride })], - fullScriptCount: 1, - handleExcludeUrl, - }); - render(); - - fireEvent.click(screen.getByRole("button", { name: /Script A/ })); - - expect(screen.getByRole("button", { name: "脚本设置" })).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "仅在 example.com 执行" })).not.toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "允许在 example.com 执行" })).not.toBeInTheDocument(); - const excludeButton = screen.getByRole("button", { name: "排除在 example.com 上执行" }); - expect(excludeButton).toHaveClass("text-type-orange"); - fireEvent.click(excludeButton); - expect(handleExcludeUrl).toHaveBeenCalledWith("u1", true); - } - ); - it("S1 全局生效时显示带确认的仅运行在与互斥排除", async () => { const handleOnlyRunOnUrl = vi.fn(); const handleExcludeFromMatch = vi.fn(); mockData = makeData({ - popupSiteScopeActions: true, scriptList: [makeScriptMenu({ isEffective: true, hasMatchOverride: false })], fullScriptCount: 1, handleOnlyRunOnUrl, @@ -241,7 +215,6 @@ describe("Popup 脚本快捷设置与站点范围操作", () => { it("S3 已包含时只显示排除并调用匹配覆盖操作", () => { const handleExcludeFromMatch = vi.fn(); mockData = makeData({ - popupSiteScopeActions: true, scriptList: [makeScriptMenu({ isEffective: true, hasMatchOverride: true })], fullScriptCount: 1, handleExcludeFromMatch, @@ -261,7 +234,6 @@ describe("Popup 脚本快捷设置与站点范围操作", () => { it.each([false, true])("S2/S4 本站不生效时只显示包含动作(hasMatchOverride=%s)", (hasMatchOverride) => { const handleAllowUrl = vi.fn(); mockData = makeData({ - popupSiteScopeActions: true, scriptList: [makeScriptMenu({ isEffective: false, hasMatchOverride })], fullScriptCount: 1, handleAllowUrl, @@ -280,7 +252,6 @@ describe("Popup 脚本快捷设置与站点范围操作", () => { it("只匹配到 iframe 的脚本隐藏站点范围动作(规则按顶层 host 生成,对它不成立)", () => { mockData = makeData({ - popupSiteScopeActions: true, scriptList: [makeScriptMenu({ isEffective: true, hasMatchOverride: false, matchesTopFrame: false })], fullScriptCount: 1, }); @@ -293,19 +264,6 @@ describe("Popup 脚本快捷设置与站点范围操作", () => { expect(screen.queryByRole("button", { name: "允许在 example.com 执行" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "排除在 example.com 上执行" })).not.toBeInTheDocument(); }); - - it("开关关闭且本站不生效时隐藏包含与排除动作", () => { - mockData = makeData({ - scriptList: [makeScriptMenu({ isEffective: false, hasMatchOverride: true })], - fullScriptCount: 1, - }); - render(); - - fireEvent.click(screen.getByRole("button", { name: /Script A/ })); - - expect(screen.queryByRole("button", { name: "允许在 example.com 执行" })).not.toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "排除在 example.com 上执行" })).not.toBeInTheDocument(); - }); }); describe("Popup 脚本列表展开/收起", () => { diff --git a/src/pages/popup/App.tsx b/src/pages/popup/App.tsx index 56e7c1e8e..069fafff5 100644 --- a/src/pages/popup/App.tsx +++ b/src/pages/popup/App.tsx @@ -167,9 +167,7 @@ export default function App() { onOpenEditor={data.handleOpenEditor} onOpenScriptSettings={data.handleOpenScriptSettings} onOpenUserConfig={data.handleOpenUserConfig} - onExcludeUrl={data.handleExcludeUrl} onExcludeFromMatch={data.handleExcludeFromMatch} - showSiteScopeActions={data.popupSiteScopeActions} onOnlyRunOnUrl={data.handleOnlyRunOnUrl} onAllowUrl={data.handleAllowUrl} onMenuClick={data.handleMenuClick} @@ -478,9 +476,7 @@ interface ScriptRowProps { onOpenEditor: (uuid: string) => void; onOpenScriptSettings: (uuid: string) => void; onOpenUserConfig: (uuid: string) => void; - onExcludeUrl?: (uuid: string, isEffective: boolean) => void; onExcludeFromMatch?: (uuid: string) => void; - showSiteScopeActions?: boolean; onOnlyRunOnUrl?: (uuid: string) => void; onAllowUrl?: (uuid: string) => void; onMenuClick: (uuid: string, menus: ScriptMenuItem[], inputValue?: any) => void; @@ -499,9 +495,7 @@ function ScriptRow({ onOpenEditor, onOpenScriptSettings, onOpenUserConfig, - onExcludeUrl, onExcludeFromMatch, - showSiteScopeActions = false, onOnlyRunOnUrl, onAllowUrl, onMenuClick, @@ -517,13 +511,6 @@ function ScriptRow({ const shouldTruncateMenus = menuExpandNum > 0 && allVisibleMenus.length > menuExpandNum; const visibleMenus = shouldTruncateMenus && !isMenuExpanded ? allVisibleMenus.slice(0, menuExpandNum) : allVisibleMenus; - const excludeSite = showSiteScopeActions - ? onExcludeFromMatch - ? () => onExcludeFromMatch(script.uuid) - : undefined - : onExcludeUrl - ? () => onExcludeUrl(script.uuid, true) - : undefined; // 只匹配到子 frame(iframe)的脚本:站点范围操作按顶层 host 生成规则,对它不成立,故不显示 const siteHost = isPageScript && script.matchesTopFrame !== false ? host : undefined; const statusBadge = getStatusBadge(script, isPageScript, t); @@ -624,25 +611,25 @@ function ScriptRow({ > {t("editor:script_setting")} - {siteHost && showSiteScopeActions && script.isEffective === false && onAllowUrl && ( + {siteHost && script.isEffective === false && onAllowUrl && ( } primary onClick={() => onAllowUrl(script.uuid)}> {t("allow_on_site").replace("$0", siteHost)} )} - {siteHost && - showSiteScopeActions && - script.isEffective === true && - !script.hasMatchOverride && - onOnlyRunOnUrl && ( - onOnlyRunOnUrl(script.uuid)}> - } primary> - {t("only_on_site").replace("$0", siteHost)} - - - )} - {/* 排除 host 无需确认;站点范围操作开启时同步维护 match 与 exclude 覆盖。 */} - {siteHost && script.isEffective === true && excludeSite && ( - } warn onClick={excludeSite}> + {siteHost && script.isEffective === true && !script.hasMatchOverride && onOnlyRunOnUrl && ( + onOnlyRunOnUrl(script.uuid)}> + } primary> + {t("only_on_site").replace("$0", siteHost)} + + + )} + {/* 关掉本站执行无需确认:优先从匹配移除,SW 判断移不掉时才写排除 */} + {siteHost && script.isEffective === true && onExcludeFromMatch && ( + } + warn + onClick={() => onExcludeFromMatch(script.uuid)} + > {t("exclude_off").replace("$0", siteHost)} )} diff --git a/src/pages/popup/preload.test.ts b/src/pages/popup/preload.test.ts index 97fbc18bf..b3291c2c5 100644 --- a/src/pages/popup/preload.test.ts +++ b/src/pages/popup/preload.test.ts @@ -8,7 +8,6 @@ const mocks = vi.hoisted(() => ({ getMenuExpandNum: vi.fn(async () => 8), getScriptListExpandNum: vi.fn(async () => 21), getPopupCompactLayout: vi.fn(async () => true), - getPopupSiteScopeActions: vi.fn(async () => true), getProvider: vi.fn(async () => "greasyfork"), getPopupData: vi.fn(), })); @@ -22,7 +21,6 @@ vi.mock("../store/global", () => ({ getMenuExpandNum: mocks.getMenuExpandNum, getScriptListExpandNum: mocks.getScriptListExpandNum, getPopupCompactLayout: mocks.getPopupCompactLayout, - getPopupSiteScopeActions: mocks.getPopupSiteScopeActions, }, })); vi.mock("../store/features/script", () => ({ popupClient: { getPopupData: mocks.getPopupData } })); @@ -58,7 +56,6 @@ describe("Popup 数据预加载", () => { expect(mocks.getPopupData).toHaveBeenCalledWith({ tabId: 7, url: "https://example.com/page" }); expect(mocks.getPopupCompactLayout).toHaveBeenCalledOnce(); - expect(mocks.getPopupSiteScopeActions).toHaveBeenCalledOnce(); expect(mocks.getScriptListExpandNum).toHaveBeenCalledOnce(); }); }); diff --git a/src/pages/popup/preload.ts b/src/pages/popup/preload.ts index 00974b0d4..41855d772 100644 --- a/src/pages/popup/preload.ts +++ b/src/pages/popup/preload.ts @@ -18,7 +18,6 @@ export type PopupInitialData = { menuExpandNum: number; scriptListExpandNum: number; popupCompactLayout: boolean; - popupSiteScopeActions: boolean; defaultScriptProvider: ScriptProvider; pageStatus: TPopupPageStatus; scriptList: ScriptMenu[]; @@ -35,25 +34,16 @@ export const scriptListSorter = (a: ScriptMenu, b: ScriptMenu) => const popupDataQuery = createPreloadableQuery<"popup", PopupInitialData>({ key: (key) => key, load: async (_key, signal) => { - const [ - tab, - isEnableScript, - checkUpdate, - menuExpandNum, - scriptListExpandNum, - popupCompactLayout, - popupSiteScopeActions, - provider, - ] = await Promise.all([ - getCurrentTab(), - systemConfig.getEnableScript(), - systemConfig.getCheckUpdate({ sanitizeHTML }), - systemConfig.getMenuExpandNum(), - systemConfig.getScriptListExpandNum(), - systemConfig.getPopupCompactLayout(), - systemConfig.getPopupSiteScopeActions(), - cacheInstance.get("default_script_provider"), - ]); + const [tab, isEnableScript, checkUpdate, menuExpandNum, scriptListExpandNum, popupCompactLayout, provider] = + await Promise.all([ + getCurrentTab(), + systemConfig.getEnableScript(), + systemConfig.getCheckUpdate({ sanitizeHTML }), + systemConfig.getMenuExpandNum(), + systemConfig.getScriptListExpandNum(), + systemConfig.getPopupCompactLayout(), + cacheInstance.get("default_script_provider"), + ]); if (signal.aborted) throw new DOMException("Popup preload aborted", "AbortError"); @@ -75,7 +65,6 @@ const popupDataQuery = createPreloadableQuery<"popup", PopupInitialData>({ menuExpandNum, scriptListExpandNum, popupCompactLayout, - popupSiteScopeActions, defaultScriptProvider: provider ?? "scriptcat", pageStatus: popupData.pageStatus, scriptList: popupData.scriptList.sort(scriptListSorter), diff --git a/src/pages/popup/usePopupData.test.ts b/src/pages/popup/usePopupData.test.ts index 974b1def9..d66831886 100644 --- a/src/pages/popup/usePopupData.test.ts +++ b/src/pages/popup/usePopupData.test.ts @@ -11,7 +11,6 @@ const popupInitialData = vi.hoisted(() => ({ menuExpandNum: 5, scriptListExpandNum: 5, popupCompactLayout: false, - popupSiteScopeActions: false, defaultScriptProvider: "scriptcat" as const, pageStatus: "ok" as const, scriptList: [ @@ -286,7 +285,8 @@ describe("usePopupData 站点范围快捷操作", () => { await result.current.handleExcludeFromMatch("script-1"); }); - expect(mockExcludeFromMatch).toHaveBeenCalledWith("script-1", "*://example.com/*"); + // SW 需要 host 与完整网址:host 用于定位要移出的匹配,网址用于判断移出后是否还得补排除 + expect(mockExcludeFromMatch).toHaveBeenCalledWith("script-1", "example.com", "https://example.com/page"); expect(result.current.scriptList[0]?.isEffective).toBe(false); expect(result.current.scriptList[0]?.hasMatchOverride).toBe(true); expect(notify.success).not.toHaveBeenCalled(); diff --git a/src/pages/popup/usePopupData.ts b/src/pages/popup/usePopupData.ts index 6664e14d4..b6c8da565 100644 --- a/src/pages/popup/usePopupData.ts +++ b/src/pages/popup/usePopupData.ts @@ -98,7 +98,6 @@ export function usePopupData() { const [menuExpandNum, setMenuExpandNum] = useState(initialData?.menuExpandNum ?? 5); const [scriptListExpandNum, setScriptListExpandNum] = useState(initialData?.scriptListExpandNum ?? 5); const [popupCompactLayout, setPopupCompactLayout] = useState(initialData?.popupCompactLayout ?? false); - const [popupSiteScopeActions, setPopupSiteScopeActions] = useState(initialData?.popupSiteScopeActions ?? false); const [defaultScriptProvider, setDefaultScriptProvider] = useState( initialData?.defaultScriptProvider ?? "scriptcat" ); @@ -141,7 +140,6 @@ export function usePopupData() { setMenuExpandNum(initialData.menuExpandNum); setScriptListExpandNum(initialData.scriptListExpandNum); setPopupCompactLayout(initialData.popupCompactLayout); - setPopupSiteScopeActions(initialData.popupSiteScopeActions); setDefaultScriptProvider(initialData.defaultScriptProvider); setInitialized(true); } @@ -268,18 +266,6 @@ export function usePopupData() { window.close(); }, []); - const handleExcludeUrl = useCallback(async (uuid: string, isEffective: boolean) => { - const host = extractHost(stateRef.current.currentUrl); - if (!host) return; - try { - // isEffective=true → 排除(remove=false); isEffective=false → 取消排除(remove=true) - await scriptClient.excludeUrl(uuid, `*://${host}/*`, !isEffective); - setScriptList((prev) => prev.map((s) => (s.uuid === uuid ? { ...s, isEffective: !isEffective } : s))); - } catch (e) { - console.error("Failed to toggle exclude:", e); - } - }, []); - const handleOnlyRunOnUrl = useCallback( async (uuid: string) => { const host = extractHost(stateRef.current.currentUrl); @@ -299,10 +285,11 @@ export function usePopupData() { const handleExcludeFromMatch = useCallback( async (uuid: string) => { - const host = extractHost(stateRef.current.currentUrl); + const { currentUrl } = stateRef.current; + const host = extractHost(currentUrl); if (!host) return; try { - await scriptClient.excludeFromMatch(uuid, `*://${host}/*`); + await scriptClient.excludeFromMatch(uuid, host, currentUrl); setScriptList((prev) => prev.map((s) => (s.uuid === uuid ? { ...s, isEffective: false } : s))); } catch (e) { showError(String(e)); @@ -464,7 +451,6 @@ export function usePopupData() { handleOpenEditor, handleOpenScriptSettings, handleOpenUserConfig, - handleExcludeUrl, handleExcludeFromMatch, handleOnlyRunOnUrl, handleAllowUrl, @@ -486,7 +472,6 @@ export function usePopupData() { showAlert, menuExpandNum, popupCompactLayout, - popupSiteScopeActions, handleSearch, handleToggleExpand, }; diff --git a/src/pkg/backup/config_sections.ts b/src/pkg/backup/config_sections.ts index 2eb70982b..37ae0f872 100644 --- a/src/pkg/backup/config_sections.ts +++ b/src/pkg/backup/config_sections.ts @@ -15,7 +15,6 @@ const APPEARANCE_KEYS = new Set([ "badge_text_color", "favicon_service", "popup_compact_layout", - "popup_site_scope_actions", ]); const UPDATE_KEYS = new Set([ "check_script_update_cycle", diff --git a/src/pkg/config/config.test.ts b/src/pkg/config/config.test.ts index 49cd542dd..a31c26936 100644 --- a/src/pkg/config/config.test.ts +++ b/src/pkg/config/config.test.ts @@ -192,15 +192,6 @@ describe("SystemConfig 双 storage 与懒迁移", () => { expect(localData["system_popup_compact_layout"]).toBeUndefined(); }); - it("popup 站点范围快捷操作应默认关闭并写入 sync storage", async () => { - await expect(config.getPopupSiteScopeActions()).resolves.toBe(false); - config.setPopupSiteScopeActions(true); - await expect(config.getPopupSiteScopeActions()).resolves.toBe(true); - await expect(chrome.storage.sync.get("system_popup_site_scope_actions")).resolves.toMatchObject({ - system_popup_site_scope_actions: true, - }); - }); - it("编辑器偏好应返回默认值并写入 sync storage", async () => { await expect(config.getEditorPreferences()).resolves.toEqual({ version: 1, diff --git a/src/pkg/config/config.ts b/src/pkg/config/config.ts index f6f0249fa..6ed77a95c 100644 --- a/src/pkg/config/config.ts +++ b/src/pkg/config/config.ts @@ -588,14 +588,6 @@ export class SystemConfig { this._set("popup_compact_layout", val); } - getPopupSiteScopeActions() { - return this._get("popup_site_scope_actions", false); - } - - setPopupSiteScopeActions(val: boolean) { - this._set("popup_site_scope_actions", val); - } - async getLanguage() { if (globalThis.localStorage) { const cachedLanguage = localStorage.getItem("language"); diff --git a/src/pkg/utils/url_matcher.test.ts b/src/pkg/utils/url_matcher.test.ts index 62e7781fc..fc5b0bc33 100644 --- a/src/pkg/utils/url_matcher.test.ts +++ b/src/pkg/utils/url_matcher.test.ts @@ -7,6 +7,7 @@ import { extractMatchPatternsFromGlobs, extractSchemesOfGlobs, embeddedPatternCheckerString, + getMatchPatternHost, } from "./url_matcher"; describe.concurrent("extractMatchPatternsFromGlobs", () => { @@ -914,6 +915,25 @@ describe.concurrent("getApiMatchesAndGlobs-3 (全面性测试)", () => { }); }); +describe.concurrent("getMatchPatternHost", () => { + it.concurrent("应取出 match pattern 绑定的具体网域", () => { + expect(getMatchPatternHost("*://www.example.com/*")).toBe("www.example.com"); + expect(getMatchPatternHost("https://www.example.com/path/*")).toBe("www.example.com"); + // 无 scheme 的 TM 兼容写法 + expect(getMatchPatternHost("www.example.com/*")).toBe("www.example.com"); + }); + + it.concurrent("通配网域不绑定到任何具体网域", () => { + // ".example.com" 覆盖全部子域,"" 覆盖全部网域,都不等于任何单一 host + expect(getMatchPatternHost("*://*.example.com/*")).toBe(".example.com"); + expect(getMatchPatternHost("*://*/*")).toBe(""); + }); + + it.concurrent("非 match pattern 应返回 null", () => { + expect(getMatchPatternHost("/^https:\\/\\/example\\.com/")).toBeNull(); + }); +}); + describe.concurrent("embeddedPatternChecker", () => { // 构造 URLRuleEntry 的 ruleContent 格式与 extractUrlPatterns 一致 // match: [scheme, host, pathPattern], glob: string[], regex: [pattern, flags] diff --git a/src/pkg/utils/url_matcher.ts b/src/pkg/utils/url_matcher.ts index 37c13346d..a0e246114 100644 --- a/src/pkg/utils/url_matcher.ts +++ b/src/pkg/utils/url_matcher.ts @@ -230,6 +230,16 @@ export const extractUrlPatterns = (lines: string[]): URLRuleEntry[] => { return rules; }; +/** + * 取 @match pattern 归一化后的网域部分:具体网域返回该网域,`*.example.com` 返回 `.example.com`, + * `*` 返回空字串,不是 match pattern 则返回 null。 + * 用于判断一条 @match 是否只服务于某个具体网域(== host 时才是)。 + */ +export const getMatchPatternHost = (pattern: string): string | null => { + const [rule] = extractUrlPatterns([`@match ${pattern}`]); + return rule ? (rule.ruleContent as string[])[1] : null; +}; + export const toUniquePatternStrings = (x: URLRuleEntry[]) => { return [...new Set(x.map((e: URLRuleEntry) => e.patternString))]; };