研究 · @full-self-browsing

PhantomStream.

DOM 原生的实时浏览器镜像。把真实标签页以结构化 DOM 流式传输,而不是像素。

PhantomStream 发送一个样式内联快照,然后发送由稳定节点 ID 寻址的微小 MutationObserver 差异。观看者只需屏幕流媒体带宽的一小部分即可获得实时的、语义可寻址的、可远程控制的页面副本。

npm install @full-self-browsing/phantom-stream
v0.9.9.1Plain JS · ESMNode >=18 MIT
概述

廉价、精确且可寻址的实时镜子

AI 代理驱动浏览器时,监管人员需要实时查看代理正在执行的操作。 PhantomStream 流式传输 DOM 本身 而不是像素。它捕获页面一次作为样式内联快照,在 WeakMap 中分配稳定标识符,然后使用 MutationObserver 监视页面并仅发送小的差异(addrmattrtext)由节点 ID 键入,并按页面自己的绘制节奏进行批处理。

回报是一面镜子,其带宽跟踪页面实际变化的程度,而不是帧速率。文本以任何分辨率原生呈现,远程控制通过稳定的 ID 定位真实元素,而不是猜测坐标。

PhantomStream 最初是 v0.9.9.1 "Phantom Stream" 里程碑的一部分,位于 FSB 内部,为仪表板提供自动浏览会话的实时预览。本仓库将其发展为独立的即插即用框架、可重新接入 SDKFSB,同时也是配套研究论文的工作仓库。

动机

像素问题

实时取景最明显的工具是视频。 WebRTCCDP 截屏视频或一系列屏幕截图均发送像素。 像素很重。无论页面是否更改,每一帧都会消耗带宽,编码和解码会增加延迟,结果是有损和分辨率限制。

更糟糕的是,像素是不透明的。观察者无法询问代理正在触摸哪个元素,无法突出显示节点,无法注释视图,并且无法可靠地驱动页面返回。对视频流的远程控制意味着在可能已经过时的帧上单击像素坐标,因此未对齐的单击会落在错误的位置上。

比较

为什么 DOM 流而不是视频

流式结构的 DOM 而不是框架改变了镜像在每个轴上的经济性和功能,这对监督代理很重要。

视频/截图
PhantomStream
带宽
连续帧,与页面内容无关
一张快照,然后仅在页面更改时出现微小差异
延迟
每帧编码、传输、然后解码
文本变更只是一个很小的 JSON 操作
富达
有损和分辨率限制
精确 DOM,本机文本渲染,与分辨率无关
远程控制
针对可能陈旧帧的像素坐标
由稳定节点 ID 寻址的真实元素
可检验性
不透明像素
镜像为DOM:可查询、可突出显示、可注释
建筑

四级管道

页面被捕获,主机包装每条消息以进行传输,中继将消息扇出给查看器,然后查看器重建并应用它们。远程控制以相反的方式运行相同的路径。

页面

快照:克隆、内联样式、标记节点 ID,然后 rAF 批处理差异

宿主

LZ-string 信封、会话标记、看门狗

继电器

WebSocket 扇出、1 MiB 帽、背压降

观众

沙盒 iframe、按 ID 应用差异、覆盖、缩放以适应

核心机制

稳定的节点身份

捕获在 WeakMap<Element, string> 中拥有的身份,并发出带有快照和添加操作的 nodeIds sidecar,因此后期差异始终会落在正确的元素上,而不会改变实时页面。

精心策划的计算式捕获

每个元素仅内联约 85 个影响视觉保真度的 CSS 属性,而非全部 300 多项,并省略默认值。这使 YouTube 的序列化时间从 45 秒缩短到足以实时交互的水平。

显示匹配的差异

突变在 requestAnimationFrame 上批量和刷新,因此镜像以页面绘制的相同节奏更新。

会话身份

每条消息都携带 streamSessionIdsnapshotId。查看器拒绝过时的消息,因此与前一页的最新差异永远不会损坏镜像。

能力

你从盒子里得到什么

捕获核心是普通的 JavaScript,它作为内容脚本、addInitScript 或书签注入,没有运行时构建步骤。通过一个小的传输接缝捕获、查看和转发所有通话。

稳定的节点ID

差异和远程控制操作通过 WeakMap 标识来寻址节点,而不是通过坐标或脆弱选择器。

精选样式内联

每个元素大约有 85 个保真度关键的 CSS 属性(默认省略),从而保持大量页面的交互性。

绘画节奏差异

每个实际更改一个紧凑的操作,以页面自己的更新速率刷新在 requestAnimationFrame 上。

预算快照

通过在整个元素边界上放置屏幕外子树,快照可以保持在中继的每条消息上限之下。

双看门狗

捕获端计时器和主机端警报通过强制刷新或刷新快照来独立恢复楔入流。

沙盒渲染

查看器在 iframe 沙盒内重建为 allow-same-origin(绝不是 allow-scripts),并使用 CSP 和解析后清理。

隐私屏蔽

blockSelectormaskTextSelector 和掩码函数会在敏感内容离开页面之前对其进行编辑。密码始终被屏蔽。

参考媒体

图像、视频和音频从观看者自己的浏览器中的源 URL 加载,因此媒体字节永远不会穿过中继。

Playwright 适配器

通过现成的适配器将捕获核心放入PlaywrightCDP页面中,并具有授权的反向远程控制。

安全性

嵌入安全合同

PhantomStream 呈现受攻击者影响的 HTML,并通过构造禁用脚本执行。序列化仅删除线路克隆上的危险内容。 直播页面从未被触及。

  • Sandbox 正是 allow-same-origin,从不 allow-scripts
  • on* 处理程序、危险 URL 方案、srcdoc,以及 object/embed 元素都会被移除
  • 传输前在捕获端屏蔽的私有文本和表单值
  • 失败关闭的媒体获取:仅 https、私有范围拒绝、无引用

沙箱保障

iframe sandbox = allow-same-origin,且仅允许此项
CSP 元标签与解析后清理
在传输副本的捕获端执行清理
在解析 srcdoc 前进行字符串层拦截
文档级 no-referrer 策略
快速入门

捕捉、镜像、中继

PhantomStream 每个阶段公开一个子路径。连接页面上下文中的捕获、远程上下文中的查看器以及 Node 上的中继,以在它们之间传递消息。

capture.js
import { createCapture } from '@full-self-browsing/phantom-stream/capture';
import { createWebSocketTransport } from '@full-self-browsing/phantom-stream/transport/websocket';

const transport = createWebSocketTransport({
  url: 'wss://relay.example.com/ws?room=ROOM&role=source',
  role: 'source'
});

const capture = createCapture({
  transport,
  skipElement: (el) => el.id === 'my-own-overlay' // 排除你自己的 UI
});

capture.start(); // 先生成一次快照,再流式传输差异
import { createViewer } from '@full-self-browsing/phantom-stream/renderer';
import { createWebSocketTransport } from '@full-self-browsing/phantom-stream/transport/websocket';

const transport = createWebSocketTransport({
  url: 'wss://relay.example.com/ws?room=ROOM&role=viewer',
  role: 'viewer'
});

const viewer = createViewer({
  container: document.getElementById('mirror'),
  transport
});

viewer.on('state', (e) => console.log('查看器状态为', e.state));
// connecting | live | stale | disconnected
import http from 'node:http';
import { createRelay, createWebSocketRelayBackend } from '@full-self-browsing/phantom-stream/relay';

const relay = createRelay();   // 每条消息上限 1 MiB,背压时丢弃
const server = http.createServer();
createWebSocketRelayBackend({ server, relay, path: '/ws' });
server.listen(8787);

// 客户端使用以下参数加入 ?room=<id>&role=source|viewer

或者直接从存储库运行它: npm run demo 将源选项卡镜像到查看器选项卡中,而 npm run demo:playwright 在查看器镜像页面时驱动页面。

参考

文档

更深层次的治疗与源头并存。每个文档都是独立的。