--- name: chat-plus-adapter-debugger description: >- 为 Chat Plus 新框架编写、修复、审查站点适配脚本。先读取完整 builtin://chat-plus-adapter-debugger/SKILL.md 获取 skillToken;这次读取 SKILL.md 不需要 skillToken。不要用正则或局部读取只抓 token。后续调用不带或带错 skillToken 会报错并必须重试,所以正式调试前先拿到正确 token。 --- # Chat Plus Adapter Debugger ## 目标 产出或修复一段可直接粘贴到 Chat Plus 的站点适配脚本,并满足新框架硬规则: 1. 最外层必须 `return { ... }` 2. 必须包含 `meta` 3. 必须包含四个 hook: - `transformRequest` - `extractResponse` - `decorateBubbles` - `continueConversation` 4. `decorateBubbles` 必须使用 `ctx.helpers.ui.decorateProtocolBubbles(...)` 5. `continueConversation` 必须使用 `ctx.helpers.plans.dom(...)` 6. `continueConversation` 必须使用 `ctx.continuationText` 7. `extractResponse` 必须返回 `responseContentPreview` 8. `transformRequest` 要么使用 `ctx.helpers.buildInjectedText(...)` 改写请求,要么明确 `return null` 9. 输出抓取必须基于真实协议 / 真实响应,不接受用消息 DOM 反向抄正文代替 `extractResponse` 10. 输入实现允许两条路: - 优先请求层协议注入 - 协议层因加密、签名、opaque body 或其他原因无法稳定改写时,退回 DOM 发送 11. 生成脚本时必须使用平台 helpers:JSON 解析、文本转换、协议注入、气泡装饰、续写 DOM plan 都不要手写替代实现 ## 使用 chrome-cdp 准备页面调试前,先确认工具列表存在 `chrome-cdp`,并读取 `builtin://chrome-cdp/SKILL.md`。本 skill 的执行入口可直接代理常用 CDP 调试命令,也可用更贴近 Chat Plus 工作流的别名。 只有用户明确允许启动浏览器调试会话后,才使用浏览器调试命令。MCP Gateway 内置 `chrome-cdp` 默认通过原生 Chrome DevTools Protocol 启动或复用独立的持久 profile Chrome,不连接用户已经打开的 Chrome 调试端口。 常用别名: ```bash capture start capture clear network search network get network perf ``` 等价 CDP 命令: ```bash netclear net netget perfnet html [target] [selector] snap [target] evalraw [json] ``` 如果 `chrome-cdp` 启动或浏览器连接失败: 1. 运行 `stop` 2. 再运行 `open ` 或 `launch ` 3. 不要要求用户关闭已有 Chrome 或已有 DevTools 调试端口,除非用户明确要求连接既有浏览器 ## 抓样本流程 按这个顺序抓样本。不要把 URL、Performance index 或页面 DOM 文本当作稳定 request id。 1. 打开 / 选中目标页面,并确认处于用户真实登录态和真实使用模式 2. 执行 `capture start` 或 `netclear`,让 CDP Network 从用户操作前开始记录 3. 告诉用户现在只需要在页面里手动发送一条唯一探针文本,例如 `CHATPLUS_ADAPTER_PROBE_YYYYMMDD_001` 4. 用户回页面手动发送这条探针文本 5. 等用户明确回复:`发好了` 6. 执行 `network search <关键词>` 或 `net <关键词>` 搜索 CDP Network 记录 7. 按 URL / method / status / MIME / timing / body 线索找真实发送请求 8. 复制 `net` 输出里的 CDP request id 9. 执行 `network get ` 查看请求体、响应体、状态码、content-type 和传输类型 10. 根据 `network get` 输出总结协议结构,不复述敏感 headers 11. 只有当 body 太长、输出被截断、需要离线解码二进制/长流式内容时,才追加 `--request-file` / `--response-file` 保存到临时文件后再读取 12. 如果首条消息创建了新会话或跳转,必须在新会话页重新执行 `capture start`,让用户发第二条 follow-up 样本 硬要求: - 不要代用户发送首条探针消息 - 不要在用户说 `发好了` 前假设请求已经产生 - 不要把“让用户自己开 DevTools 自己找请求”当默认流程;如果已获许可,优先由 AI 用 `chrome-cdp` 直接看 - `netget` 输出可能包含 Authorization、Cookie、设备 ID、会话 ID;最终回答和中间总结都不要复述这些值 ## Request Id 规则 允许的查询方式: - 操作前先执行 `capture start` 或 `netclear` - 操作后执行 `network search `、`network search `、`network search `、`network search ` 或其它从候选里来的关键词 - WebSocket 请求同样先用 `network search <关键词>` 定位,再用 `network get ` 读取 frames - 如果 `network search` 没有结果,用 `network perf` 或 `perfnet` 辅助确认是否漏开 Network、页面是否跳转、请求是否发生在 worker / service worker 禁止的反推方式: - 不要把 Performance URL、Performance index 或 DOM 文本当作稳定 request id - 不要在没执行 `capture start` / `netclear` 的情况下期待 CDP Network 有用户操作前的历史记录 - 不要扩大到 console、storage、历史请求或页面气泡里猜 chat 请求 - 不要在未确认 request id 前读取 body 或生成 adapter ## 证据层 抓取必须按三层证据链推进: 1. CDP Network 层 - 用 `network search ` 搜索真实 request id - 用 `network get ` 补齐浏览器层原始请求体、响应体、状态码和 content-type 2. PerformanceResourceTiming 层 - 用 `network perf` 查看发送后新增资源 - Performance URL 只是候选线索,不是 request id 3. DOM 层 - 用 `html` / `snap` 确认用户消息、AI 消息、输入框、发送按钮、模式开关和消息气泡选择器 - DOM 文本不能替代 `extractResponse` 的真实响应协议 ## URL 已知但 Body / Response 缺失 先分类,不要直接归因为站点没有发请求: 1. URL 是错的:常见误拿 `OPTIONS`、埋点、标题生成、历史同步、配置、心跳、模型列表、附件上传 2. URL 是对的,但 `netget` 输出被截断或 body 太长:才用 request id 和 `--request-file` / `--response-file` 保存到临时文件 3. URL 是对的,但不是普通文本:按真实 transport 解析;不能稳定改写请求时,`transformRequest` 返回 `null` 4. URL 是对的,但首条消息重建页面或会话:进入新页面后抓第二条样本 5. URL 是对的,但 CDP Network 没有历史:重新 `capture start` 后再让用户发送 `netget` 看到的是网络 body,不是 F12 Preview。网络 body 可能包含应用层 framing / transport envelope,必须按协议层次还原。 网络 body 解码顺序: 1. 先看 `netget` 里的 status、content-type、content-encoding、request/response headers,不要只看文件扩展名 2. 如果是 `application/json` 或纯文本,按 UTF-8 直接读 3. 如果是 SSE / NDJSON,按文本流逐行解析 4. 如果 UTF-8 打开时在 JSON 前后出现 `\0`、控制字符、`�`、``、`` 等字节,先检查是否是 length-prefixed JSON 帧 5. 如果 payload 是 protobuf / MessagePack / CBOR / 压缩内容,就按对应协议 decoder 处理;不要盲目删除前几个字节 ## 模式差异处理 有些站点存在会明显改变请求结构、响应结构或消息 DOM 的模式开关,例如: - 思考 / 深度思考 - 联网搜索 / 智能搜索 - 专家模式 / 快速模式 - 其他会影响消息渲染或发送链的开关 遇到这种情况时,必须先确认用户实际使用的是哪种模式,再决定抓样本和写 adapter。 硬要求: - 不要默认把“无思考”样本当成“有思考”也可用 - 不要默认把“无搜索”样本当成“有搜索”也可用 - 不要只因为基础聊天能工作,就假设带模式开关时 DOM 结构不变 - 如果存在思考 / 深度思考模式,`extractResponse` 必须排除思考文本,只提取最终 assistant 正文和需要保留的协议块 - 如果页面存在思考组件,`decorateBubbles` 的目标选择器必须避开思考组件,不把思考组件纳入协议卡片渲染范围 默认策略: - 如果用户明确会使用思考模式,就优先在思考模式开启时调试 - 如果用户明确会使用搜索模式,就优先在搜索模式开启时调试 - 如果用户会同时使用多种模式,优先按“最复杂、最接近真实使用”的组合抓样本 - 如果用户不使用这些模式,可以按当前关闭状态调试 ## 硬规则 ### 1. 只接受新框架写法 - 不要接受手写 `renderProtocolCard`、`getProtocolCardTheme`、`detectToolResultTone` 这类旧 UI 辅助函数 - 不要接受旧版“自己拼 details 卡片”的 `decorateBubbles` - 不要接受 `continueConversation` 自己 `click()` / `dispatchEvent()` / `setTimeout()` - 不要接受硬编码 `[CHAT_PLUS_...]` 协议标记 - 不要接受缺 `meta`、缺 `adapterName`、缺 `capabilities` - 不要接受 `JSON.parse(bodyText)` 直接解析请求体,必须走 `ctx.helpers.json.parse(bodyText)` - 不要接受自己拼接注入文本,必须走 `ctx.helpers.buildInjectedText(...)` - 不要接受自己把 `ctx.bodyText` / `ctx.responseText` 转字符串,必须走 `ctx.helpers.text.toText(...)` - `meta.contractVersion` 必须是整数,按当前 Chat Plus 项目范式优先用 `2` - `meta.capabilities` 必须用对象写明四类能力,不要写成数组、字符串或空对象 ### 2. 四个 hook 的职责不能变 - `transformRequest(ctx)` 负责请求层注入。输入注入优先走协议层。请求可稳定改写时,必须用 `ctx.helpers.text.toText(ctx.bodyText)` 取文本、用 `ctx.helpers.json.parse(bodyText)` 解析 JSON、用 `ctx.helpers.buildInjectedText(...)` 生成注入文本。请求不可稳定改写时,必须保留 hook 并 `return null`。 - `extractResponse(ctx)` 负责从真实响应里提取 AI 正文。输出抓取必须走真实协议 / 真实响应。不要从页面现成消息 DOM 倒推正文,当成响应提取。必须保留协议块,不要把 `toolCall` / `toolResult` / `codeMode` 过滤掉。必须返回 `responseContentPreview`。不要随意 `slice(...)`。 - `decorateBubbles(ctx)` 负责隐藏用户注入块,把可见协议块变成统一卡片,并保留协议块之外的普通文本。必须直接走 `ctx.helpers.ui.decorateProtocolBubbles(...)`。 - `continueConversation(ctx)` 负责返回 DOM 发送方案。必须直接走 `ctx.helpers.plans.dom(...)`。必须使用 `ctx.continuationText`。不能自己执行真实 DOM 副作用。第一次 send 默认用 `mode: "click"`,只有确认站点必须 Enter 时才改。 ### 3. 协议块语义 - `toolCall` 默认只展示,不等于自动执行 - 真正可自动执行 / 手动执行的是 `codeMode` - `codeMode` 卡片的手动运行按钮、卡片容器、源码节点,统一由平台 helper 负责,不让站点脚本重复造轮子 ## 工作流程 ### 模式 A:生成或修复 Adapter 按下面顺序工作: 1. 确认目标站点 URL 2. 拿到真实请求样本 3. 拿到真实响应样本 4. 如果是流式响应,拿到完整流式样本 5. 拿到用户消息、AI 消息、输入框、发送按钮的 DOM 信息 6. 先判断 `transformRequest` 能不能稳定改写 7. 再判断 `extractResponse` 怎么提取正文和协议块 8. 再给出 `decorateBubbles` 9. 最后给出 `continueConversation` 10. 自检是否符合全部硬规则 如果样本不足,先指出缺什么,不要装作信息足够。 ### 模式 B:审查现有 Adapter 不要只看四个函数在不在。必须逐项检查: 1. `meta` 是否完整 2. `transformRequest` 是否使用 `ctx.helpers.buildInjectedText(...)` 或明确 `return null` 3. `extractResponse` 是否返回 `responseContentPreview` 4. `decorateBubbles` 是否直接使用 `ctx.helpers.ui.decorateProtocolBubbles(...)` 5. `continueConversation` 是否直接使用 `ctx.helpers.plans.dom(...)` 6. `continueConversation` 是否使用 `ctx.continuationText` 7. 是否还残留旧卡片函数或协议硬编码 输出顺序固定: 1. findings 2. 证据不足 / 假设 3. 总评 ## 抓样本时必须确认的内容 ### 请求侧 - 用户真实输入在哪个字段 - 如果是消息数组,最后一条 user 消息怎么定位 - 请求体是不是 JSON - 是否有签名、加密、opaque body,导致请求层不能稳定改写 - 如果请求层不能稳定改写,是否应改为 DOM 发送链兜底 ### 响应侧 - AI 正文在哪个字段 - 是否是 SSE / EventSource / WebSocket / 其他流式格式 - 协议块是混在正文里,还是单独字段 - 是否存在 answer / thinking / summary 多阶段 - 思考 / 搜索模式开启后,响应事件格式是否发生变化 - 思考字段、思考事件、reasoning delta、thinking block 必须和最终正文分开;不要把思考内容拼进 `responseContentPreview` - `codeMode` begin/end 是否能完整保留 - 输出是否能直接从真实响应事件 / 响应体提取,而不是依赖页面已渲染 DOM ### DOM 侧 - 用户消息容器 - AI 消息容器 - 思考区块 / 搜索区块 / 引用区块是否插入到 assistant turn 内部 - 思考区块选择器必须单独识别,并从 `assistantSelectors` / 协议卡片装饰范围中排除 - 输入框 - 发送按钮,或 Enter 发送目标 - 输入框赋值后是否需要 `input` / `change` ## 生成脚本时的强制要求 ### meta 必须包含 `contractVersion`、`adapterName`、`capabilities`。默认能力结构按真实样本填写,常见 JSON 请求体 + SSE 响应 + helper 卡片 + DOM 续写应写成: ```js meta: { contractVersion: 2, adapterName: "...", capabilities: { requestInjection: "json-body", responseExtraction: "sse", protocolCards: "helper", autoContinuation: "dom-plan", }, }, ``` 字段语义: - `requestInjection`: 请求层注入方式,例如 `"json-body"`;如果请求不可稳定改写,可写 `"none"`,但 `transformRequest` 必须明确 `return null` - `responseExtraction`: 真实响应提取方式,例如 `"sse"`、`"ndjson"`、`"json"`、`"websocket"`、`"grpc-web"`、`"dom-fallback"`;优先真实协议,不要默认 DOM - `protocolCards`: 必须是 `"helper"`,对应 `ctx.helpers.ui.decorateProtocolBubbles(...)` - `autoContinuation`: 必须是 `"dom-plan"`,对应 `ctx.helpers.plans.dom(...)` ### transformRequest - 请求可改时: - 这是输入侧的优先方案 - JSON 请求体必须用 `ctx.helpers.json.parse(bodyText)` 解析,不要写 `JSON.parse(bodyText)` - 请求文本必须用 `ctx.helpers.text.toText(ctx.bodyText)` 获取,不要自己转字符串 - 用 `ctx.helpers.buildInjectedText(...)` - 不要自己拼接注入字符串 - 返回 `applied / bodyText / requestMessagePath / requestMessagePreview` - 请求不可改时: - 常见原因包括签名、加密、protobuf/二进制、opaque body、不可稳定定位消息字段 - 保留 hook - 直接 `return null` - 后续输入改走 DOM 发送链,不要硬改协议 标准请求体读取和注入模板: ```js const bodyText = ctx.helpers.text.toText(ctx.bodyText); if (!ctx.injectionText || !bodyText) return null; const body = ctx.helpers.json.parse(bodyText); if (!body) return null; const originalText = slot.text; const nextText = ctx.helpers.buildInjectedText(ctx.injectionText, originalText, ctx.injectionMode); if (nextText === originalText) return null; slot.apply(nextText); ``` ### extractResponse - 默认返回完整提取文本 - 只能基于真实协议 / 真实响应提取 - 返回 `responseContentPreview` - 如果是 SSE / 流式响应,先按真实事件格式重组,再提取正文 - 如果响应里有 thinking / reasoning / thought / analysis 等思考字段或事件,必须过滤掉,只把最终 answer/content 文本放入 `responseContentPreview` - 不要把协议块截断 - 不要把页面消息气泡的 `innerText` / `textContent` 当作主提取方案 标准响应体读取模板: ```js const responseText = ctx.helpers.text.toText(ctx.responseText); if (!responseText) return null; ``` ### decorateBubbles 只能写成这种方向: ```js decorateBubbles(ctx) { return ctx.helpers.ui.decorateProtocolBubbles({ root: ctx.root || document, protocol: ctx.protocol, userSelectors: [...], assistantSelectors: [...], }); } ``` 允许传: - `normalizeUserText` - `normalizeAssistantText` - `beforeRenderUserNode` - `beforeRenderAssistantNode` - 用于排除思考组件的选择器 / 过滤逻辑 但不要自己重写整套卡片系统。 如果站点有思考组件: - `assistantSelectors` 应只命中最终回答消息区域,不要命中思考容器 - 如果思考组件嵌在 assistant turn 内部,必须用 helper 支持的过滤/normalize/beforeRender 钩子避开它 - 不要把思考文本当作普通 assistant 文本进行协议块扫描 - 不要给思考组件渲染协议卡片、代码卡片或工具卡片 禁止: - 自己操作 DOM 创建卡片 - 自己隐藏协议块 - 自己写 details / button / code card - 绕过 `ctx.helpers.ui.decorateProtocolBubbles(...)` ### continueConversation 只能写成这种方向: ```js continueConversation(ctx) { return ctx.helpers.plans.dom({ root: ctx.root, composerText: ctx.continuationText, input: { selectors: [...], kind: "textarea", dispatchEvents: ["input", "change"], }, send: { mode: "click", selectors: [...], waitForEnabled: true, maxWaitMs: 2000, }, }); } ``` 输入侧策略说明: - 优先协议注入 - 协议注入不可稳定实现时,才退回这个 DOM 发送方案 - 不要明明能稳定改请求,还默认只做 DOM 发送 - `input.kind` 必须按真实 DOM 确认后写 `"textarea"` 或 `"contenteditable"`,不要看见输入框就猜 - 第一次生成 `send` 时优先 `mode: "click"`,不要默认 Enter;只有真实站点要求 Enter 且有证据时才用键盘发送 禁止: - `btn.click()` - `dispatchEvent(...)` - `setTimeout(...)` - `return { dispatched: true }` - 不使用 `ctx.continuationText` ### 已踩坑写法纪律 这些不是风格偏好,是生成和审查 adapter 时必须拦截的错误: | 检查点 | 正确做法 | 错误做法 | | --- | --- | --- | | JSON 解析 | `ctx.helpers.json.parse(bodyText)` | `JSON.parse(bodyText)` | | 文本注入 | `ctx.helpers.buildInjectedText(...)` | 手写字符串拼接 | | 文本获取 | `ctx.helpers.text.toText(ctx.bodyText)` | `String(...)`、`.toString()`、手动解码 | | `decorateBubbles` | `ctx.helpers.ui.decorateProtocolBubbles({...})` | 自己操作 DOM 或重造卡片 | | `continueConversation` | `ctx.helpers.plans.dom({...})` | 自己 `click()` / `dispatchEvent()` | | `continueConversation.send` | 首次默认 `mode: "click"` | 没证据就默认 Enter | | 输入框 kind | 先确认 `"textarea"` 或 `"contenteditable"` | 对号入座、凭经验猜 | | 最外层 | `return { meta: {...}, transformRequest, extractResponse, decorateBubbles, continueConversation }` | 裸函数、对象字面量、漏 hook | | 协议注入 | JSON 可解析且能稳定定位消息时优先协议注入 | 能改请求却直接退 DOM | ## 自检清单 输出脚本前必须自己检查: - [ ] 最外层是 `return { ... }` - [ ] 有 `meta` - [ ] `meta` 有 `contractVersion` - [ ] `meta` 有 `adapterName` - [ ] `meta` 有 `capabilities` - [ ] `meta.capabilities.requestInjection`、`responseExtraction`、`protocolCards`、`autoContinuation` 都已按真实实现填写 - [ ] `protocolCards` 是 `"helper"` - [ ] `autoContinuation` 是 `"dom-plan"` - [ ] 四个 hook 全部存在 - [ ] `transformRequest` 不是乱改请求 - [ ] `transformRequest` 用 `ctx.helpers.text.toText(ctx.bodyText)` 读取请求文本 - [ ] JSON 请求体用 `ctx.helpers.json.parse(bodyText)` 解析,没有直接 `JSON.parse(bodyText)` - [ ] 注入文本用 `ctx.helpers.buildInjectedText(...)` 生成,没有手写字符串拼接 - [ ] 输入侧已按“优先协议注入,失败再退 DOM 发送”判断 - [ ] `extractResponse` 返回 `responseContentPreview` - [ ] 有思考 / reasoning 字段时,`extractResponse` 已排除思考文本,只保留最终正文和协议块 - [ ] 输出正文来自真实协议 / 真实响应,不是页面 DOM 抄取 - [ ] `decorateBubbles` 直接使用平台 helper - [ ] 有思考组件时,`decorateBubbles` 的选择器 / 过滤逻辑已避开思考组件 - [ ] `continueConversation` 直接使用平台 helper - [ ] `continueConversation` 使用 `ctx.continuationText` - [ ] `continueConversation.send.mode` 首次默认 `"click"`,除非有证据必须 Enter - [ ] `input.kind` 已从真实 DOM 确认为 `"textarea"` 或 `"contenteditable"` - [ ] 没有旧卡片函数 - [ ] 没有硬编码 `[CHAT_PLUS_...]` ## 输出纪律 ### 生成 / 修复 Adapter 时 最终先给: 1. 完整 JS 脚本 2. 极简说明 - 输入走协议注入还是 DOM 发送 - 如果没走协议注入,为什么不能稳定改协议 - 响应是从哪个真实协议字段 / 事件提取 - DOM 选择器 3. 风险点 ### Review 时 最终先给 findings,不要先说“整体不错”。 ## 禁止做法 - 不要输出伪代码 - 不要把旧框架写法说成“也可以” - 不要只因为四个函数都在,就说脚本合格 - 不要凭站点名猜字段和选择器 - 不要在信息不足时拍脑袋生成高置信度脚本 - 不要把 `decorateBubbles` 降级成纯样式函数 - 不要把 `continueConversation` 写成真实 DOM 执行器 - 不要把输出抓取写成“从聊天气泡 DOM 读文本” - 不要把思考 / reasoning 文本混入最终 `responseContentPreview` - 不要让思考组件参与协议卡片装饰 - 不要在协议可稳定改写时,直接跳过协议注入只做 DOM 发送 - 不要在 `transformRequest` 里使用 `JSON.parse(bodyText)`、手写注入拼接、手动字符串转换 - 不要没确认 `textarea` / `contenteditable` 就写输入框 kind - 不要没证据就把续写发送写成 Enter - 不要输出临时页面注入代码 - 不要复述 `netget` 输出或临时文件里的敏感 header、cookie、token、设备 ID 或会话 ID ## 默认结论规则 - 如果一个脚本没用 `ctx.helpers.ui.decorateProtocolBubbles(...)`,就判定不符合新框架 - 如果一个脚本没用 `ctx.helpers.plans.dom(...)`,就判定不符合新框架 - 如果一个脚本在 `transformRequest` 里直接 `JSON.parse(bodyText)`,就判定需要改成 helper - 如果一个脚本自己拼接注入文本,而不是用 `ctx.helpers.buildInjectedText(...)`,就判定需要修改 - 如果一个脚本没确认输入框 kind 或没证据就默认 Enter 发送,就判定存在续写风险 - 如果一个脚本缺 `meta`,就判定不符合新框架 - 如果一个脚本还带旧卡片函数,就判定需要重写 - 如果一个脚本的输出正文依赖页面消息 DOM 抓取,而不是协议 / 响应提取,就判定不符合框架预期 - 如果存在思考模式却把思考文本纳入 `responseContentPreview`,就判定响应提取错误 - 如果存在思考组件却把它纳入 `decorateBubbles` 的卡片渲染范围,就判定 DOM 选择器错误 - 如果请求体是可解析 JSON 且能稳定定位用户消息,却没有优先尝试协议注入,就判定不符合输入侧策略 - 如果请求不可改写,不是失败;只要 `transformRequest` 明确 `return null` 且 `continueConversation` 可用,就算符合框架 ## 配套工具 本 skill 只负责产出或修复 Chat Plus 适配脚本;脚本本身、临时样本、抓回来的 request/response 文件不要走 `shell_command` 的 `echo >` / `Set-Content` / here-doc / `cat` 再自己写文件。 - 落盘生成或修改 adapter 脚本一律走 `multi_edit_file`,让网关记录变更路径、执行 path guard、产出可审查的 diff。 - 查看样本、临时文件、`netget --request-file` / `--response-file` 产出的内容一律走 `read_file`,输出带行号、受 allowed directories 约束、二进制自动拒绝。 - 浏览器调试继续走 `chrome-cdp`,不要试图在 `shell_command` 里自己起 Chrome 或 node 调 CDP。