T3 Code 非官方中文翻译不是官方产品,也不是 T3 Tools 官方文档。源提交 d2c25950b4d9英文原文GitHub 源文件
Skip to content

设备 ​

环境(environment)服务器拥有模拟器和仿真器的方式,和它拥有终端一样:发现、串流和智能体访问都在那里运行,每一个客户端(client)都通过环境连接到达它们。因此设备面板可以经过 Tailscale 和 T3 Connect 工作,包括设备跑在 SSH 主机上的时候。

两个外部工具,一条接缝 ​

expo-device-hub 负责串流,agent-device 负责操控。每一个都在对应的设备面板同意步骤之后,以固定版本用 npm 安装进 T3 主目录。手动设置只安装并启动 expo-device-hub;在授予智能体访问之前,agent-device 保持不存在,并且处于停止状态。两者都用服务器的 Node 运行;npx 会让重启之后的第一次 device_open 取决于注册表。中心是受监督的子进程,而不是导入进来的中间件,因为 serve-sim 会通过原生附加模块加载私有的 CoreSimulator 框架,那里一旦崩溃,不能把服务器一起拖垮。

所有与平台相关的部分都放在 DeviceHost 后面。服务通过主机的设备操作来列出、启动和关闭设备;代理和 MCP 工具只看得到一个中心 origin 和一个 agent-device 端点。运行 expo-device-hub 的主机,共用同一套由中心支撑的这些操作实现,因此设备位于别处的主机,只要替换这些操作即可。SSH 主机把两个端点都转发到服务器的回环地址。每一个被代理的请求还会带上主机 id;只靠设备 id,跨主机时并不唯一。

EAS Simulator 主机是计费中的会话 ​

EAS 主机 背后没有一台机器:每打开一台设备,就是一次 EAS Simulator 会话,在它停止之前一直向 Expo 账户计费。每次会话创建时都带上该主机的运行上限和空闲上限,主机在超过会话上限后拒绝再启动,关闭主机会停掉它的会话。它启动的会话记在状态目录下;如果这台服务器挂了,由下一台服务器把它们停掉。同一账户上由其他工具启动的会话,绝不会被碰到。

每次会话都自带自己的预览主机(serve-sim,Android 则是 serve-emu)和 agent-device 守护进程,以及各自的令牌。代理让主机为每个请求做路由(DeviceHostReady.upstream),并在服务器侧附上会话令牌,因此客户端(client)从不持有这个令牌。智能体的配置文件按设备写入(DeviceOperations.agentEndpoint),因此 device_open 会先打开设备,再指定它为目标。设备设置和工具要在设备所在机器上运行 simctl 或 adb。EAS 并不暴露这台机器,所以这两项在那里不可用。

Simlock 主机经由 agent-device 前端租用 ​

Simlock 主机(alpha)打开设备时,是用 T3 自己的 agent-device CLI 去访问网关的 agent-device 前端(先 connect proxy,再 open),而不是走 Simlock 的租约 HTTP API:agent-device 无法采纳一份不是它自己分配的租约。这份租约绑定在那一个 agent-device 会话上,也绑定在 open 当时所在的目录上,因此 agentEndpoint 交给每一个对话线程(thread)的是同一个会话、一份保存着连接记录的 stateDir,并且不带 --udid/--serial(网关拒绝选择器)。只有从那个目录安装才有效;device_open 标出这个目录。

Simlock 按 serve-sim 的格式,把一份租约的画面和输入提供给持有者,两个平台都如此:/v1/leases/<id>/stream 上是 AVCC 信封,/v1/leases/<id>/input 上是 serve-sim 的输入消息,两者都带智能体令牌。因此客户端把 Simlock 的 Android 租约当作 iOS 处理(串流客户端中的 serveSimOnly),upstream 把 serve-sim 的 stream.avcc 和 helper/ws 路径映射到这些路由。Simlock 没有 MJPEG、截图或主机命令这些路由,所以面板在那里只提供输入。租约在被跟踪时会续期,并在到达主机的寿命上限时释放;崩溃的服务器留下的租约,会在一个 TTL 之后自行结束,所以没有账本。

中心绝不对外暴露 ​

serve-sim 有一条 shell-exec 路由,令牌可以从它自己那条未经认证的 /api 读出来,而 serve-emu 的动作路由则完全没有认证。中心绑定在回环地址上,唯一的入口是代理。代理用允许列表放行串流、配置和截图路由,并把每个请求认证为一次环境会话。<img> 和 WebSocket 无法携带标头,所以代理按 /ws 升级的方式来认证:使用 cookie,或者使用一张短时的 wsTicket,由 bearer 客户端和 DPoP 客户端在已认证的 HTTP 上签发。请求到达中心之前,这张票据会被去掉。

串流响应带有 Cache-Control: no-transform;不然压缩中间件会去缓冲一份永远不会结束的 MJPEG 消息体。浏览器要等到下一段开始,才会画出当前这段 MJPEG,而中心只在画面变化时才写入一段,所以代理会在一段短暂的静默之后重发上一段。浏览器开发时,Vite 代理必须把 /api 的 WebSocket 升级也转发出去,而不只是 /ws。

设备设置绝不经过中心 ​

serve-sim 的预览靠同一条 exec 通道发送 shell 命令(command),来驱动它的工具面板。把这条通道代理出去,哪怕加了允许列表,也会让任意一次环境会话都能在主机上执行任意命令,所以 T3 不这样做。device.action 这条 RPC 自行运行底层的 simctl、adb 和 serve-sim 辅助二进制,经由 DeviceHostReady.run 调用,每个控件一个类型化动作,并把读回来的设置返回。代理允许列表只随着读取路由增加(无障碍树、前台应用、事件(event)日志),并且除截图捕获和串流调优之外,所有地方都拒绝非 GET 方法。

智能体通过 CLI 操控 ​

device_* 工具包有意只设四个工具:列出、打开、截图、关闭。操控通过 agent-device CLI 完成。它具备智能体需要的语义快照模型,并且随自己的版本发布保持同步。T3 会在提供方(provider)的 PATH 最前面加上一个垫片目录。即使环境服务器自己跑不了模拟器,CLI 也安装在环境服务器上。主机按需启动。

提供方子进程一拉起,这个环境就固定了,所以 prepareMcpSession 只在同时满足这些条件时才启动 agent-device:设备支持和智能体访问都已启用,这次会话(session)具备 device 能力,并且这台机器至少能运行一个平台。如果改到以后再从 device_open 启动,已经在运行的智能体就得不到这个 CLI。

如何操控设备由 device_open 返回,而不是放进一份始终加载的提示或 skill:从不打开设备的对话线程(thread)不会因此增加开销,内容也不会偏离被固定的 CLI 版本。device_open 返回一个绑定到该 agent-device 会话的启动器,智能体因此不需要目标标志。启动器会略去一条命令会拒绝的那些标志(cdp 联系的是 Metro,不是设备),拒绝会换掉 T3 连接的命令,并先检查这条连接:agent-device 把沙箱中的 shell 和已经掉线的隧道都报告成不可达的守护进程,所以启动器会点明究竟是哪一种。结束一次会话(device_close、设备面板,或一次关闭)也会关闭它的 agent-device 会话,从而退出该会话打开的应用,并释放 CLI 对这台设备的占用。

智能体在打开任何东西之前,要在这些工具和 ssh 之间选择,所以持有 device 的会话会通过自己适配器(adapter)的指令通道,从 orchestrationInstructions.ts 得到一小段文字。这段文字把本地主机和 SSH 主机上的模拟器与仿真器界面工作指向这些工具;工具覆盖不到的部分,仍可以使用 simctl、adb 和 ssh。Pi 和 Muse 目前还不会发送这段文字。Claude 的原生子智能体看不到这次会话的系统提示,所以用一个 SubagentStart 钩子把这段文字传给它们。运行时会在那里执行钩子,而子智能体的上下文并没有被隔离。

查看器同时解码两种随附协议 ​

中心随附两台线路格式不同的串流服务器。iOS 视频是一段由 AVCC 信封组成、用 WebCodecs 解码的 HTTP 消息体,输入走另一条二进制 WebSocket;Android 在同一条 WebSocket 上复用 SEMU 分帧的 H.264 和 JSON 手势。DeviceStreamView.tsx 同时处理这两种格式,因此一个面板覆盖两个平台。

WebCodecs 只能在安全上下文中使用,所以查看器会探测 isConfigSupported,解码不可用时在 iOS 上回退到 MJPEG。Android 的 SEMU 套接字没有 MJPEG;这种情况下面板会报告无法解码。

serve-sim 也可以被锁定为 WebRTC,EAS Simulator 就是如此,这时它会用一个写明传输方式的 409 拒绝 AVCC。查看器仍然先请求 AVCC,因此本身就提供 AVCC 的中心没有额外开销,并在遇到这个 409 时切换,或者当它的 MJPEG 图像在具备 RTCPeerConnection 的浏览器中失败时切换。Offer 不采用涓流:serve-sim 一次接收包含全部候选的一个 offer。video 元素只把画面送进同一个 canvas 汇,因此面板和移动端 WebView 共用同一个画面表面。Android 在打开视频套接字之前先查询 serve-emu 的 /api,若对方通告了 WebRTC(EAS Simulator)就用 WebRTC 串流,同时为输入保留单独的 video=0 套接字,使输入可以重连,而不必重启视频。

非官方译文 · 源提交 d2c25950b4d9 · 不代表 T3 Tools