Skip to main content
Glama

Daedalus

通过 Chrome 扩展程序实现的远程浏览器控制。Eval 桥接 + 持久热修复 + 按标签页控制 + 截图、CDP、Cookie、网络捕获,作用于 Chrome 运行该扩展程序的页面。

安装

安装前须知: 普通 eval 使用无横幅的 MAIN-world 注入。当无源代码的 CSP 探针无法确认动态编译可用时,Daedalus 会尝试 CDP 回退;在回退运行期间,一次成功的附加会让 Chrome 显示“Daedalus started debugging this browser”横幅。保持 CDP 会话或网络捕获会延长附加的持续时间。该横幅表示存在调试器附加,并不是值完整性的保证。

在 Chrome 中加载已解压的扩展(extension/):

  1. 访问 chrome://extensions

  2. 启用开发者模式

  3. 点击加载已解压的扩展程序,选择 extension/

首次安装时会自动生成唯一 token,并存储在 chrome.storage.local。可以通过扩展选项页(拼图图标 → Daedalus → Options)查看或修改。

如需多标签页并行抓取,请禁用 Chrome 的后台标签页节流:

chrome --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding

工作原理

  1. Token:安装时通过 crypto.randomUUID() 生成一次,并存储在 chrome.storage.local

  2. 标签页 ID:使用 Chrome 原生 tabs API——每个标签页由其 Chrome tabId 标识,在创建/更新时注册,并带 30 秒的 chrome.alarms 心跳

  3. 单条 SSE 流background.js 打开一条持久的 fetch SSE 连接(tab=extension),并将收到的命令分发到对应标签页

  4. 页面桥接content.js(ISOLATED world)在后台与 page.js(MAIN world)之间中继 window.GM 消息。eval 首先使用 chrome.scripting 进行 MAIN-world 注入。无源动态编译探针会把受 CSP 限制的页面导向 CDP;若在 CDP 处附加失败,则进入页面中继。每条通道都按页面 MAIN-world 语义执行。

  5. 热修复:存储在扩展级 chrome.storage.local 密钥 daedalus-hotfixes(并非按 token 存储),会在每个符合条件的顶层页面加载时重放,非永久修复受版本门控。轮换 token 既不能隔离也不会清除该存储。

发送命令

Daedalus 将其扩展命令接口作为 MCP 服务器公开在 <your-bridge>/mcp(streamable-HTTP 传输)。在分派请求之前,它要求 Bearer 值与通过 CLI 既有配置路径解析出的桥接 token 完全一致:TOKEN 会覆盖 DAEDALUS_TOKEN,也可以包含可选的 _settings 提供器。若未配置 token,MCP 接口会以 401 安全失败。将其添加到 Claude Code(或任何 MCP 客户端):

{
  "mcpServers": {
    "daedalus": {
      "url": "https://daedalus.example.com/mcp",
      "headers": { "Authorization": "Bearer <your-bridge-token>" }
    }
  }
}

桥接 token 就是扩展在安装时生成的那个 token,可在扩展选项页面(拼图图标 → Daedalus → Options)中查看。

该示例用公共主机名托管 MCP 服务器,但该传输默认只允许回环地址(127.0.0.1:*,localhost:*):必须在 DAEDALUS_MCP_ALLOWED_HOSTS 中指定该公共主机名,否则代理的请求都会被拒绝。所有三项 MCP 设置请参见下方“服务器”部分。

40 个工具分 7 组——tabs、eval/debug、media、cookies、CSS/blocking、hotfixes、network/CDP。完整列表见 CLAUDE.md<mcp> 部分,也可调用 MCP 端点上的 tools/list 查看。

手动验证辅助(无需 MCP 客户端):

TOKEN=<tok> python3 scripts/mcp_probe.py list
TOKEN=<tok> python3 scripts/mcp_probe.py call title '{"tab_id":"<tabId>"}'
TOKEN=<tok> python3 scripts/mcp_probe.py call screenshot '{"include_image":true}'

CLI

本仓库附带的 shell CLI 以 daedalus-cli wheel 发布(daedalus_cli/),安装后会提供 daedalus 命令。它从环境读取 DAEDALUS_URLDAEDALUS_TOKEN,并支持用 TOKEN 作为一次性覆盖,用 ID=<tabId> 来定向操作(省略则广播)。

DAEDALUS_TOKEN=<tok> daedalus tabs
DAEDALUS_TOKEN=<tok> ID=<tabId> daedalus title
DAEDALUS_TOKEN=<tok> daedalus exec myid 'document.title'

daedalus --help 列出所有子命令,每个子命令也有自己的 --help。你发送的 exec 代码是一个表达式或函数体,其返回值会作为结果返回——具体约定见上文“发送命令”。

还有一个可选导入接口:如果 sys.path 上存在可导入的名为 _settings 的模块,CLI 会改用该文件的 setting(name, default)required(name) 函数来获取 DAEDALUS_URLDAEDALUS_TOKEN,而不是用一个内置的环境变量回退。TOKEN 环境变量仍会作为一次性 token 覆盖并优先使用;没有 _settings 时,DAEDALUS_URL 使用默认值,DAEDALUS_TOKEN 则是必填项。ID 仍是用于按标签页定向的环境变量。

你也可以用原子方式发布一个原始命令文件(不经过 MCP,也不经 CLI)。不支持直接重定向到最终的 .json 文件名,因为流可能会写入工程结束前就观察到该文件。请先写一个以 .tmp 结尾的同名文件,然后在同一目录中重命名它:

# Broadcast to all tabs
commands_dir="$DAEDALUS_DIR/commands"
final="$commands_dir/<token>.json"
tmp="$(mktemp "$commands_dir/.<token>.XXXXXX.tmp")"
printf '%s\n' '{"id":"test1","code":"document.title"}' > "$tmp" &&
  mv "$tmp" "$final"

# Target a specific tab
final="$commands_dir/<token>_<tabId>.json"
tmp="$(mktemp "$commands_dir/.<token>_<tabId>.XXXXXX.tmp")"
printf '%s\n' '{"id":"test1","code":"document.title"}' > "$tmp" &&
  mv "$tmp" "$final"

读取方会忽略同级别存在的 .tmp 文件。如果较旧写入方在最终名称处留下格式错误的 JSON,读取方不会修改它并会进行重试,而不是删除一个可能仍在进行的写入。原子重命名之后,SE 流会交付并消费该命令。结果会落在 $DAEDALUS_DIR/results/<token>_<tabId>.json(按标签页)和 $DAEDALUS_DIR/results/<token>.json(last-writer-wins)中。

page-main 注入和排队mid-Level 的 eval 完成可携带 exec_ms 字段——即页面上下文的执行时间(毫秒)。页面可以在任一通道上伪造或省略这个页面计时字段;CDP 完成不携带 exec_ms。与排队交付相关的结果还会携带 roundtrip_ms——这是从命令读取(put / putCommand)到结果到达(result)所观察到的服务端完整往返耗时,单位毫秒。它包含队列等待 + SSE 交付 + 客户端中继 + 执行 + 返回路径,因此在两个字段都存在时,roundtrip_ms − exec_ms 近似于传输/队列开销。这些测量使用不同时钟:exec_ms 使用页面中的 performance.now(),而 roundtrip_ms 使用服务端墙钟毫秒,因此差异是近似差值,而不是精确相减。没有 _did 的遗留帧不会 roundtrip_ms

异步支持

默认的 page-main 通道通过页面自有的 eval 执行表达式、通过页面自有的 Function 执行函数体;一个异步包装器提供顶层 await。在提交源代码之前,后台脚本只注入一个常量 Function 探针。如果该探针返回 true,源注入只会尝试一次,而且每一个结果都是终态的:值、异常或注入中的传输错误都会被报告,而不会在 CDP 上重试该源代码。页面拥有 Function,并且可以影响这个路由提示,但探针不包含上传源代码源,因此另一条通道的选择不会导致其副作用被复制。

如果无源代码的探针未返回 true(最常见是因为页面 CSP 阻止了动态编译),Daedalus 会尝试 CDP。成功附加会显示 Chrome 的调试器横幅。Runtime.evaluate 使用 REPL 模式支持顶层 await;包含 return 的原文字会作为函数体处理,除非包装探针将解析为表达式。这个包装探针是口解析启发式,而不是不执行的安全边界:由 operator 构造的代码可以绕过它并执行。当最终的 CDP 求值下发后,每个结果都是终态。只有在提交的源码运行之前出现 attach 或 shape 失败,才进入页面中继;CDP Promise 的结算上限为 10 秒,即使会话被保留或连接仍未关闭,返回结果和异常对象中的句柄也会被释放。

在一个你不控制的页面中执行 JavaScript 时,无论由哪条通道执行,返回的值都由该页面决定。world 字段仅记录身份执行哪个执行通道运行了提交的源代码;这是面向 CSP 与调试器行为的诊断元数据,不是信任信号。它的 world 值在常规注入时是 page-main,检查器回退是 inspector 回退,最终中继则是 page:<hostname>,其中 <hostname> 是内容脚本的 location.hostname。后台程序会加上 page: 前缀,因此或其他 relay 主机名不会和 cdppage 混淆;但这一命名空间事实并不会对被 await 的值本身产生效用。CLI 以 channel=... 显示该字段,仪表板也同样如此;MCP 的 execputresultping 则保留该确切的 world 值。它们都不给这些值分配任何一种信任类别。

默认注入和 CDP 回退会保留已记录的非标准(sloppy-mode)经典脚本行为:with 可以编译,传统八进制字面量会被接受,未声明的赋值会创建全局变量。CDP REPL 模式还允许一个里运行作用域内重复声明 letconst。CLI 与 MCP 默认等待结果 15 秒;调用方超时并不会取消页面中已经运行的异步代码。Blob 中继器会把含 await 的代码单独等最多 10 秒,其他代码等 3 秒,然后才报告 fallback 超时。

Relay 相关性是描述性的且受边界约束,并受以下控制:来自发件标签页必须匹配,一条已接受的消息会按一次随机关系消耗关系 id,而在再次执行动作之前,在 probepre-dispatch CDP fallback 结束时源注入探针与 CDP 回退完成之前,不会注册任何 relay id。这些控制可防止跨标签页或重复 Relay 完成,但不会保证结果的完整性。

最多可有 1,000 个页面中继条目处于活动状态。容量已满时,新的回退会收到一个终态容量错误,但不会需要被驱逐的现有工作,而是会收到一条一边的容量失败;已有条目每 300,000 ms 过期一次,如果在同同样式下没有相同标签页完成或绑定,说明更早的发送失败并将其移除,它会收到一个终态超时错误。

Dashboard

浏览器端控制界面位于 <your-bridge>/dashboard,可驱动全部扩展命令,而无需 CLI——包括实时标签页列表、eval REPL、截图、Cookie、热修复、阻止规则、网络捕获、CDP、CSS 注入、fetch 计时和上传浏览器。

  1. 在扩展可控制的浏览器标签中打开该 URL。

  2. 滚动到 §12 Settings,并从扩展设置页(扩展图标 → Daedalus → Options)粘贴 token。保存。

  3. 当仪表板订阅到实时事件时,顶栏中的 SSE 状态点变为青色。

它直接由 server.py 从仓库的 dashboard/ 目录提供(原生 JS + ES 模块,不经历构建)。实时更新通过已有 /stream 端点完成:当 /register 更新了一个现有 tab 时,以及在 /sync-tabs/unregister/result 的成功路径中,server.py 都会将事件扩展到 add 到 commands/<token>_dashboard/<ts>_<uuid>.json/unregister 即使没有 tab 存在也会发出事件。Dashboard 以 kind:'event' 帧的形式消耗这些事件。

**注意:**扩展的 content + page 脚本会插入匹配的页面,包括 Dashboard 所在的标签页。广播 eval 命令(exec -b)也会在 Security 中运行——因此推荐使用定向标签页,或在广播我们执行的破坏性代码前关闭 Dashboard。

如打补丁

热修复

持久保存小补丁,并在每个符合条件的顶层页面加载时重放。通过 MCP 工具完成:store_hotfixlist_hotfixesclear_hotfixclear_hotfixesset_permanent。示例(通过探针脚本):

TOKEN=<tok> python3 scripts/mcp_probe.py call store_hotfix '{"fix_id":"my-fix","code":"console.log(\"patched\")"}'
TOKEN=<tok> python3 scripts/mcp_probe.py call store_hotfix '{"fix_id":"always-on","code":"console.log(\"baseline\")","permanent":true}'
TOKEN=<tok> python3 scripts/mcp_probe.py call set_permanent '{"fix_id":"my-fix","permanent":true}'
TOKEN=<tok> python3 scripts/mcp_probe.py call list_hotfixes
TOKEN=<tok> python3 scripts/mcp_probe.py call clear_hotfix '{"fix_id":"my-fix"}'
TOKEN=<tok> python3 scripts/mcp_probe.py call clear_hotfixes
TOKEN=<tok> python3 scripts/mcp_probe.py call clear_hotfixes '{"include_permanent":true}'

热修复默认受版本门控。在扩展版本更改后,保留的非永久修复会被跳过,而不是删除;保存一个修复会将记录更新到当前版本,并让其保留的非永久修复重新可用而后。将修复标记为永久(通过 store_hotfixpermanent: trueset_permanent),可让它跨版本再次重放。clear_hotfixes 默认会移除全部非永久修复并保留永久修复;传入 include_permanent: true 可删除整个存储。

扩展命令

background service worker 接受类型化命令(通过 MCP 工具或写入带有 "type": "..." 的 JSON 到 $DAEDALUS_DIR/commands/):

命令

用途

screenshot

将当前可见标签页捕获为 PNG

cdp

发出原始 Chrome DevTools Protocol 调用

net-capture / net-capture-stop / net-capture-get

通过 CDP 完整拦截请求/响应

cookies / set-cookie / remove-cookie / clear-cookies

访问 Cookie 存储

open-tab / open-tabs / focus-tab / close-tab / navigate / reload

标签页控制

inject-css / remove-css

按标签页注入 CSS

block-requests / unblock-requests / list-block-rules

使用 declarativeNetRequest 阻止请求

store-hotfix / clear-hotfix / clear-all-hotfixes / list-hotfixes / set-permanent

热修复管理(永久修复在版本升级后仍然保留)

ext-reload

从磁盘重新加载扩展本身

fetch-timings

fetch 中继的诊断环形缓冲区

GM 桥接

window.GM(位于 page.js 的 MAIN world)提供以下 Tampermonkey 风格的子集:

方法

说明

GM.getValue(key, default)

从扩展级别的 chrome.storage.local 读取非保留字符串键(不是按 token 隔离)

GM.setValue(key, value)

向扩展级别的存储写入非保留字符串键

GM.deleteValue(key)

从扩展级别的存储中删除非保留字符串键

GM.listValues()

列出非保留存储键

GM.xmlhttpRequest(opts)

通过后台转发的 HTTP 请求(不受 CSP 防护限制)

GM.addStyle(css)

注入 CSS

GM.setClipboard(text, type)

写入剪贴板

GM.notification(opts)

桌面通知

GM.openInTab(url, opts)

打开新标签页

GM.download(opts)

触发下载

GM.info

脚本元数据

Cookie 访问属于操作员能力,而非页面能力:它通过上面需要令牌认证的 cookies / set-cookie / remove-cookie / clear-cookies 命令进行,并且刻意不暴露给页面上下文——page.js 在每个匹配的顶层页面中运行,否则它就能读取其自身 document.cookie 无法看到的 Cookie。

架构

Browser (matching tab)                           Server (your bridge host)
┌────────────────────────────────────────────┐   ┌──────────────────────┐
│ MAIN world                                 │   │ bridge (server.py)   │
│  ├─ page-main: default injection channel   │   │ /stream?token        │
│  └─ page.js: GM + relay channel            │   │ watches commands/    │
│            ▲                               │   │                      │
│            │ window.postMessage            │   │ /result writes       │
│ content.js (ISOLATED)                      │   │    results/          │
│            ▲                               │   │                      │
│            │ chrome.runtime                │   │                      │
│ background.js (service worker)             │   │                      │
│  ├─ CDP: CSP fallback channel              │   │                      │
│  ├─ single SSE stream ◄────────────────────┼───┤                      │
│  └─ POST result, fetch ────────────────────┼──►│                      │
└────────────────────────────────────────────┘   └──────────────────────┘
  • 单条 SSE 流:后台打开一条使用 fetch 的 SSE 连接(tab=extension),并通过 chrome.tabs.sendMessage 将命令路由到目标标签页。30 秒看门狗会在流停滞时强制重连。

  • 按标签页路由:命令指定特定的 Chrome tabId,或向所有标签页广播。

  • CSP 行为GM.xmlhttpRequest 会把实际的 HTTP 请求工作发送到后台 service worker 的 fetcheval 通常使用无条码、无牌子提示的 MAIN world 注入。当无源码探测报告动态编译不可用时,CDP 提供 CSP 回退;若附加成功会像那个页面显示 Chrome 的调试器横幅;附加失败则转到页面/本地 Blob 中继。最终的 world 值只描述这个通道,并不做出任何完整性声明。

服务端

用三个必填设置的桥接启动——不提供 DAEDALUS_DIRDAEDALUS_PORT 时,进程在启动时退出,并且每个桥接控制路由在没有配置令牌时都会默认拒绝服务:

DAEDALUS_DIR=<data-dir> DAEDALUS_PORT=<port> DAEDALUS_TOKEN=<bridge-token> python3 server.py

server.py 在你使用的任何 supervisor 下运行(这里通常是 systemd)。如果你要把它暴露在 localhost 之外,就在前面放置一层结束时完成 TLS 终止的反向代理;桥接本身只使用普通 HTTP。桥接控制路由和存储路由会比较各自带上来的令牌与 CLI 配置路径解析出的某个密钥:TOKEN 是一时覆盖,否则必须提供 DAEDALUS_TOKEN(嵌入式的 _settings 模块也可以提供它)。配置缺失或不匹配都会默认拒绝。只有面向页面的 POST /segmentGET /segment-status 路由改用任务作用域的 capability。

进程内 MCP 前端需要三个字节设置,这里放在一起说明,因为它们描述的是同一个监听器:DAEDALUS_MCP_PORT(默认 8086)是它绑定到的回环端口;其工具处理器会使用桥接的实际绑定回环 URL,即使当 DAEDALUS_PORT=0 时;DAEDALUS_LOCAL_URL 会显式覆盖该 URL,用于前端连接到运行在其他地方的桥接的独立 MCP 部署;而 DAEDALUS_MCP_ALLOWED_HOSTS(默认 127.0.0.1:*,localhost:*)是其 DNS-rebinding 保护所接受的以文件号分隔的主机白名单,所以如果你使用公共主机名公开 /mcp,就需要在那里列出这个主机名。

端点列表:GET /stream, GET /tabs, GET /health, GET /dashboard[/<asset>], POST /register, POST /sync-tabs, POST /unregister, POST /poll, POST /result, PUT /command, GET /result, POST/GET/DELETE /upload, GET /screenshot, POST /segment-job, POST /segment + GET /segment-statusPOST /segment-job 需要配置好的桥接令牌;只有 POST /segmentGET /segment-status 使用任务作用域的 sig。具体请求体和端点说明参见 CLAUDE.md

POST /poll 在旧式广播状态文件存在时,会把它读取并删除。

PUT /command 将命令放入按目标区分的 FIFO 目录队列(同一标签页连续发送命令不再互相覆盖),并给每个命令一个投递 id(_did),这样扩展可以对重发的命令去重;超过 DAEDALUS_CMD_TTL 秒(默认 90)仍未被领取的命令会被 TTL 丢弃。即使从未有 SSE 消费者连接,后台收集也会应用该 TTL 并删除空的队列目录。GET /health 报告流/注册表/最近一次投递的状态,用来检测桥接是否已经悄悄“死亡”。

桥接出于对不够稳定的考虑,会拒绝重复的权限字段,而不是选择其中一个值:比如查询字符串或 JSON 正文中的重复 token,以及 segment-capability 路由上重复出现的 job / sig,即使重复且值相同或空白。MCP 传输同样拒绝重复的 AuthorizationMcp-Session-Id,、HostOrigin 请求头,也拒绝 segment 工具位置出现重复的 job 参数。

当扩展发布一个结果时,服务端会把该命令的 _dt 作为 deliveryId 暴露出去,并为该结果分配一个新的 resultGeneration。CLI 和 MCP 等待者先查看共享结果槽,同时匹配命令 id 和 deliveryId,然后再用 GET /result?...&consume=1&expected=<resultGeneration> 条件化地消费那个世代。如果在这个过程期间有另一个结果替换了槽,那么条件化消费会保留新结果,并报告上一个结果没有被消费。没有 expected 的裸 consume=1,则仍然是对当前结果槽的一次破坏性兼容读取。

GET /stream 会无限期保持 SSE 连接,通过在 DAEDALUS_STREAM_KEEPALIVE 秒(默认 15)发送一次 keepalive 注释来证明存活,而不是按定时器断开重连;DAEDALUS_STREAM_MAX_AGE(默认 3600)只是最后的保护上限。关闭路径由 tests/test_stream_lifecycle.py 固定。

DAEDALUS_MAX_BODY_SIZE(默认 64 * 1024 * 1024 字节,即 64 MiB)会限制 POSTPUTDELETE 处理器读取请求体的大小;显式声明比上限大的请求体会收到 413。当需要中继更大的分段或包含其他负载时,提高该值。

基于文件系统的调用方值使用一套路径组件策略:它拒绝 ..、C0/C1 控制字符和代理项字符、Windows 不合法的路径字符和设备名、结尾的点或空格,以及长度超过 240 字节的 UTF-8 编码。桥接令牌更严格,同时拒绝点号和无图标。其他 UTF-8 任务名则是允许的,所以客户端必须在查询字符串中进行 URL 编码。

POST /segment-job 会把三个已固定的配额复制到每条新任务记录中:DAEDALUS_MAX_SEGMENT_INDEX(默认 99999)、DAEDALUS_MAX_SEGMENTS_PER_JOB(默认 10000)和 DAEDALUS_MAX_SEGMENT_JOB_SIZE(默认 4 * 1024 * 1024 * 1024 字节,即 4 GiB)。更改这些设置只会影响后续创建的任务;已经保存了配额的任务继续使用记录中的值。DAEDALUS_MAX_BODY_SIZE 同时限制单个分段请求的大小。

安全

安装此扩展前请先阅读本节。

它会向每个匹配的顶层页面注入一个 window.GM shim<all_urls>,MAIN world),这就是 PUT 发送的脚本能够发起跨源请求的原因。因此,任何匹配你访问过的页面都可以调用这个 shim,所以该站点可以通过扩展的权限发起跨源请求,这和带通配符 @match 的用户脚本管理器一样。如果你不能接受这一点,请把 extension/manifest.json 中的 matches 收窄到你实际驱动的那些主机,并且接受这样会让桥接在其他页面中不再起作用。

桥接令牌和服务端 URL 不能通过页面的 GM 存储方法访问到。 它们存放在 chrome.storage.local 中带前导 daedalus- 的键下,中继会拒绝页面脚本读取、写入这一命名空间。没有这个规则,任何访问过的站点都可以用 GM.getValue('daedalus-token') 读取你的桥接令牌,或者用 GM.setValue('daedalus-server', ...) 安静地把桥接重定向到其它位置。tests/test_repo_contract.py 用于固定此规则。

热修复的来源是刻意以页面方式送达的,不被视为扩展的机密状态。content.js 读取 daedalus-hotfixes 记录,选择永久性和匹配当前扩展版本的非永久性修补验证值,然后把这些选中的完整对象发布到每个可用的页面中。MASIN 世界的 page.js 随后执行每个对象的 code,因此页面能够观察到这种由页面送达的源代码。

MCP Bearer 不能为 MCP 进程读取服务端主机上任意指定的文件。 MCP 的 put 命令、CSS 注入/移除以及热门修复存储工具只接受内联的源代码。本地 CLI 保留文件路径这种便利方式,但由 CLI 进程读取操作者选定的文件,然后将其内容内联提交。因此,持有桥接令牌只授予文档所述的浏览器和扩展控制能力(包括读取已存热修复源的来源),但不会因此允许通过这些工具任意读取主机文件系统。

DAEDALUS_DIR=<data-dir> DAEDALUS_PORT=<port> DAEDALUS_TOKEN=<bridge-token> python3 server.py

(补充的 GXP8 小节不需要翻译,按原样保留)| 命令 | 用途 | | ------------------------------------------------------------------------------------------ | --------------------------------------------------------- | | screenshot | 将当前可见标签页捕获为 PNG | | cdp | 发出原始 Chrome DevTools 协议调用 | | net-capture / net-capture-stop / net-capture-get | 通过 CDP 完整捕获请求/响应交互 | | cookies / set-cookie / remove-cookie / clear-cookies | 访问 cookie 仓库 | | open-tab / open-tabs / focus-tab / close-tab / navigate / reload | 标签页控制 | | inject-css / remove-css | 按标签页注入 CSS | | block-requests / unblock-requests / list-block-rules | 使用 declarativeNetRequest 阻止请求 | | store-hotfix / clear-hotfix / clear-all-hotfixes / list-hotfixes / set-permanent | 热修复管理(永久修复在版本升级后仍保留) | | ext-reload | 从磁盘重新加载扩展本身 | | fetch-timings | 用于 fetch 中继的诊断换回 |

GM 桥

window.GM(属于 page.js 的 MAIN world)提供以下 Tampermonkey 风格子集:

方法

说明

GM.getValue(key, default)

从扩展级 chrome.storage.local 读取非保留字符串键(不是每个 token)

GM.setValue(key, value)

向扩展级存储写入非保留字符串键

GM.deleteValue(key)

从扩展级存储中删除非保留字符串键

GM.listValues()

列出非保留存储键

GM.xmlhttpRequest(opts)

通过后台转发的 HTTP 请求(不受 CSP 约束)

GM.addStyle(css)

注入 CSS

GM.setClipboard(text, type)

写入剪贴板

GM.notification(opts)

桌面通知

GM.openInTab(url, opts)

打开新标签页

GM.download(opts)

触发下载

GM.info

脚本元数据

Cookie 访问是一种操作员能力,而不是页面能力:它通过上面 token 认证的 cookies / set-cookie / remove-cookie / clear-cookies 命令进行,并刻意不暴露给页面上下文——page.js 在每个匹配的顶级页面中运行,否则它可以读取自身 document.cookie 无法看到的 cookie。

架构

Browser (matching tab)                           Server (your bridge host)
┌────────────────────────────────────────────┐   ┌──────────────────────┐
│ MAIN world                                 │   │ bridge (server.py)   │
│  ├─ page-main: default injection channel   │   │ /stream?token        │
│  └─ page.js: GM + relay channel            │   │ watches commands/    │
│            ▲                               │   │                      │
│            │ window.postMessage            │   │ /result writes       │
│ content.js (ISOLATED)                      │   │    results/          │
│            ▲                               │   │                      │
│            │ chrome.runtime                │   │                      │
│ background.js (service worker)             │   │                      │
│  ├─ CDP: CSP fallback channel              │   │                      │
│  ├─ single SSE stream ◄────────────────────┼───┤                      │
│  └─ POST result, fetch ────────────────────┼──►│                      │
└────────────────────────────────────────────┘   └──────────────────────┘
  • 单条 SSE 流:后台打开一个 fetch SSE 连接(tab=extension),通过 chrome.tabs.sendMessage 将命令路由到指定标签页。30 秒看门狗强制在流停滞时重连。

  • 逐标签页路由:命令针对特定 Chrome tabId,或向所有标签页广播。

  • CSP 行为GM.xmlhttpRequest 将实际的 HTTP 请求发送到后台 service worker 的 fetch。Eval 通常使用无标签、无横幅的 MAIN-world 注入。当无源探测报告动态编译不可用时,CDP 提供 CSP 回退并在附加成功时向页面显示 Chrome 调试器横幅;附加失败回到页面/Blob 中继。最终的 world 值描述该通道,并且不做完整性保证。

服务端

使用三个必需设置启动桥接——如果没有 DAEDALUS_DIRDAEDALUS_PORT,它会在启动时退出,且所有桥接控制路由在未配置 token 时都会关闭失败:

DAEDALUS_DIR=<data-dir> DAEDALUS_PORT=<port> DAEDALUS_TOKEN=<bridge-token> python3 server.py

server.py 在你使用的任何 supervisor 下运行(这里是一个 systemd 单元)。如果要将桥接暴露到 localhost 以外,请在其前面放一个 TLS 终止的反向代理;桥接本身只提供原版 HTTP。桥接控制和存储路由用于处理其 token 和一个已配置的密钥(通过 CLI 配置路径解决):TOKEN 是一次性覆盖,否则需要 DAEDALUS_TOKEN(并且附加 _settings 模块可以提供它)。缺少配置和不匹配都会安全失败。只有面向页面的 POST /segmentGET /segment-status 路由才使用 job 作用域的 capability。

进程内 MCP 服务端有三个可选设置,放在这里一起说明,因为它们描述同一个监听器:DAEDALUS_MCP_PORT 的默认值是 8086,它绑定的回环端口;它的工具处理器使用桥接实际绑定的回环 URL,包括当 DAEDALUS_PORT=0 时;DAEDALUS_LOCAL_URL 用于暴露出一个单独运行的桥接的独立 MCP;DAEDALUS_MCP_ALLOWED_HOSTS(默认 127.0.0.1:*,localhost:*)是逗号分隔的主机允许列表,其 DNS 重新绑定保护会接受它声明的主机名,因此,如果用公共主机名代理 /mcp,需要在这里命名该主机名。

端点:GET /streamGET /tabsGET /healthGET /dashboard[/<asset>]POST /registerPOST /sync-tabsPOST /unregisterPOST /pollPOST /resultPUT /commandGET /resultPOST/GET/DELETE /uploadGET /screenshotPOST /segment-jobPOST /segment + GET /segment-statusPOST /segment-job 需要配置的桥接 token;只有 POST /segmentGET /segment-status 使用一个 job 作用域的 sig。有关 payload 和端点说明,请见 CLAUDE.md

POST /poll 会消费并在存在旧版广播命令文件时删除。

PUT /command 进入每个目标 FIFO 目录队列(每个标签页大致相同的命令不再互相覆盖),并给每个命令添加 _did,以便扩展对重发的帧去重,并在 DAEDALUS_CMD_TTL 秒(默认 90 秒)后取消未认领的命令。后台收集器会在没有任何 SSE 消费者连接时应用该 TTL 并删除空队列目录。GET /health 报告流/注册表/最后一项命令的活性,用于检测静默死掉的桥接。

桥接拒绝重复的授权方信息,而不是选择一个值:这包括字符串 / JSON 体中的 token,segment-capability 路由上的 job / sig,即使重复值是相等或空的。MCP 传输同样拒绝重复的 AuthorizationMcp-Session-IdHostOrigin 头,也拒绝 segment 工具中的重复 agent

当扩展提交结果时,服务端将命令的 _did 作为 deliveryId 公开,并分配一个新的 resultGeneration。CLI 和 MCP 等待者首先查看共享结果槽,同时匹配命令 id 和 deliveryId,然后用 GET /result?...&consume=1&expected=<resultGeneration> 条件地消费该结果。如果另一个结果覆盖了槽,条件消耗不会只消耗。不带 expected 的裸 consume=1 仍然是对当前槽的一次破坏性兼容读取。

GET /stream 一直保持 SSE 连接,通过 DAEDALUS_STREAM_KEEPALIVE 秒(默认 15)间隔 keepalive 注释来证明活性,而不是用定时器循环连接;DAEDALUS_STREAM_MAX_AGE(默认 3600)只作为最后保底 max。关闭路径由 tests/test_stream_lifecycle.py 锚定。

DAEDALUS_MAX_BODY_SIZE(默认 64 * 1024 * 1024 字节,64 MiB)限制 POST 读取的大小、PUTDELETE;如果声明的 body 大于限制,则返回 413。在转发更大的分段或其他 payload 时增加该值。

基于文件系统的调用值使用一种路径组件策略:它拒绝 ..、C0/C1 控制字符和代理字符、Windows 无效路径字符和设备名、尾部点号或空格,以及超过 240 字节的 UTF-8 编码。桥接 token 更严格,并且也拒绝点号或无用信息。其他 UTF-8 job 名称接受接受,因此客户端必须对查询字符串中的它们进行 URL 编码。

POST /segment-job 将三个固定配额放入每条新 job 记录:DAEDALUS_MAX_SEGMENT_INDEX(默认 99999)、DAEDALUS_MAX_SEGMENTS_PER_JOB(默认 10000)和 DAEDALUS_MAX_SEGMENT_JOB_SIZE(默认 4 * 1024 * 1024 * 1024 字节,4 GiB)。更改这些设置只影响后续创建的 jobs;已有存储值的 job 会继续使用记录中的值。每个单独的分段请求也受 DAEDALUS_MAX_BODY_SIZE 限制。

安全性

安装此扩展前请阅读此项。

它会向每个匹配的顶级页面注入 window.GM shim<all_urls>,MAIN world),这让通过 put 发送的脚本能够发起跨源请求。结果是需要刻意为之的部分:任何你访问的匹配的站点都可以调用该 shim,因此 该站点可以通过扩展的权限发起跨源请求,正是带通配符 @match 的 userscript manager 那样。如果你觉得不能接受,把 extension/manifest.json 中的 matches 收窄到你实际驱动的主机,并接受桥接在其他页面上不做任何事。

桥接 token 和服务端 URL 不能通过页面的 GM 存储方法访问。 它们只存在于 chrome.storage.local 中带 daedalus- 前缀的键上,并且拒绝桥接侧脚本的读取存储。没有这条规则,任何访问过的站点都可以用 GM.getValue('daedalus-token') 读取桥接 token,或用 GM.setValue('daedalus-server', ...) 静默。tests/test_repo_contract.py 固定了此规则。

hotfix source 是刻意页面传递的状态,不是扩展的机密状态。content.js 读取 daedalus-hotfixes 记录,选择永久修复,以及非永久修复中匹配扩展版本的部分,将所选完整对象发布到每个符合条件的页面。MAIN-world 的 page.js 随后执行每个对象的 code,因此页面可以观察该页面传递的 source。

MCP bearer 不能声明 servers-host 的文件让 MCP 进程读取。 MCP 的 put、CSS 注入/移除和 hotfix 存储工具只接受内联 source。本地 CLI 保留了文件路径的便利,但 CLI 进程读取该操作者选择的文件并提交内联内容。因此,持有桥接 token 可以获得文档中的浏览器和扩展控制权,但并不会通过这些工具获得任意主机文件系统上的读权限。

桥接控制与存储路由都需要已配置的桥接令牌;/segment/segment-status 是仅有的能力例外。 服务器会把请求里的令牌与从 TOKENDAEDALUS_TOKEN 解析出的密钥进行比较,并且在没有配置密钥时拒绝请求。POST /segment-job 需要该桥令牌,因为它要铸造作业作用域的能力;不受信任的页面 JavaScript 只能用该能力来投递和查询那个作业。任何持有桥令牌的人都可以操纵你的浏览器。除非有终止 TLS 的反向代理,否则不要将桥接端口暴露到回环地址之外,并且要把令牌当作凭据对待。

Eval 结果不提供值完整性保证。 在你无法控制的页面中执行的 JavaScript,其返回值可由该页面任意选择,无论它由哪一条通道执行。world 字段说明的是所提交源码如何运行,而不是其值是否可信:page-main 是普通的 MAIN 世界注入,cdp 是 inspector 的 CSP 回退,page:<hostname> 则是中继。强制性的 page: 前缀是在页面上下文之外添加的,所以任何主机名都不可能产出 cdppage-hint;这种抗冲突性只是描述性的。在 page-fault 上,页面拥有的 evalFunction 绑定既能读取提交的源码,也能影响它返回的值。

CDP 编译会避免解析页面的 evalFunction 绑定,实现在序列化之前按引用取得直接句柄。这些传输机制不会改变信任边界:提交的源码仍然可以读取页面控制的状态,并且可以在 CDP 接收之前,借助页面的 promise 机制把任意值(包括原始值)绕出来。只要提交的源码使用了这些页面控制的路径,页面也可以决定 cdp 返回值。

中继只接受与随机 id 关联的那个已存储调用,要求发送方的标签页与本页面匹配,并且条目只消耗一次。降级不会泄露桥令牌、服务器 URL、投递 ID 或结果路由,不会授予额外的扩展或浏览器权限,也不会影响其他标签页中的调用。这些属性保护路由与浏览器权限;然而,它们并不会把中继标记或其返回的 JavaScript 值变成可信信号。

部署

桥接器在回环地址上以纯 HTTP 工作。桥接控制与存储路由需要已配置的令牌,未配置时全部拒绝;只有 /segment

/segment-status 使用作业作用域的能力。它故意不去做两件事,因为这两件事属于放在它前面的那一层:

  • TLS 与 CORS。 server.py 不发送任何 CORS 头。当桥接器跨域时,示例 HLS 中继的页内 fetch 调用需要同时访问 GET /segment-statusPOST /segment;代理必须在两个路由中都允许页面源,并处理 POST 预检所要求的方法/头。如果 status GET 被阻止、不可用、非 2xx 或无效,示例就会把作业当作全新作业,并对每一个 segment 重新 POST。这些写入仍然会替换同一组按索引文件,但这次运行会失去 resume/skip 的节省。如果部署无法满足该 CORS 策略,就把两个请求改从扩展的 GM 桥发送。

  • 无法交付存储的上传。仪表盘在 /uploads/<path> 链接下载,但桥接器没有这个路由。不要在仪表盘源上直接提供调用者提供的上传的名字与字节,那会把可执行内容与携带令牌的仪表盘存储共享到同一源。要么让那些链接不可用,要么让 /uploads/ 重定向到独立的、只负责下载的源,Content-Disposition: attachmentapplication/octet-stream,并附带 X-Content-Type-Options: nosniff 响应头。GET /uploads(列表)与 GET /screenshot 由桥接器自身提供,不需要代理代劳。

直接运行它并在 server 前放置一个 Token,以上两点都不存在——其他所有功能照常。

Files

File

Description

extension/manifest.json

MV3 清单

extension/background.js

Service worker——SSE、命令分发、fetch 中继、截图、CDP、cookie、下载

extension/content.js

页面与后台之间的消息中继

extension/page.js

MAIN-world 桥:window.GM 加上 eval 处理程序

extension/options.html / options.js

Token/服务器设置界面

server.py

调试服务器(同时托管 127.0.0.1:8086 上的 MCP 守护进程)

mcp_server.py

MCP 服务器:把扩展命令接 HTTP 接入 server.py

scripts/mcp_probe.py

供手动验证的最小 MCP 客户端助手

scripts/check_versions.py

每个版本位点的版本一致性检查 / 提示

.githooks/

pre-commit 与 pre-push 的版本一致性检查口

daedalus_cli/

daedalus-cli wheel 发布的命令行

dashboard/

page_? da 浏览器控制界面,由 server.py/dashboard 挂载

examples/

put 在页面里运行的脚本——见下方说明

tests/run_tests.py

测试集;python3 run_tests.py 运行所有测试

示例

examples/ 存放使用 put 在页面内运行的脚本。其中五个展示的是桥接器的某个方面,而不是某个特定站点;Discord 那个则特意做成站点特定的,因为回滚虚拟化列表这种技术,没有真实虚拟化列表就没法演示:

示例

演示内容

request-log-hotfix.js

挂到 document_start 的永久 MAIN world 热修复,以及为什么这三个词都起着承重作用

hls-segment-relay.js

/segment / /segment-status 中继:可恢复、幂等、有界重试

kill-hls.js

找到并销毁一个即使如此仍不停重试的播放器实例

react-fill-input.js

填入 React 受控的 input,从而真正改变 React 自己的状态

scrape-discord-messages.js

在虚拟化消息列表中回滚并提取

open-tabs.js

用最简单的 GM.openInTab 调用

这些脚本通过 __PLACEHOLDER__ 替换来接收配置,因为 put 发送的只是一个脚本,无法传递参数。“”它们都是 async 函数的函数体,而不是独立脚本;桥接器会把你发送的内容包起来。这也解释了为什么大多数都以顶层的 return 结尾,其中 scrape-discord-messages.js 还用了顶层 await——这在真正运行的环境中都是合法的。node --check 会用 CommonJS 包装方式解析每个文件,它可以接受顶层 return,但会拒绝顶层 await——所以恰好是那一个文件不能通过语法检查。

开发

版本字符串散落在扩展、dashboard 和 CLI 包里多个位置。python3 scripts/check_versions.py 就是那把列表——它会列出每个版本点,并报告一共找到多少,因此这一段就不重复数字,免得它在下一次加版本之后立刻过时。每次都要一起把版本升上去,不要手工改:

python3 scripts/check_versions.py --set 0.18.0   # rewrite every site
python3 scripts/check_versions.py                # verify the working tree

.githooks/pre-commit 检查暂存区,.githooks/pre-push 检查每个被推送的提交,因此半提版本永远无法落地。每一个 master 都要一次性 opt-in——否则 git 不会执行被跟踪目录里以及钩子方针执行的钩子:

git config core.hooksPath .githooks

如果你用的浏览器是从不同于你编辑的 checkout 里加载扩展,那就得也在那边安装这些钩子——并记住,重新加载扩展会重新从浏览器的那一侧重新读取文件,而不是从你编辑好的那一侧。

-
license - not tested
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • Hosted real Google Chrome MCP with per-user persistent state. Navigate, click, type, screenshot.

  • Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.

  • Access Kernel's cloud-based browsers and app actions via MCP (remote HTTP + OAuth).

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Nitjsefnie-Harness-Commons/daedalus'

If you have feedback or need assistance with the MCP directory API, please join our Discord server