把 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-locked、cert-untrusted、profile-expired、tunnel-failed、device-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_text 或 expect_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 运行时输出:snapshot(log show --last <duration>,默认 2m)或 follow(有界实时捕获,默认 10 秒,上限 60,不挂起不返回)。输出上限约 300 行 / 30 KB。
ios_sim_processes 列出运行中的 App 进程,模拟器走 launchd,真机走 devicectl。ios_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 不够——xcodebuild、xcrun simctl 和模拟器运行时都随 Xcode 提供。Xcode 里至少安装一个 iOS 模拟器运行时。DSH ≥ 0.1.0-rc.6 且使用 Web 版才能显示面板;headless 配置下 22 个工具照常工作但没有实时画面。
serve-sim 作为 npm 依赖随包安装。AXe 是可选的(ios_sim_ui_tree、ios_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 的 sample,ios_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_logssnapshot/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_interact 和 ios_sim_tap_element 在真机上每次点按后等待约 300 毫秒截图,加上 WDA 往返,总耗时通常 1–2 秒。这是 WDA 架构的限制,不是插件问题。

