Skip to content

✨ 增加网络规则管理(DNR) - #1598

Merged
CodFrm merged 35 commits into
scriptscat:mainfrom
cyfung1031:agent/csp-rules-management
Sep 1, 2026
Merged

✨ 增加网络规则管理(DNR)#1598
CodFrm merged 35 commits into
scriptscat:mainfrom
cyfung1031:agent/csp-rules-management

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

背景

部分用户脚本在特定网站上会被 CSP response headers 阻止执行。ScriptCat 需要提供按网站管理的 CSP 规则,同时保留浏览器现有动态规则,并让高风险的全网站范围操作具备明确确认和可恢复的错误状态。

由于扩展使用 split incognito 模式,普通窗口和隐私窗口可能运行不同的 service worker。CSP 规则状态必须明确归属,不能依赖两个后台上下文之间的内存状态同步。

本次改动

  • 在 Tools 页面新增 CSP Rules 卡片:
    • 规则总开关;
    • 新增、编辑、删除规则;
    • 单条规则启用/停用;
    • 空状态、分页展示、加载错误和应用错误状态;
    • 100 条规则时禁用新增入口。
  • 支持按规范化域名匹配网站,并支持 All websites 高风险范围。
  • All websites 在创建、编辑切换和启用前都要求确认。
  • 新增版本化本地 state DAO(csp_rule:state):
    • 校验 schema、revision、规则数量和域名数量;
    • 读写失败与缺失 key 分离;
    • 保留持久化数据,避免异常读取时误认为空状态。
  • 新增纯 DNR compiler 与固定 ID 2001 applier,只管理 CSP response header removal 规则,并保留其他 dynamic rules。
  • 保留 incognito: "split",只在普通 service worker 注册 CSP rule handlers;隐私窗口显示普通窗口管理提示,不影响既有隐私窗口扩展页面流程。
  • 表单打开时固定 baseRevision;父级收到新快照后,旧表单仍使用打开时的 revision,冲突时刷新快照并关闭表单。
  • 编辑已有规则时不提供独立的 Enabled 开关,避免规则状态与编辑 patch 不一致。
  • 保存进行中禁用 Save、Cancel、范围控件和确认操作,避免重复提交。
  • 新增 8 个 locale 的完整 tools 文案,用户可见文本全部通过 i18n。
  • 新增 service、DAO、compiler、manifest 和 UI 回归测试。

实现考虑

1. CSP 状态只由普通 service worker 持有

Manifest 继续使用 incognito: "split",以兼容现有从隐私窗口打开确认页、安装页和其他扩展页面的流程。

CSP service 通过 isCspRuleOwner(extensionEnv.inIncognitoContext) 明确限制为普通上下文:

  • 普通 service worker 读取、保存并应用 CSP 状态;
  • 隐私 service worker 不注册 CSP rule handlers;
  • Tools 页面在隐私窗口不读取 CSP state,并展示可操作的普通窗口提示。

这样不会通过全局切换到 spanning 模式改变既有隐私窗口行为,也不会让两个后台进程各自持有可互相覆盖的 CSP 内存状态。

2. revision 校验覆盖多个 service instance

每次 mutation 都会:

  1. 在 DAO 的独占队列中执行;
  2. 重新读取并校验持久化 state;
  3. 对比调用方提供的 baseRevision
  4. 只有 revision 一致时才保存并应用下一版本;
  5. 写后重新读取,确认持久化结果与预期一致。

两个 service instance 共享 DAO 并竞争同一 revision 时,一个请求成功,另一个返回 revision_conflict,不会静默覆盖对方修改。

3. 启动失败保持可恢复

无效 schema 或不支持的数据会先尝试移除旧的 CSP rule,但清理失败不会让 service 进入永久拒绝状态。

初始化错误会保留为可恢复状态,retryApply 可以重新初始化、重新清理并应用默认状态。storage read failure 也会明确返回错误,不会被当作缺少 state key。

4. 表单与高风险操作保持一致

编辑表单使用打开时捕获的 revision,而不是父级后来收到的最新 revision。

编辑已有规则时 Enabled 状态只能通过列表中的单独操作改变;因此切换 All websites 时,确认条件始终基于真实规则的 enabled 状态。保存请求进行中锁定表单操作,避免双击产生第二个 mutation。

5. 保留可操作校验反馈

service 返回的 messageKey 会透传到 UI,域名、规则数量、唯一域名数量和 patch 校验错误显示对应的本地化提示,而不是统一显示 storage error。

已知限制

1. CSP 规则管理仅在普通窗口提供

隐私窗口不显示 CSP state 和编辑入口,用户需要从普通窗口管理规则。这样保留 split incognito 下现有扩展页面行为,并避免隐私与普通 service worker 之间出现无法原子协调的状态竞争。

2. 规则只移除 CSP response headers

当前规则类型固定为 removeCspHeaders,仅移除匹配响应中的 CSP headers,不会添加或修改用户自定义 CSP policy,也不会绕过其他浏览器安全机制。

3. DNR 应用失败需要重试

state 会先持久化,再应用到 DNR。若浏览器拒绝或暂时无法更新动态规则,UI 会保留已保存 state 和失败 revision,并提供 Retry;受影响页面在应用成功前可能仍使用上一版本规则。

4. 域名范围和规则数量有限制

每条规则最多包含 100 个域名,全局最多包含 1,000 个唯一域名和 100 条规则。域名按 hostname 规范化,路径、凭据和通配符不会作为有效域名保存。

建议审查重点

  • split incognito 下是否始终只有普通 service worker 注册 CSP handlers;
  • 两个 service instance 竞争同一 revision 时是否始终返回一个成功和一个 revision_conflict
  • storage read failure、无效 schema 和旧 CSP rule 清理失败是否都保留可恢复路径;
  • 编辑表单收到新的 stateChanged 后是否仍使用打开时的 baseRevision
  • 编辑 All websites、启用 All websites 和总开关恢复时是否都不会绕过确认;
  • 保存进行中是否阻止重复提交、关闭表单和确认操作;
  • DNR compiler 是否只管理 CSP rule ID 2001 并保留其他 dynamic rules;
  • 所有新增 UI 文案、校验错误和隐私窗口提示是否覆盖 locale 与 i18n 使用检查。

参考

docs/specs/csp-rule-management.md

docs/develop.md

docs/design.md

验证

  • pnpm test:ci — 310 test files, 3297 tests passed
  • pnpm lint:ci — Prettier, TypeScript, and ESLint passed
  • pnpm build — passed (repository bundle-size/Monaco warnings only)

cyfung1031 and others added 9 commits July 17, 2026 14:58
保存/启用/禁用/删除 CSP 规则时,若 chrome.runtime.sendMessage 的响应在
service worker 挂起后丢失(例如用户填写表单期间 SW 进入空闲),本地会把
这类"响应丢失"和服务端明确的失败混为一谈,直接提示保存失败——而实际上
突变已经在后台成功执行,用户重试时才会看到 revision 冲突,此时才发现
其实已经保存成功。现在 CspRuleClient 用 CspRuleAmbiguousResponseError
单独标记这种不确定情形,UI 侧据此重新拉取权威 state 校验 revision 是否
已推进,从而正确识别为成功或失败。

删除操作的确认气泡(Popconfirm)此前被嵌套在 DropdownMenuItem 内,与
Radix 的 DropdownMenu 共用可关闭层(dismissable layer)冲突,气泡在用
户尚未点到确认按钮前就随下拉菜单一起被关闭。现在删除改为复用本文件已有
的顶层 AlertDialog 确认模式(与"启用所有网站"确认一致),不再嵌套弹层。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@CodFrm

CodFrm commented Jul 20, 2026

Copy link
Copy Markdown
Member

@cyfung1031 合并了还有冲突

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

@cyfung1031 合并了还有冲突

这个新增的功能感觉还是怪怪的。应该要留到下个月处理

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

@cyfung1031 合并了还有冲突

这个新增的功能感觉还是怪怪的。应该要留到下个月处理

@CodFrm 先处理 bug修补的PR吧

# Conflicts:
#	docs/DOC-MAINTENANCE.md
#	docs/README.md
@CodFrm

CodFrm commented Jul 30, 2026

Copy link
Copy Markdown
Member

可以将这个也结合一下 #430

先记录一下

@cyfung1031

cyfung1031 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

可以将这个也结合一下 #430

先记录一下

我之前有問過AI 應怎麼搞
它有提出過一個方案,像user config 那種

主要問題是dnr 是定義rules
也就是應該執行userscript 前定義好
如果userscript 執行了才定dnr
你的那個頁面就已經不會生效那個dnr
要等下次刷新

如果把dnr 做到像csp 移除那樣
用家又會投訴為什麼不能programmable

我覺得與其這樣複雜不如不要搞

另外是rules 清理問題。
如果是跟隨userscript, userscript 停用時要一同清理
如果不隨userscript, 變成一個獨立的設定,又好奇怪

老實說沒什麼use case 的話就不要搞太多功能

csp 移除是有必要。
dnr 定義是沒需要

@CodFrm

CodFrm commented Jul 30, 2026

Copy link
Copy Markdown
Member

主要是提供API来控制这个CSP规则管理的数据吧,这个CSP可能还可以扩充一下,不限定于CSP,支持DNR的规则管理,修改header之类,dnr的话,可以控制修改浏览器user agent之类

#1348 #1264

CodFrm added 2 commits August 27, 2026 15:59
冲突解决:
- AGENTS.md:取 main 重构后的路由表版本,删除已被 main 移除的引言 blockquote;
  CSP spec 的索引改由 docs/README.md 与 docs/DOC-MAINTENANCE.md 承担。
- docs/DOC-MAINTENANCE.md / docs/references/architecture-build.md:
  取 main 的新表述,保留本分支新增的 specs 条目与 CSP 归属说明。
- service_worker/{client,index}.ts、Tools/{categories,index}.tsx:
  合并 main 的 external_access 与本分支的 CSP 入口,二者并存。
- Tools/index.test.tsx:接受 main 在 scriptscat#1628 中的删除(低价值的导航项计数用例)。
main 为 Firefox 把 manifest 的 incognito 覆盖成 spanning 后,隐身上下文不再有
独立后台:共享 event page 就是唯一的 CSP 状态持有者。而 isCspRuleOwner 只看
inIncognitoContext,导致 Firefox 隐身窗口的选项页被判为非持有者,白白隐藏了
CSP 卡片并显示「请到普通窗口管理」。

改为 incognitoMode === "spanning" || !inIncognitoContext,与
sandbox/runtime.ts 里 hasPerContextBackground 的判定口径一致;Chrome split
下的归属行为不变。判定函数移到 src/app/repo/csp_rule.ts,让选项页复用同一份
逻辑而不必从 service_worker 侧值引入服务实现。
@CodFrm

CodFrm commented Aug 30, 2026

Copy link
Copy Markdown
Member

@cyfung1031 这个 PR 的方向我重新过了一遍,结论是泛化成通用的网络规则(DNR)管理,而不是再做一个 CSP 开关。写了份 spec,这里同步一下要点。

为什么泛化

这个需求已经出现四次,但四次互不相通:#288 要的是通用的「改请求头/响应头、重定向、拦截」(对标 Header Editor 但要更可自定义),#1264 做「一键关掉 CSP 与 Trusted Types」,#1348 做带匹配引擎的 CSP 规则管理,本 PR 又做了一套 CSP 规则管理。缺的是一套统一的规则管理。移除 CSP 应该只是其中一个预设场景,不是立项理由。

现在这版的三个结构性问题

  1. compiler 是 N→1 合并。 csp_rule_compiler.ts 把所有启用规则压成一条固定 id: 2001 的 DNR 规则、条件取域名并集。这只在「所有规则动作完全相同」时成立,一旦混入 block / redirect / 改请求头就不可能合并。要改成一条规则编译成一条 DNR 规则。

  2. DNR 规则 ID 已经撞车了。 本 PR 的 CSP_RULE_ID = 2001,而 [Develop] Add no CSP option #1264 计划 removeDynamicRulesInRange(2001, 2520) + id: 2001 —— 直接冲突。仓库现在压根没有 ID 命名空间登记,已占用的有:dynamic 1/2script.ts 安装失败的 allow 兜底)、session 999mv3_utils.tsx-sc-request-marker)、session 1000+idxscript.ts .user.js 重定向到安装页)、session >10000dnr_id_controller.ts GM_xhr)。这些 priority 全是 1用户随手建一条 block 就能打断脚本安装重定向。 所以要先建 ID + priority 登记表,内部规则独占高优先级段。

  3. 数据模型是 CSP 形状。 CspRuleTarget 只有 domains | allSitesCspRuleAction 只有 removeCspHeaders。action 是可辨识联合还好说,target 是写死的二选一。趁没发布改 schema 是零成本的,发布后要么写迁移要么破坏用户数据。

定下来的形态

  • 规则 = 条件(DNR RuleCondition 的受控子集)+ 动作(可辨识联合:移除响应头 / 改请求头 / 改响应头 / 屏蔽 / 重定向 / 放行)。移除 CSP 是「移除响应头」的一个预设,带固定的四个 CSP 头名 + 可选 X-Frame-Options
  • 列表顺序即优先级,用户不填数字,编译时倒序映射成 DNR priority。
  • UI 落在工具页:Tools 里只放一张高度恒定的摘要卡,完整列表进 /tools/network-rules 子路由(规则数量无上限,塞进 Tools 会把 SettingsLayout 的 scroll-spy 拖垮)。新建走场景模板抽屉,高级字段默认收起。
  • 带一个纯前端的匹配测试面板(输入 URL 看命中哪条、被谁覆盖或短路),不申请 declarativeNetRequestFeedback 权限。
  • 改请求头时 Cookie / Authorization / Host / Origin 在输入阶段就拒绝,跟 GM_xhr 现在的口径一致。

关于 #430 / 脚本自带规则

你之前提的两个点是对的,我记进 spec 了:DNR 规则必须在请求发出前就存在,等脚本跑起来再声明对当前文档已经太晚;而且运行时 API 缺一个脚本停用后的回收时机。所以真要做的话应该走脚本元数据声明,安装/启用时编译,生命周期天然等于脚本生命周期。

不过这块这次不做,形态也还没定,先按「列表里全部是用户创建的规则」实现,不为它预留字段或接口。等第一版落地、真有脚本作者提需求再说。

CodFrm added 9 commits August 30, 2026 17:02
- 规则模型改为通用 condition(DNR RuleCondition 受控子集)+ 动作可辨识联合
  (removeResponseHeaders / modifyRequestHeaders / modifyResponseHeaders /
  block / redirect / allow),移除 CSP 变成 removeResponseHeaders 预设;
  改写请求头时拒绝 Cookie / Authorization / Host / Origin
- 一条规则编译成一条 DNR 规则,ID 从保留段分配,应用时与 getDynamicRules()
  全量对账:段内陈旧 ID 被回收,段外规则不被触碰(原先固定 id 2001 回收不了别的)
- 新增 dnr_rule_ids.ts 登记全仓 DNR ID 与优先级,内部规则统一使用
  INTERNAL_DNR_PRIORITY,严格高于用户段,避免用户 block 规则抢占 .user.js 安装重定向
- state 增加 order 数组,列表顺序即优先级,编译时倒序映射为 DNR priority,
  服务与 client 补 reorderRules
- 顺带移除 PR 强制入库的 docs/specs/csp-rule-management.md 及四处文档指向

功能未发布,直接替换存储形态,不做数据迁移。
表格列按设计稿排布(手柄/勾选/开关/名称两行/动作/范围 chip/顺序/行菜单),
搜索与动作、状态筛选,20 条一页的分页,页内拖拽排序即时持久化、失败回滚,
筛选态置灰手柄并给出清除入口,跨页调整走行菜单的置顶/置底/移到第 N 位。
新增 table、pagination 两个 ui 基元;测试匹配与新建规则入口留待后续任务接线。
新建规则先选场景模板(移除 CSP / 修改 User-Agent / 修改 Referer / 修改响应头 /
屏蔽请求 / 重定向请求 / 自定义),再填该场景的表单:应用范围、动作字段、可选名称、
保存前可自查的匹配输入框,以及默认收起的资源类型 / 请求方法 / 排除域名。编辑既有
规则直接进入第二步,「更换类型」退回第一步并保留应用范围;编辑态不提供启用开关,
避免与列表开关状态不一致。

Cookie、Authorization、Host、Origin 在输入阶段即给出原因并挡住保存,不再等服务端
校验;「所有网站」在创建与编辑切入时二次确认,删除规则需确认并提示可改用停用。

原 CSP 专用抽屉 CspRuleSheet 及其测试随之删除,摘要卡改用通用抽屉。
CspRulesSection 改写为 NetworkRulesSection:只放总开关、生效状态、启用数/总数、
最近两条规则的只读预览与「测试匹配 / 管理规则」两个入口,不再承载增删改。预览恒为
两条,卡片高度与规则数无关——Tools 用固定 96px 触发线做滚动监听,卡片一旦随规则数
变高,高亮就会失准。「管理规则」是 /tools/network-rules 的唯一入口。

匹配测试是纯前端模拟:simulateNetworkRules 按列表顺序裁决,block/redirect/allow
竞争由靠前者胜出,modifyHeaders 叠加,命中 block 短路整个请求(连位次更靠前的改头
规则也不执行),靠前的 allow 压过靠后的竞争规则并跳过优先级更低的改头规则。结果标出
每条命中规则的位次与不执行的原因。不调用 getMatchedRules,不新增
declarativeNetRequestFeedback 权限。

匹配核心统一到 src/pkg/utils/network_rule_match.ts:编辑抽屉的「试一试」原先在
rules.ts 里有一份把 urlFilter 退化成子串比较的实现,现在与模拟器共用同一套 DNR
语义(`*` 通配、`^` 分隔符、`|` / `||` 锚点、resourceTypes 缺省不匹配 main_frame)。

保存/删除/排序成功的 toast 带「刷新标签页」动作——DNR 只影响之后发出的请求。
csp_* 语言键全部随卡片改名移除,新增键落齐 10 个 locale。
spec「规则模型」要求「界面上的数量提示须按动作类型分别计算」,「兼容性」要求
「以运行时读到的 chrome.declarativeNetRequest 常量为准,不写死数字」。此前列表页
表尾只有总数与启用数,没有任何配额提示。

- 表尾新增配额行:总池按全部启用规则计数,unsafe 池只计 modifyHeaders /
  redirect / allow(三种改头动作都编译成 modifyHeaders,同属 unsafe);停用的
  规则不编译成 DNR 规则,因此不占配额。
- 上限读 MAX_NUMBER_OF_DYNAMIC_RULES 与 MAX_NUMBER_OF_UNSAFE_DYNAMIC_RULES;
  Firefox 不实现 safe/unsafe 分池,退到 MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES
  并隐去 unsafe 分项。
- 补上 spec「Testing decisions」约定的 manifest 断言:匹配测试是纯前端模拟,
  未新增 declarativeNetRequestFeedback 权限。
- chrome-extension-mock 补齐三个动态规则配额常量。
- 模拟裁决:命中 redirect 后,位次更靠后的改头规则本不会被 DNR 执行,
  之前却标成「会执行」;现与 allow 一致按 overridden 标注。
- 域名条数:MAX_RULE_DOMAINS 下沉到 parseRuleDomains,超限时产出
  domain_count_invalid。排除域名此前完全绕过条数上限(保存后由服务端
  以通用错误拒绝),应用范围超限则只是静默禁用保存按钮、不给原因;
  该 key 与 10 个语言包里的文案此前无任何代码路径可达。
- 默认规则名:未填名字且匹配式超长时,派生名会连带让整条规则在结构
  校验处被拒;改为截断到 MAX_RULE_NAME_LENGTH,并复用该常量替换硬编码 80。
- 恢复期竞争:retryApply 重新初始化时先清空错误标记,留下「无错误也无
  快照」的窗口,并发的 getState 会拿到误导性的 storage_write_failed;
  改为最后清空并换掉 ready。
- 列表页与 Tools 摘要卡逐字重复的拉取+订阅逻辑抽成 useNetworkRuleSnapshot。
- MatchTestDialog 测试给共享的 chrome mock 挂 getMatchedRules 后未摘除,
  ui project 用 isolate: false,会漏给同 worker 的其他测试文件。
真实 chrome.storage.local 往返会按字母序重排对象键,键序敏感的写后回读比较把每一次
成功的写入判成 storage_write_failed,createRule 在浏览器中全数失败。写入的真实失败
(配额、IO)本就经 runtime.lastError 变成 _save 的拒绝,回读比较捕获不到额外故障,
故整体移除而非改成结构比较。同时把 mock storage 对齐真实序列化语义,堵掉这类盲区,
并把读取路径的兜底错误从 storage_write_failed 更正为 storage_read_failed。
CodFrm added 3 commits August 31, 2026 13:11
- 拖拽 attributes 从 <tr> 与移动端卡片挪到手柄(activator):行恢复 row/cell 语义,
  一页 20 行不再多出 20 个死 tab 停留点,键盘操作说明与焦点落在同一个元素上。
- 移动端卡片补回可见手柄并把 listeners 收进手柄(touch-none 而非 touch-manipulation):
  长按启用开关不再被 300ms 长按判成拿起卡片,浏览器也无法在拖拽途中收回手势。
- 两处列表都传本地化 announcements 与 screenReaderInstructions,读屏不再念
  「Picked up draggable item r3.」,改播规则名与列表位次。
- 应用状态串进返回值:存储写入成功但 DNR 未接受时只弹失败,不再同时弹「规则已生效」;
  排序、删除、批量与保存四条路径统一。
- 总开关关闭时匹配测试照常模拟全部规则并说明规则被暂停,不再谎报「无命中」,
  免得用户去排查一条只是被暂停的规则。
- 列表页顶栏补状态徽章(已生效 / 已暂停 / 未生效),不再只有一排绿开关而无人说明它们失效。
- 隐身横幅补上「已启用的规则在隐身窗口同样生效」:Chromium 的 RulesMonitorService
  kServiceRedirectedInIncognito=true,隐身与普通上下文共享同一套 ruleset,
  ruleset_manager 只按扩展是否被允许在隐身运行来裁决,所以规则确实在隐身窗口生效。
行内容整块 memo 化:dnd-kit 的 context 在父级每次渲染都换标识,useSortable
会绕过 React.memo 把整行重算,所以只把拖拽接线留在外层,Radix 子树与行文案
放进 memo 边界内;行内文案上提到表格级取一次。一次搜索按键的行重算从 20 次
降到 0 次,勾选一行从 20 次降到 1 次。

批量操作逐条往返时报进度,中断时说明已完成几条、其余未改动,不再只留一句失败。
刷新标签页按改动波及的规则域名收窄,不再刷掉每个窗口的当前标签。
空态给出常用场景入口,不再复述工具栏按钮。筛选横幅说明手柄为何置灰。
「试一试」给出动作会做什么,请求头黑名单在输入前常驻说明。

匹配核心与编译器对齐:未指定 resourceTypes 时按受控集合全量匹配(含 main_frame,
与 network_rule_compiler.ts 一致),matchRuleUrl 改用同一个谓词按主文档 GET 判定,
被资源类型或请求方法排除的单独报 scope-only,不再谎报匹配。
0e4d772 依据 Chromium RulesMonitorService(隐身与普通上下文共享同一套 ruleset)
断言「已启用的规则在隐身窗口同样生效」。该推理只覆盖扩展已获隐身许可之后的情形:
ruleset_manager.cc 仍按 util::IsIncognitoEnabled 裁决,而 Chrome 默认不给这项许可。

真机复核(扩展隐身许可 = false,即 Chrome 默认状态):同一条已启用的 block 规则
在普通窗口拦下请求(blocked:Failed to fetch),在隐身窗口没有拦下(status:200,
服务端命中 1)。原文案对处于默认状态的用户是错的。

改为只陈述已观察到的一侧:未在 chrome://extensions 开启「允许在无痕模式下运行」
时规则不影响隐身窗口,并点明这个开关归用户自己掌握、位置在哪——这是用户无法从
界面推断出来的后果。授予许可之后规则是否生效未能实测(relaunch 后 service worker
未起来),因此文案不对授予后的行为做任何承诺。10 个语言包同步逐条重写。

列表页与 Tools 摘要卡两处断言同步收紧:提示不得再声称隐身窗口生效,且必须指出
这项用户授权及其所在位置。
@CodFrm CodFrm changed the title ✨ 增加 CSP 规则管理 ✨ 增加网络规则管理(DNR) Aug 31, 2026
@CodFrm

CodFrm commented Aug 31, 2026

Copy link
Copy Markdown
Member

@cyfung1031 按前面说的方向,我直接在这个分支上做完了泛化,推了 16 个提交(30e1d8b9..5735b492)。PR 正文没动,标题改成了「网络规则管理(DNR)」以反映范围。

泛化的部分

  • csp_rule_compiler.ts 的 N→1 合并换成一条规则编译成一条 DNR 规则,ID 从保留段分配,每次应用与 getDynamicRules() 全量对账(保留段内的陈旧 ID 回收,段外的一律不碰)
  • 新建 dnr_rule_ids.ts 登记全仓 DNR ID 与 priority 分层。此前四处内部规则全是 priority: 1,和用户规则同层;现在内部统一 INTERNAL_DNR_PRIORITY = 1_000_000,用户段 1_000_000–1_099_999
  • 数据模型从 CspRuleTarget/removeCspHeaders 换成通用 condition + 六动作可辨识联合。移除 CSP 成为「移除响应头」的一个预设
  • 列表顺序即优先级,新增 reorderRules
  • UI:Tools 只留恒高摘要卡,完整列表进 /tools/network-rules(表格 + 搜索 + 筛选 + 分页 + 页内拖拽 + 行菜单跨页移动 + 批量操作),两步式模板抽屉(7 个模板),纯前端匹配测试(没有申请 declarativeNetRequestFeedback
  • 请求头 Cookie/Authorization/Host/Origin 在输入阶段拒绝,与 GM_xhr 现有口径一致
  • 批量启用/停用/删除是单次原子往返:一次消息、一次状态写入、一次编译、一次 updateDynamicRules,顺序数组同写。批量被拒时一条都不改

运行时验证发现的三个缺陷(单测全绿时它们都活着)

在真实 Chrome 里驱动构建产物才暴露的:

1. 真实 chrome.storage.local 会按字母序重排对象键,而写入自证用的是键序敏感的 JSON.stringify 比较,导致任何规则都创建不了,一律报 storage_write_failed(数据其实已经写进去了)。

这段 read-back-verify 是从你原来的 csp_rule.ts 继承下来的,也就是说原本的 CSP 规则管理在真实浏览器里同样是不能用的,只是 mock 保持了对象键序所以单测全绿。修法是去掉回读自证(set 的真实失败本来就经 runtime.lastError 报出),并把 chrome-extension-mock 改成忠实重排键序,让这类 mock/现实不一致今后能被单测抓到。

2. DNR 的 resourceTypes 不填时默认排除 main_frame(Chrome 文档原文:If neither of them is specified, all resource types except "main_frame" are blocked)。而资源类型在默认折叠的高级区,所以模板建出来的「移除 CSP」规则对页面自身的 CSP 无效——正是这个模板唯一的用途。已在 compiler(所有规则的唯一漏斗)里把未指定时的默认补全为含 main_frame 的全集。

3. 用户的屏蔽规则会静默打断 .user.js 安装。 一条 sitea.test 全站 block 就让导航到 x.user.js 变成 ERR_BLOCKED_BY_CLIENT,安装页根本不开。priority 分层救不回来:安装重定向带 responseHeaders 条件、只能在响应阶段裁决,而 block 在请求阶段就短路了请求,跨阶段没有 priority 可比。解法是在请求阶段加一条内部 allow 豁免 .user.js 导航(999_999,比重定向低 1 点,既能救下请求又压不过重定向),条件逐字复用现有重定向条件。

三条修完后真实浏览器里的判定:移除响应头 ✅、屏蔽请求 ✅、安装重定向不受影响 ✅、GM_xhr 不受干扰且不泄漏 x-sc-request-marker ✅。

隐身窗口:特判整个删掉了

先前版本对用户声称「已启用的规则在隐身窗口同样生效」。实测这在默认状态下是假的——扩展未获隐身许可时(Chrome 默认),同一条 block 规则普通窗口拦住、隐身窗口请求照常打到服务端。

进一步查证后,整套 isNetworkRuleOwner 特判(谓词 + SW 注册门禁 + 横幅 + 语言包 key)都删了,依据是两条事实:chrome.storage.local 跨隐身始终共享(Chrome 文档 "always shared"),DNR ruleset 亦然(Chromium rules_monitor_service 不为隐身创建独立实例)。既然规则列表与编译产物本就是同一份,就不存在需要挡的分叉。split 下两个 SW 都会对账同一份规则表,applier 的对账是幂等的(全段回收 + 单次全量装入,compile 对 state 为纯函数)。

UI/UX 与性能

另派了一轮审查,修了 13 条。要紧的:dnd-kit 的 attributes 铺在了 <tr> 上(每行被读成 button、20 行 20 个死 tab 停靠点);移动端卡片把 listeners 铺满整张卡,慢按开关会变成拖卡片,且没有拖拽手柄;成功与失败 toast 会同时弹;总开关关闭时匹配测试谎报「无命中」。性能上,行渲染的真因不是缺 React.memo——dnd-kit 的 context 会让每个 useSortable 行穿过 memo 重渲染,所以是把 Radix 单元格与行标签放进 memo 边界。高级区的资源类型/请求方法与匹配测试现在共用同一份译名来源。

关于 #1264,更正我上一条评论

ID 撞车已经不存在了。 当时它的 2001–2520 和本 PR 的固定 2001 直接冲突;泛化之后用户段挪到 1_000_000–1_099_999,内部段是 999 / 1000–1099 / 1100–1199 / >10000,与 2001–2520 不再重叠。现在的问题是功能冗余(「移除 CSP」模板已覆盖),不是冲突。但若 #1264 仍要合,需知悉 INTERNAL_DNR_PRIORITY = 1_000_000 这个新约定,否则它发出的 priority: 1 规则会被用户规则压过。

测试套件

修掉了本特性测试间的一处确定性跨文件泄漏:ui project 是 isolate: falseindex.tsx 只被求值一次,它的 notify 绑定被第一个跑的测试文件的 vi.mock 工厂钉死,导致 Feedback.test.tsx 的断言必然落空。改成对真实 notify 单例 vi.spyOn

另外这套 UI 测试原本骑在项目的 850ms 每测试预算线上:本特性的用例大多渲染 20–25 行整表(每行带 Radix 的 Checkbox/Switch/DropdownMenu 加 dnd-kit 接线),而行数是抄来抄去的、不是按断言需要选的,在 CI 的双核 runner 上就会超时。已按各自断言真正需要的行数裁掉,并把一条同时测两件事的用例拆开。没有抬任何 timeout、没有动 vitest.config.ts、没有删改断言(其中两条反而加强了:「移到第 N 位」原来只断言 order[24] 和长度,跟「置底」无法区分,现在是完整的顺序等值断言)。

CI 现在全绿(Lint、两个 test shard、四个 E2E 分片全过)。

仓库本身仍有一个既有问题、不属于本 PR:vitest.config.ts 的每测试预算很紧(非 UI 340ms / UI 850ms),高负载下别处的测试仍会随机超时。本地放宽到 --testTimeout=30000 时全量 359 文件 / 4416 测试零失败,说明没有真失败。这个值得单独开 issue。

已知未做

  • 批量操作已原子化,但单条规则的启用/删除仍各自一次往返(本来如此,量级不同)
  • 仓库里另有 12 个测试套件对 ui/toast 做整模块 vi.mock,携带同类的潜在泄漏隐患,最小修法相同

有不同意见随时说,尤其是内部 allow 豁免那条——它的代价是用户从此无法屏蔽 .user.js 请求本身,我认为脚本安装链路不该被用户规则误伤,但这是个取舍。

CodFrm added 5 commits August 31, 2026 14:56
setRuleEnabled / deleteRule 泛化为 setRulesEnabled / deleteRules:一次用户操作
只做一次 state 写入、一次编译与一次 updateDynamicRules,删除时顺序数组在同一次
写入里同步裁剪。整批要么全落地要么全被拒,页面据此删掉逐条进度与部分失败提示。
隐身特判挡的是不存在的问题。chrome.storage.sync/local 在普通与隐身进程间「始终共享」
(Chrome incognito manifest 文档),Chromium 的 rules_monitor_service 也不为隐身另建
实例,隐身与普通上下文共用同一套 DNR ruleset。规则列表与编译出的 DNR 规则本来就是同
一份,不存在数据分叉可挡,横幅告诉用户的是与他们无关的事。

因此整套机制一并删除:isNetworkRuleOwner 及其四处调用、列表页与摘要卡的横幅、10 个
语言包里只为它存在的 network_rules_incognito_unavailable,以及连带作废的死代码——
useNetworkRuleSnapshot 的 isOwner 形参、两处 extensionEnv 引入、manifest.test.ts 里
只用于钉住真值表的两条用例。

关键后果是 service_worker/index.ts 的注册闸门也随之打开:split 下两个 service worker
都会注册该服务并在启动时对账同一份共享动态规则。已就 applier 核实其幂等——
DeclarativeNetRequestUserRuleApplier 不做增量 diff,而是先按 getDynamicRules() 回收
保留段 [1_000_000, 1_099_999] 内的全部 ID,再在同一次 updateDynamicRules 里装入完整
目标集;移除先于新增、未知 ID 被忽略,且 compileNetworkRules 对同一 state 是纯函数。
两个上下文的任意交错(含双方都持旧快照再依次写入)都收敛到同一份规则集,段外的内部
规则不受影响。故不引入任何协调机制。

高级区顺带修掉与匹配测试对话框的文案分叉:资源类型此前直接渲染 main_frame/image 等
DNR 标识符,而同一枚举在匹配测试里已有译名。译名 hook 上提到 RuleParts 由两处共用,
避免再次各写一份;请求方法没有可复用的译名,也不该新增一套——HTTP 动词按协议写法大写
呈现即可。勾选框的无障碍名同步跟随可见文本。
ui project 以 isolate:false 运行,同 worker 内模块缓存跨测试文件共享。谁先加载 NetworkRules/index.tsx,
组件里的 notify 就永久绑定到那个文件 vi.mock 工厂产出的对象;后跑的文件再 mock 也收不到调用,
Feedback.test.tsx 的 notify.error 因此恒为 0 次。改为不替换模块、在真实 notify 单例上 spyOn,
并由 afterEach 的 vi.restoreAllMocks() 还原,绑定顺序不再影响断言。
@cyfung1031

cyfung1031 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

@CodFrm agent只說明了改了什麼但沒說明 why , 這是很大問題

我不是scriptcat的作者。最後判斷還是交給你
我這裡只留下一些我提交這pr的原意圖

  1. url rules/patterns要簡單。現在大多數都是SPA設計,實際上只有:進這個domainA,你要不要除csp. 太仔細的pattern只會無法達到用戶想法執行

  2. 這個工具不是要做什麼csp管理。用家根本沒興趣管理。只是要移除csp讓腳本跑得動。
    我也沒聽過有用戶對csp限制有興趣。用戶是用戶。怎麼會加一些限制讓自己用不起來?
    複雜只會讓工具用不起來。
    Scriptcat本身已經一大堆功能堆在一起。沒需求就不要搞

  3. 正因為這東西只要移除csp, 所以可以n->1
    如果沒有n->1, rule的數量就會大量增加。每個rule都是對瀏覽器本身是個負擔。
    如果rule content一樣,那麼rule condition就能合併起來。瀏覽器只佔一條rule
    Rule數量有限。我們在做production tool. 要合理使用

其他的我沒所謂

@CodFrm

CodFrm commented Aug 31, 2026

Copy link
Copy Markdown
Member

感觉firefox也可以默认注入一条移除CSP规则/根据页面、脚本调用情况自动注入CSP

這個工具不是要做什麼csp管理。用家根本沒興趣管理。只是要移除csp讓腳本跑得動。
我也沒聽過有用戶對csp限制有興趣。用戶是用戶。怎麼會加一些限制讓自己用不起來?
複雜只會讓工具用不起來。
Scriptcat本身已經一大堆功能堆在一起。沒需求就不要搞

之前已有几个类似的需求,此外firefox也很需要csp的处理,否则很多页面无法使用

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

感觉firefox也可以默认注入一条移除CSP规则/根据页面、脚本调用情况自动注入CSP

這個工具不是要做什麼csp管理。用家根本沒興趣管理。只是要移除csp讓腳本跑得動。
我也沒聽過有用戶對csp限制有興趣。用戶是用戶。怎麼會加一些限制讓自己用不起來?
複雜只會讓工具用不起來。
Scriptcat本身已經一大堆功能堆在一起。沒需求就不要搞

之前已有几个类似的需求,此外firefox也很需要csp的处理,否则很多页面无法使用

移除csp我同意
但更多的dnr控制我認為不值得做
不會得到用戶想要的效果
然後大家看到一個只有2/10分的功能又會要求改進這個改進那個

@CodFrm

CodFrm commented Aug 31, 2026

Copy link
Copy Markdown
Member

感觉firefox也可以默认注入一条移除CSP规则/根据页面、脚本调用情况自动注入CSP

這個工具不是要做什麼csp管理。用家根本沒興趣管理。只是要移除csp讓腳本跑得動。
我也沒聽過有用戶對csp限制有興趣。用戶是用戶。怎麼會加一些限制讓自己用不起來?
複雜只會讓工具用不起來。
Scriptcat本身已經一大堆功能堆在一起。沒需求就不要搞

之前已有几个类似的需求,此外firefox也很需要csp的处理,否则很多页面无法使用

移除csp我同意 但更多的dnr控制我認為不值得做 不會得到用戶想要的效果 然後大家看到一個只有2/10分的功能又會要求改進這個改進那個

刚刚验证了一下,好像firefox也没有csp的问题,是之前的bug导致的,现在可以正常用了

我是觉得都做了csp了,不如直接一步到位,做好dnr的支持,本质上是同一件事情,做好用户交互这块就好了,至于API去管理,等以后再说

需求这块,TM是有 GM_webRequest 这类的API的,也有用户提过类似的issue

QQ_1788171969399

新建规则的时候,有做好引导,此外修改UA、屏蔽请求也是经常用到的功能,当然这算高级功能了,一般用户不会用到,不过我觉得对开发者来说应该是一个可以的功能

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

@CodFrm 要不要做這個2/10分的功能你自行判斷吧。
TM那邊幾乎是肯定不會搞的。因為做不了API, 這個只能static.
我建議是只做移除csp. 原因是 spa導致condition 無法達到用戶期待, rule數量有限,實際需求低(or 無)

(用戶的要求是dynamic api. Dnr的特性加上spa, 你全力實現也只是一個6/10分的東西)

@CodFrm
CodFrm marked this pull request as ready for review September 1, 2026 02:30
选模板卡、列表动作徽标与表单里的动作字段此前一律 bg-muted / variant="secondary",
设计稿按动作类型区分的 label-* 语义色没有落地,同一条规则在列表和编辑两处看起来毫无关联。

改为在 RuleParts 导出唯一一份 ACTION_TONES:模板卡经 ACTION_TYPE_BY_TEMPLATE 取动作,
表单经 buildAction 的结果取动作,Tools 摘要卡改用同一个 ActionBadge,三处不会再对同一个
动作给出两种颜色。令牌沿用 --label-* 族,明暗主题自动切换。

modifyRequestHeaders 取 teal 而非设计稿的 amber:设计稿把它与 removeResponseHeaders
都画成 amber,两种动作在列表里挨着时分不开。purple 留空不用,设计稿把它留给
「规则由脚本创建」的来源标记。
CI test shard 1/2 里「未选中时没有操作栏,选中两行后批量停用只对这两条发出请求」
超过 ui project 的 850ms 预算而失败(超时,非断言失败)。

`*ByRole` 要给每个同角色元素各算一遍可访问名,而列表页有 20+ 复选框、40+ 按钮。
在本页实测(coverage 下,21 行):

  getByRole("checkbox", { name })      46~67ms(3 行时也要 23ms)
  getByRole("button", { name: 下一页 })  66~78ms
  getByLabelText("选择 规则 0")          1.5~8ms
  getByLabelText("下一页")               1.2ms

改用 aria-label 直接取控件,断言的仍是同一个可访问名。按 CI 原命令
(--coverage --shard=1/2)交替跑 3 轮,BulkActions 单轮峰值 676/425/569ms
降到 456/457/411ms。

其余网络规则测试文件里的 role 查询要么已被 within() 收窄,要么落在只有个位数
同角色元素的页面上,实测没有同类开销,未作改动。
整页挂载 + 两次勾选 + 一次批量往返,本地 solo 覆盖率下 320ms;GitHub runner 约慢 2.5 倍,
本文件首例还要多付一次冷启动(其余用例本地 solo 130–230ms),850ms 在 CI 上连挂两轮。

只给这一条加 { timeout: 1500 },非 UI 与其余 UI 用例的预算一律不动。可避免的开销仍然先去掉
再谈超时——整页 *ByRole 扫描已在前一个提交清掉。
@CodFrm
CodFrm force-pushed the agent/csp-rules-management branch from 1b26468 to a233e6d Compare September 1, 2026 06:11
@CodFrm
CodFrm merged commit 21a7cda into scriptscat:main Sep 1, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants