Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Entwicklung · SKILL.md

chat-plus-adapter-debugger

chat-plus-adapter-debugger

为 Chat Plus 新框架编写、修复、审查站点适配脚本。先读取完整 builtin://chat-plus-adapter-debugger/SKILL.md 获取 skillToken;这次读取 SKILL.md 不需要 skillToken。不要用正则或局部读取只抓 token。后续调用不带或带错 skillToken 会报错并必须重试,所以正式调试前先拿到正确 token。

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

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 发送
  1. 生成脚本时必须使用平台 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 调试端口。

常用别名:

capture start
capture clear
network search <filter>
network get <request-id>
network perf

等价 CDP 命令:

netclear
net <filter>
netget <request-id>
perfnet
html [target] [selector]
snap [target]
evalraw <target> <method> [json]

如果 chrome-cdp 启动或浏览器连接失败:

  1. 运行 stop
  2. 再运行 open <url> 或 launch <url>
  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 <request-id> 查看请求体、响应体、状态码、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 <url-fragment>、network search <api-path>、network search <method>、network search <mime> 或其它从候选里来的关键词
  • WebSocket 请求同样先用 network search <关键词> 定位,再用 network get <request-id> 读取 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 <filter> 搜索真实 request id
    • 用 network get <request-id> 补齐浏览器层原始请求体、响应体、状态码和 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 续写应写成:

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 发送链,不要硬改协议

标准请求体读取和注入模板:

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 当作主提取方案

标准响应体读取模板:

const responseText = ctx.helpers.text.toText(ctx.responseText);
if (!responseText) return null;

decorateBubbles

只能写成这种方向:

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

只能写成这种方向:

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()、手动解码
decorateBubblesctx.helpers.ui.decorateProtocolBubbles({...})自己操作 DOM 或重造卡片
continueConversationctx.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。
Gespeicherte Skills