把 iOS 模拟器搬进对话里:DSH iOS 插件的设计取舍与实操笔记

在 DSH(DeepSeek Harness)的对话界面里直接操作一台 iPhone 模拟器,这件事在没见到之前听起来总觉得哪里不对:模拟器的画面怎么能渲染在浏览器里?点按是怎么从网页传递到模拟器进程的?真机也能这么干吗?更关键的是,这东西在真实工作流里到底能不能帮上忙,还是一个展示型的玩具?

把这套插件在 macOS 上完整跑了一遍之后,我想把它的真实工作方式、哪些地方设计得比较周全、哪些地方有明确的坑,都记录下来。这篇文章不是操作手册的复读,而是从“真正装过、配过、踩过”的角度,把它的核心机制和关键工具的用法讲清楚。

它解决的是什么场景的问题

先给一个具体的使用画面:你在 DSH 里和智能体对话,说“帮我在 iPhone 17 Pro 模拟器里打开设置,然后截个图”,右侧边栏立刻亮起一个实时画面,模拟器在启动、旋转、响应点按。你不需要在 Xcode 和浏览器之间来回切,也不需要手动操作模拟器再截图贴回对话里。智能体自己驱动设备,你看着画面确认结果。

这个场景能成立,需要同时解决几个问题:画面要实时推流到网页,点按要从网页反向传回模拟器,所有工具调用的结果要以卡片形式沉淀在对话里,方便回溯。如果只支持模拟器,这套东西的价值就砍半——真机能用同样的工具集操作,才是真正能用在测试和调试工作流里的关键。

整体架构:22 个工具、一个常驻面板、两种设备后端

插件向 DSH 注册了 22 个工具,覆盖设备管理、构建运行、UI 自动化、日志读取、调试和 SwiftUI 预览。所有工具只返回纯 JSON,视觉数据(截图、实时画面)不走工具返回值里的图片块,而是通过 DSH Web 服务器的签名路由渲染到侧边栏面板或卡片里。

面板叫“iOS 模拟器”,在对话右侧常驻,显示实时 MJPEG 画面。你可以在画面上直接点按、拖拽、旋转设备,工具栏提供 Home、截图、刷新和旋转按钮。尺寸有适应模式、百分比缩放(50%–125%)和 S/M/L 三档预设,边框支持无框、边框和真机框三种样式。面板宽度可以拖拽调整,上限 960px,双击恢复默认宽度;横屏时面板自动加宽,转回竖屏时恢复之前的宽度。

这个面板同时服务于模拟器和 USB 真机。区别只在于后端:模拟器用 serve-sim 推流,真机用 WebDriverAgent。面板的交互方式完全一致,你不需要区分当前画面来自哪种设备。

模拟器工具的核心链路

ios_sim_devices 是第一步。它列出这台 Mac 上所有可用的模拟器设备(udid、名称、运行时、状态),以及哪些已经启动;同时在 realDevices 字段里列出 USB 连接的真机 iPhone(udid、名称、osVersion、model、state、developerMode)。

选好设备后,ios_sim_boot 启动它并开始 serve-sim 推流。推流在对话期间保持存活,面板实时展示画面。serve-sim 只绑定 127.0.0.1 的专属端口段(3181–3244),不会动用户自己在 3100 端口上的 serve-sim,也不会用 --host 暴露到外网。

关机用 ios_sim_shutdown,传 udid,若推流目标正是该设备则同时停止推流。

截图用 ios_sim_screenshot,返回 JSON 摘要(路径、字节数、尺寸、设备),图片渲染在卡片或面板里,不以内联图片块返回。正在推流的模拟器和 USB 真机(经 WebDriverAgent)都可以截。

交互用 ios_sim_interact,在归一化坐标(0..1)上点按、输入文字、按下硬件按键(home、lock、volumeUp)、滚动或发送触摸手势。操作稳定后约 300 毫秒附带一张新截图展示效果。

构建与运行:模拟器和真机的不同路径

ios_sim_build_run 是另一个高频工具。给它一个 .xcodeproj.xcworkspace 或 Swift 包路径,它会构建、安装生成的 .app 并启动。模拟器 udid 走 xcodebuild + simctl,真机 udid 改为在手机上构建、安装并启动,需要 Apple Development 签名。

完整构建通常需要几分钟。构建失败时返回过滤后的 xcodebuild 报错尾部,不会把几千行日志全塞回来。

一个容易被忽略的点:如果你没有传 udid,插件会按“推流设备 → 已启动设备 → 最新运行时 iPhone”的顺序自动选一个,并自动启动。这意味着你可以在对话里只说“构建运行这个项目”,而不必每次都指定设备。

真机支持:WebDriverAgent 的启动与隧道转发

真机的支持方式是 ios_real_start_wda。它在 USB 连接的 iPhone 上启动 WebDriverAgent(WDA),把控制(REST)与画面(MJPEG)端口经回环隧道转发。设备必须处于解锁状态。

首次在真机上调用时,xcodebuild 会构建并启动 WDA,冷构建可能耗时数分钟。构建完成后,WDA 会在手机上运行,控制端口和 MJPEG 端口被转发到本地的回环地址,面板通过 DSH Web 服务器的签名路由拿到画面,工具通过同样的隧道发送控制指令。

如果 WDA 已经在响应,ios_real_start_wda 会直接接管,不重复构建。免费团队签名的描述文件 7 天后过期,需要重新运行。

调用失败时,面板状态里会给出编码原因:device-lockedcert-untrustedprofile-expiredtunnel-faileddevice-unplugged。这个设计比“WDA 启动失败”这种模糊信息有用得多。

UI 自动化:按身份点还是按文字点

插件提供了两套 UI 自动化路径,针对不同的 App 结构。

ios_sim_ui_tree 导出前台 App 的无障碍元素树(标签、标识符、取值、以点为单位的 frame)以及屏幕尺寸。模拟器走 AXe,真机走 WebDriverAgent。真机默认限制快照深度,因为不限深的快照在复杂 App 上实测约 32 秒 / 751 KB,限深后约 2 秒。输出上限约 40 KB,超出时裁掉最深层级并置 truncated 提示。

ios_sim_tap_element 按身份点击元素:先精确匹配,再做不区分大小写的子串匹配(identifier/label),嵌套重复元素折叠为同一个目标,若有多个不同元素匹配则逐一列出候选。点击落在元素中心,随后约 300 毫秒截一张效果图。传 expect_textexpect_gone 时点击与验证合并为一次往返。

这套流程对标准无障碍支持良好的 App 很顺畅。但对列表/信息流类 App(每条内容聚合在一个 Cell 里,标签包含“57 回复。18 喜欢。592 次查看”,没有可匹配的逐控件子按钮),需要另一套工具。

列表与信息流行:行级操作

ios_sim_ui_rows 把深层无障碍快照转成“行”而不是原始树。每一行包含索引、以点为单位的 frame、聚合标签,以及从标签里通用解析出的计数器(数字 + 分类词,如 57 回复 → 回复=57,不内置任何 App 词汇)。真机默认 max_depth 为 60,每次调用约 15–25 秒 / ~0.5 MB(WDA 串行处理请求)。屏幕外的行被排除并计入 omittedOffscreen

ios_sim_tap_row 在一条可见列表行内按相对位置点按(x/y 为该行 frame 的比例,0 = 左/上边缘,1 = 右/下边缘,默认 0.5 = 中心)。安全闸:传 expect_count={key,delta} 时工具重新读取行标签,校验计数器恰好变化 +1 或 -1;若键不在该行解析出的计数器里,点按在执行前被拒绝。不传 expect_count 时点按仍会执行,但无验证。

这个设计的价值在于:真机上的每一次点按都有真实后果,不能用“点一下试试看”的方式试探。expect_count 让智能体在点按前就有明确的验证预期,点按后确认操作生效。

OCR 兜底:无障碍树看不到的文字

当无障碍树为空或退化,文字以图形渲染(角标数字、嵌进图片的价格)时,用 Vision 做 OCR。

ios_sim_find_text 对当前屏幕做 OCR,返回 {device, size, items:[{text, confidence, rect}]}。识别模型是 zh-Hans + en-US,首次使用由 swiftc 编译到 ~/Library/Caches/dsh-ios/bin/ocr。输出上限约 40 KB,truncated 表示丢掉了置信度最低的尾部,可用 query 收窄或调高 min_confidence

ios_sim_tap_text 做 OCR 后点按最佳文字匹配的中心,规则与 tap_element 相同(先精确、再忽略大小写包含、多候选报歧义)。传 expect_text / expect_gone 则点按与验证合并为一次往返。

ios_sim_wait_for 复用 OCR 流水线轮询,等待某段文字出现或消失,默认超时 8 秒,上限 60 秒。超时返回 matched:false,不抛错。真机上每轮约 1.2 秒。

日志与调试工具

ios_sim_logs 从设备统一日志读取 App 运行时输出:snapshotlog show --last <duration>,默认 2m)或 follow(有界实时捕获,默认 10 秒,上限 60,不挂起不返回)。输出上限约 300 行 / 30 KB。

ios_sim_processes 列出运行中的 App 进程,模拟器走 launchd,真机走 devicectlios_sim_backtrace 批量 LLDB attach → thread backtrace → detach,输出上限约 200 行,主线程在前。当 macOS 拒绝 attach(开发者模式未开启)时,回退到 Xcode 的 sample 引擎(不挂起进程)。仅支持模拟器。

ios_sim_leaks 用 Xcode 的 leaks 工具分析泄漏:summary(泄漏数、泄漏总字节、前约 30 种泄漏类型)或 memgraph(生成 .memgraph 工件用 Instruments 打开)。扫描期间 App 被挂起,之后恢复。仅支持模拟器。在 iOS 26.2 运行时上,即使开发者模式已开启,leaks 也可能失败,工具降级输出原始诊断,目标进程必定恢复。遇到时试试 mode: "memgraph" 或换一个运行时。

ios_sim_app_info 读取已安装 App 的包路径、可写数据容器、Info.plist 关键字段。模拟器走 simctl appinfo(附 get_app_container 回退),真机走 devicectl。未安装时返回 installed: false,并在 note 中提示改用 ios_sim_list_apps

SwiftUI 预览热重载

ios_sim_preview 在模拟器里热重载 SwiftUI 预览。start 校验包、在插件缓存里生成一次性宿主 App(不写进你的包)、编译为模拟器 dylib、安装并启动宿主,然后监听源码。每次编辑重新构建并热替换,约 2–5 秒。编译错误不杀死会话,宿主保留最后一次成功的预览,错误尾部通过 status 返回。同一时间只能运行一个预览会话。

这个工具的价值在于:你修改 SwiftUI 代码后不用重启模拟器,甚至不用重新构建整个 App,预览宿主里直接热替换。对于快速迭代 UI 的工作流,节省的时间很明显。

安全设计:流量路径与令牌机制

浏览器永远不会直接接触 serve-sim 的端口。所有流量经由 DSH Web 服务器上的 /_dsh/dsh-ios/* 路由:/stream/<token>(MJPEG 代理)、/screenshot/<token>(缓存 PNG)、/ws?token=…(HID 控制转发),以及 /grant/capture/status 端点。

令牌是 HMAC-SHA256 能力凭证,10 分钟内过期,用每个 DSH 主目录私有的密钥签名。每条路由先检查回环/可信传输围栏:回环对端地址、回环 Host(拒绝 DNS 重绑定)、Fetch-Metadata/Origin 校验。截图路由只提供插件缓存目录内的文件,拒绝符号链接并做 realpath 包含性校验。

这个设计的实际效果是:即使有恶意页面尝试访问 /_dsh/dsh-ios/ 路由,缺少有效令牌和回环 Host 校验会被拒绝。限时令牌也意味着截图和推流 URL 不能无限期复用。

孤儿进程与保活策略

若上一个 DSH 宿主被异常杀死,其 serve-sim 子进程可能存活。孤儿进程被收养时,其握手信息视为权威;若残留进程占用槽位却服务着别的设备,通过 serve-sim -k 回收并重试一次。

推流崩溃后约 5 秒在后台自动重启。当没有消费者(面板关闭、没有挂载的卡片、没有活跃路由)时,空闲 5 分钟自动停止。主动停止不会被保活逻辑对抗。真机 runner 有意豁免空闲回收,因为重启它意味着一次数分钟的 xcodebuild 重新构建。

这个设计处理了“宿主崩溃后模拟器还在跑”的边缘情况,以及在对话结束后释放资源的策略。空闲回收在长对话里可能会让推流在你思考时停止,但下次工具调用或打开面板会重启,影响不大。

环境要求与常见安装问题

macOS 且需要完整版 Xcode,仅装 Command Line Tools 不够——xcodebuildxcrun simctl 和模拟器运行时都随 Xcode 提供。Xcode 里至少安装一个 iOS 模拟器运行时。DSH ≥ 0.1.0-rc.6 且使用 Web 版才能显示面板;headless 配置下 22 个工具照常工作但没有实时画面。

serve-sim 作为 npm 依赖随包安装。AXe 是可选的(ios_sim_ui_treeios_sim_tap_element、模拟器上的 ios_sim_ui_rows/ios_sim_tap_row 需要):brew install cameroncooke/axe/axe,或让插件自动下载固定版本到 ~/Library/Caches/dsh-ios/bin。Vision OCR 也是可选的(ios_sim_find_text/ios_sim_tap_text 需要):首次使用时 swiftc 编译内置的 assets/ocr.swift~/Library/Caches/dsh-ios/bin/ocr

lldb attach 需要 macOS 开发者模式:执行一次 sudo DevToolsSecurity -enable。在此之前 ios_sim_backtrace 改用 Xcode 的 sampleios_sim_leaks 降级运行并给出开启提示。

首次 WDA 构建会安装签名的 WebDriverAgentRunner,按提示在设备上信任证书。免费团队签名描述文件 7 天后过期,需重新运行 ios_real_start_wda

速览清单

  • 安装:dsh plugin --profile web add @zseven-w/dsh-ios@latest,然后 dsh web
  • 第一步:ios_sim_devices 查看可用模拟器和真机
  • 启动模拟器:ios_sim_boot + udid 或名称,面板自动打开
  • 构建运行项目:ios_sim_build_run + 项目路径,默认自动选择设备并启动
  • 驱动真机:先 ios_real_start_wda + 真机 udid,再使用其他工具
  • 按身份点 UI:ios_sim_ui_tree 看树 → ios_sim_tap_element 点元素
  • 列表/信息流操作:ios_sim_ui_rows 看行 → ios_sim_tap_row 点行内相对位置(建议带 expect_count 验证)
  • OCR 兜底:ios_sim_find_text 找文字 → ios_sim_tap_text 点文字
  • SwiftUI 预览:ios_sim_preview start + 包路径,修改源码后 2–5 秒热替换
  • 日志:ios_sim_logs snapshot/follow
  • 调试:ios_sim_processes 列进程 → ios_sim_backtrace 看调用栈 → ios_sim_leaks 查泄漏
  • 关闭:ios_sim_shutdown + udid

常见问题

ios_sim_ui_tree 报缺少 AXe?

brew install cameroncooke/axe/axe,或让插件首次使用时自动下载到 ~/Library/Caches/dsh-ios/bin。设置 DSH_IOS_AXE_BIN 可覆盖路径,DSH_IOS_AXE_OFFLINE=1 禁用下载。

真机上 ios_sim_interact 或截图没反应?

先检查 ios_real_start_wda 是否已成功启动。若 WDA 未运行,工具会返回明确错误提示。设备必须处于解锁状态,证书需在手机上信任。

ios_sim_ui_rows 找不到任何行?

结果会说明原因:深度太浅(真机上调大 max_depth,默认 60;每次更深快照约 15–25 秒)、不是列表页,或深度读取后确实没有无障碍信息。浅读不会报告成“该 App 没有无障碍信息”。

预览热重载编译失败会怎样?

宿主保留最后一次成功的预览,不会崩溃。错误尾部通过 status 返回。每次编辑重新构建,修复后热替换。

推流自己停了,是崩溃了吗?

大多数情况是空闲策略:没有消费者时 5 分钟自动停止。下一次工具调用或打开面板会重启。如果是崩溃,会在约 5 秒内自动重启。

真机上点按为什么这么慢?

WDA 串行处理请求,复杂 App 的 UI 树读取需要时间。ios_sim_interactios_sim_tap_element 在真机上每次点按后等待约 300 毫秒截图,加上 WDA 往返,总耗时通常 1–2 秒。这是 WDA 架构的限制,不是插件问题。