✨ 增加网络规则管理(DNR) - #1598
Conversation
保存/启用/禁用/删除 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>
|
@cyfung1031 合并了还有冲突 |
这个新增的功能感觉还是怪怪的。应该要留到下个月处理 |
@CodFrm 先处理 bug修补的PR吧 |
# Conflicts: # docs/DOC-MAINTENANCE.md # docs/README.md
|
可以将这个也结合一下 #430 先记录一下 |
我之前有問過AI 應怎麼搞 主要問題是dnr 是定義rules 如果把dnr 做到像csp 移除那樣 我覺得與其這樣複雜不如不要搞 另外是rules 清理問題。 老實說沒什麼use case 的話就不要搞太多功能 csp 移除是有必要。 |
冲突解决:
- 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 侧值引入服务实现。
|
@cyfung1031 这个 PR 的方向我重新过了一遍,结论是泛化成通用的网络规则(DNR)管理,而不是再做一个 CSP 开关。写了份 spec,这里同步一下要点。 为什么泛化这个需求已经出现四次,但四次互不相通:#288 要的是通用的「改请求头/响应头、重定向、拦截」(对标 Header Editor 但要更可自定义),#1264 做「一键关掉 CSP 与 Trusted Types」,#1348 做带匹配引擎的 CSP 规则管理,本 PR 又做了一套 CSP 规则管理。缺的是一套统一的规则管理。移除 CSP 应该只是其中一个预设场景,不是立项理由。 现在这版的三个结构性问题
定下来的形态
关于 #430 / 脚本自带规则你之前提的两个点是对的,我记进 spec 了:DNR 规则必须在请求发出前就存在,等脚本跑起来再声明对当前文档已经太晚;而且运行时 API 缺一个脚本停用后的回收时机。所以真要做的话应该走脚本元数据声明,安装/启用时编译,生命周期天然等于脚本生命周期。 不过这块这次不做,形态也还没定,先按「列表里全部是用户创建的规则」实现,不为它预留字段或接口。等第一版落地、真有脚本作者提需求再说。 |
- 规则模型改为通用 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。
- 拖拽 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 摘要卡两处断言同步收紧:提示不得再声称隐身窗口生效,且必须指出 这项用户授权及其所在位置。
|
@cyfung1031 按前面说的方向,我直接在这个分支上做完了泛化,推了 16 个提交( 泛化的部分
运行时验证发现的三个缺陷(单测全绿时它们都活着)在真实 Chrome 里驱动构建产物才暴露的: 1. 真实 这段 read-back-verify 是从你原来的 2. DNR 的 3. 用户的屏蔽规则会静默打断 三条修完后真实浏览器里的判定:移除响应头 ✅、屏蔽请求 ✅、安装重定向不受影响 ✅、GM_xhr 不受干扰且不泄漏 隐身窗口:特判整个删掉了先前版本对用户声称「已启用的规则在隐身窗口同样生效」。实测这在默认状态下是假的——扩展未获隐身许可时(Chrome 默认),同一条 block 规则普通窗口拦住、隐身窗口请求照常打到服务端。 进一步查证后,整套 UI/UX 与性能另派了一轮审查,修了 13 条。要紧的:dnd-kit 的 关于 #1264,更正我上一条评论ID 撞车已经不存在了。 当时它的 测试套件修掉了本特性测试间的一处确定性跨文件泄漏: 另外这套 UI 测试原本骑在项目的 850ms 每测试预算线上:本特性的用例大多渲染 20–25 行整表(每行带 Radix 的 Checkbox/Switch/DropdownMenu 加 dnd-kit 接线),而行数是抄来抄去的、不是按断言需要选的,在 CI 的双核 runner 上就会超时。已按各自断言真正需要的行数裁掉,并把一条同时测两件事的用例拆开。没有抬任何 timeout、没有动 CI 现在全绿(Lint、两个 test shard、四个 E2E 分片全过)。 仓库本身仍有一个既有问题、不属于本 PR: 已知未做
有不同意见随时说,尤其是内部 |
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() 还原,绑定顺序不再影响断言。
|
@CodFrm agent只說明了改了什麼但沒說明 why , 這是很大問題 我不是scriptcat的作者。最後判斷還是交給你
其他的我沒所謂 |
|
感觉firefox也可以默认注入一条移除CSP规则/根据页面、脚本调用情况自动注入CSP
之前已有几个类似的需求,此外firefox也很需要csp的处理,否则很多页面无法使用 |
移除csp我同意 |
|
@CodFrm 要不要做這個2/10分的功能你自行判斷吧。 (用戶的要求是dynamic api. Dnr的特性加上spa, 你全力實現也只是一個6/10分的東西) |
选模板卡、列表动作徽标与表单里的动作字段此前一律 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 扫描已在前一个提交清掉。
1b26468 to
a233e6d
Compare

Checklist / 检查清单
背景
部分用户脚本在特定网站上会被 CSP response headers 阻止执行。ScriptCat 需要提供按网站管理的 CSP 规则,同时保留浏览器现有动态规则,并让高风险的全网站范围操作具备明确确认和可恢复的错误状态。
由于扩展使用 split incognito 模式,普通窗口和隐私窗口可能运行不同的 service worker。CSP 规则状态必须明确归属,不能依赖两个后台上下文之间的内存状态同步。
本次改动
csp_rule:state):incognito: "split",只在普通 service worker 注册 CSP rule handlers;隐私窗口显示普通窗口管理提示,不影响既有隐私窗口扩展页面流程。baseRevision;父级收到新快照后,旧表单仍使用打开时的 revision,冲突时刷新快照并关闭表单。tools文案,用户可见文本全部通过 i18n。实现考虑
1. CSP 状态只由普通 service worker 持有
Manifest 继续使用
incognito: "split",以兼容现有从隐私窗口打开确认页、安装页和其他扩展页面的流程。CSP service 通过
isCspRuleOwner(extensionEnv.inIncognitoContext)明确限制为普通上下文:这样不会通过全局切换到 spanning 模式改变既有隐私窗口行为,也不会让两个后台进程各自持有可互相覆盖的 CSP 内存状态。
2. revision 校验覆盖多个 service instance
每次 mutation 都会:
baseRevision;两个 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 规范化,路径、凭据和通配符不会作为有效域名保存。
建议审查重点
revision_conflict;baseRevision;参考
docs/specs/csp-rule-management.mddocs/develop.mddocs/design.md验证
pnpm test:ci— 310 test files, 3297 tests passedpnpm lint:ci— Prettier, TypeScript, and ESLint passedpnpm build— passed (repository bundle-size/Monaco warnings only)