ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

gstack ios-qa:用视觉驱动 Agent 循环在真机上测试 iOS 应用的完整机制

gstack ios-qa:用视觉驱动 Agent 循环在真机上测试 iOS 应用的完整机制 gstack ios-qa用视觉驱动 Agent 循环在真机上测试 iOS 应用的完整机制【免费下载链接】gstackUse Garry Tans exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack本篇基于 ios-qa/SKILL.md 展开讲清 gstack 的 ios-qa 技能如何在真实 iPhone 上执行 QA通过 USB CoreDevice IPv6 隧道连接设备读取 Swift 源码生成类型化状态访问器部署仅 Debug 配置生效的 DebugBridge再运行截图 → 分析 → 决策 → 操作 → 验证的闭环循环读完后你将掌握这套免模拟器、免 XCTest、免 WebDriverAgent 的真机测试链路的架构、四个 Phase 的操作流程、Tailnet 远程控制的权限分层以及各故障码的恢复手段。要解决的问题ios-qa 技能直接驱动一台通过 USB 连接的真实 iPhone而不是模拟器。Agent 会先读取你的 Swift 源码理解每一屏的语义生成类型化状态访问器部署一个调试桥DebugBridge然后进入 find→fix→verify 闭环。技能文档开头明确界定了它的三条不依赖No simulator, no XCTest, no WebDriverAgent。触发方式上frontmatter 声明了ios qa、test the iphone app、find bugs on the device、qa the ios app等自然语言触发词允许的工具集为 Bash、Read、Write、Edit、Grep、Glob、AskUserQuestion。整体架构双监听器 Daemon Loopback-only StateServer技能文档给出的架构如下┌──────────────────────┐ USB CoreDevice (IPv6) ┌──────────────────┐ │ gstack-ios-qa daemon │ ────────────────────────▶ │ iOS app │ │ (Mac, bun/TS) │ bearer X-Session-Id │ StateServer │ │ │ │ (loopback only) │ │ - boot token rotate │ │ - /tap /swipe │ │ - session minting │ │ - /type /state │ │ - audit redact │ │ - /snapshot │ └──────────────────────┘ └──────────────────┘ ▲ │ Tailscale (optional, --tailnet) │ ┌──────────────────────┐ │ Remote agent │ │ (OpenClaw, etc.) │ └──────────────────────┘三层信任边界的划分是整个设计的核心iOS 应用内的 StateServer 永远只绑 loopback::1127.0.0.1。从 StateServer.swift.template 可以看到它同时持有 IPv6 与 IPv4 两个NWListener注释说明单监听器的 IPv6-only 绑定曾在评审中被判定为不完整且文件整体包在#if DEBUG里。Tailnet 入站流量完全是 Mac 侧 daemon 的职责iOS 应用本身不感知 Tailscale。Mac daemon 负责身份校验与令牌铸造通过本地tailscaledsocket 的 WhoIs 端点规范化 Tailscale 身份再为远程 Agent 铸造短命会话令牌默认 1 小时。daemon 本体是 ios-qa/daemon/src/index.ts一个用 bun/TypeScript 写的进程启动流程在源码中清晰可查单实例强制index.ts通过tryClaim对~/.gstack/ios-qa-daemon.pid取排他 flock若已有 daemon 存活新调用直接打印READY: portexisting pidpid并退出调用方转而连接已存在的端口。这就是技能文档中Daemon acquires an exclusive flock … If another daemon is alive, the second invocation discovers its port and connects的底层实现。Loopback 监听器全功能面先绑127.0.0.1再在同一端口尝试绑::1index.ts。loopback 不做鉴权因为绑定本身即边界。Tailnet 监听器可选fail-closed只有同时满足用户传了--tailnet且tailscaled LocalAPI socket 探测成功两个条件才会打开socket 缺失、权限拒绝或 WhoIs 响应不可解析时daemon 打印tailnet binding refused但 loopback 侧照常运行index.ts。READY 行协议daemon 就绪后向 stdout 输出READY: portport pidpid由调用方spawnAndWaitReady解析。CLI 入口支持GSTACK_IOS_DAEMON_PORT默认 9099、GSTACK_IOS_TARGET_UDID等环境变量index.ts。一个值得注意的运维细节Xcode 26 的 CoreDevice 只在 devicectl 命令执行期间维持 IPv6 隧道因此 daemon 在成功 bootstrap 后会周期性调用devicectl info details来戳隧道保活index.ts 中startTunnelKeepalive。前置条件技能文档列出的硬性前提macOSdaemon 依赖 Xcode 的devicectliPhone 通过 USB 连接、已配对且已信任安装 Xcode Swift 工具链swift --version 5.9应用源码在磁盘上且至少有一个Observable类远程控制模式需要已安装 Tailscale 且用户已登录。Phase 0会话热启动可选如果~/.gstack/ios-qa-session.json存在且设备仍连接可跳过 Phase 1-2 直接进 Phase 3。会话缓存保存了轮换后的令牌、UDID、隧道地址和 accessor hash。三种情况下缓存失效用户传--cold强制完整 bootstrap首次 state 查询时检测到 accessor hash 不匹配daemon 报告缓存的 UDID 已不在线。文档给出的探测脚本节选SESSION$HOME/.gstack/ios-qa-session.json if [ -f $SESSION ] [ $COLD ! 1 ]; then CACHED_UDID$(python3 -c import json,os; djson.load(open(os.path.expanduser($SESSION))); print(d[udid])) CACHED_PORT$(python3 -c import json,os; djson.load(open(os.path.expanduser($SESSION))); print(d[daemon_port])) if curl -sf http://127.0.0.1:$CACHED_PORT/healthz /dev/null; then echo Warm start: daemon alive, device $CACHED_UDID connected fi fi热启动成立的依据是 daemon 的/healthz端点在 loopback 侧公开可用index.ts 返回{ version, mode: loopback }。Phase 1读源码规划代码生成这一步决定 DebugBridge 能否被安全地接入你的工程技能文档给出了明确的兼容性红线生成器目前只支持文件作用域的Observable类ObservableObject、StateObject等其他观察模型不会产生访问器。依赖接入假设的是 SwiftPM 工程清单。对于.xcodeproj/.xcworkspace不得臆造 package 或 target 接线。任一条件不满足时停止 bridge bootstrap 且不修改应用保留已安装的 Production/TestFlight 构建优先复用已有的真机 XCUITest harness确需独立 QA 构建时使用隔离的 bundle identifier 与非生产 entitlement使 QA 构建可与生产应用共存。源码扫描阶段Agent 遍历--source dir下的应用源码找出所有Observable类并记录紧跟生成器标记注释// Snapshotable的属性——这些是快照候选字段。标记用注释表达是为了与Observable宏组合使用。每个被标记字段必须满足属于文件作用域 observable 类、是可写的实例var、有显式类型、setter 为 internal 或 public。快照类型限于 JSON 原生标量String、Bool、各整型宽度、Float、Double、CGFloat、数组、String 键字典及其 Optional 组合且 key 必须在所有 observable 类之间唯一。任何约束被违反时代码生成以源码诊断信息停止而不是产出破损或有损的 harness——这在 gen-accessors.ts 中对应AccessorGenerationError会把每条诊断计算属性/不可变/不可访问/无类型/嵌套/重复 key/非 JSON 类型逐条列出。生成器有两套实现SwiftPM 版本gen-accessors-tool/Sources/GenAccessors/main.swift基于 swift-syntax首次构建需 2-5 分钟和 TS 快速路径gen-accessors.ts后者用轻量 Swift 词法扫描器识别Observable类声明、// Snapshotable标记以及存量集成的遗留Snapshotable属性、多行类型签名和 JSON 原生泛型。缓存 key 是swift_version || tool_git_rev || platform_triple || source_content_hash的复合值只按源码内容哈希会漏掉生成器自身逻辑变更。扫描时还会显式排除DebugBridgeGenerated子树与StateAccessor.swift生成文件防止生成物污染下一次缓存 key。最后向用户展示访问器清单并用一次 AskUserQuestion 询问是否把 DebugBridge SPM 依赖装入Package.swift。Phase 2引导设备桥一条确定性命令生成桥~/.claude/skills/gstack/bin/gstack-ios-qa-regen \ --app-source source-dir \ --bridge-dir source-dir/DebugBridge该命令一次生成规范本地桥包、类型化访问器和已安装版本标记。regen 还会清除旧版 ios-sync 创建的过期扁平文件集防止陈旧的第二套 harness 残留在 app target 里。将生成的DebugBridge本地 SPM 依赖加入Package.swift。从 Package.swift.template 可以看到包提供三个仅 Debug 配置的库产物DebugBridgeCoreSwift跨平台StateServer 桥协议DebugBridgeTouchObjective-C仅 iOSKIF 衍生的进程内触摸合成iOS 18 用_UIHitTestContext完成 SwiftUI 命中测试DebugBridgeUISwift仅 iOSScreenshot / Elements / Mutation 桥实现。app target 以.when(configuration: .debug)依赖DebugBridgeUI传递引入 Core Touch。模板注释里还记录了一次真实事故的防护曾有 Release 构建把DebugBridgeTouch.m链接进可发布二进制nm -j检出 15 个 DebugBridge 符号与IOHIDEventCreateDigitizer字符串构成 App Store 指南 2.5.1 风险。因此防护分两层——所有源文件#if DEBUG守卫真正生效的那层 消费方的 configuration 条件并配有 CI 不变式swift build -c release后nm -j build/Release/binary | grep -q DebugBridge exit 1。在mainApp init 中接线#if DEBUG门控#if DEBUG import DebugBridgeCore #if canImport(UIKit) import DebugBridgeUI // Install resolvers before StateServer opens its listener. DebugBridgeUIWiring.installAll() #endif // Replace AppState/AppStateAccessor with the type discovered in Phase 1. DebugBridgeManager.shared.start( appState: appState, register: AppStateAccessor.register ) #endif对应的模板文件是 DebugBridgeWiring.swift.template 与 DebugBridgeManager.swift.template。构建并部署到设备xcodebuild -scheme SchemeName -destination platformiOS,idUDID build install。启动应用devicectl device process launch --device UDID --console bundle-id从os_log捕获首次运行打印的 boot token。按需拉起 Mac 侧 daemongstack-ios-qa-daemon。令牌轮换daemon 立即向 iOS StateServer 发POST /auth/rotate换成全新的仅存内存的令牌boot token 约 5 秒后作废——此后任何再抓取os_log或磁盘 token 文件的行为拿到的都是死凭证。第 7 步的完整流程在 tunnel-bootstrap.ts 中实现为一个六步编排用devicectl list devices找到配对设备内置设备排序USB 直连优先其次已建隧道优先、启动应用已在运行则幂等跳过、解析隧道 IPv6先devicectl info details再 mDNSdns.lookup最后dns.resolve6兜底、从应用沙盒tmp/gstack-ios-qa.token读取 boot token、必要时重启一次应用再取令牌、最后POST /auth/rotate完成轮换。其中重启一次的触发条件值得注意如果前一个 daemon 已经消费了那个一次性 boot token新 daemon 无法恢复已轮换的内存 bearer就--terminate-existing重启应用让 StateServer 重新铸造一枚tunnel-bootstrap.ts。轮换时新令牌是 32 字节base64url随机值tunnel-bootstrap.ts。StateServer 侧的实现细节StateServer.swift.templateboot token 初始为UUID().uuidString同时写入NSTemporaryDirectory()下权限0600的文件作为 os_log 抓取的兜底服务器还维护一个 5 分钟 TTL 的会话锁sliding window on mutations only以及供代码生成器注册的读/写 handler 表和 restore 校验钩子——restore 是两阶段校验任何模型校验失败时不允许部分 restore 生效schema 不匹配会返回schemaMismatch(expected, got)这正是后文409 schema_mismatch故障的来源。Phase 3视觉驱动 Agent 循环每次迭代固定八步GET /screenshot经 daemon 代理→ 保存 PNGGET /elements→ 无障碍树GET /state/snapshot只含// Snapshotable字段→ 当前状态基于屏幕所见 vs 测试目标决定下一步动作POST /session/acquire抢占设备锁执行POST /tap、/swipe、/type或POST /state/key状态写入重新截图、对比若发现 bug 记录 finding迭代结束POST /session/release。设备锁对应 StateServer 内那个 5 分钟孤儿超时的会话结构StateServer.swift.template防止两个 Agent 并发操作同一台设备。安全侧每个经过 tailnet 监听器的已认证变更请求都会向~/.gstack/security/ios-qa-audit.jsonl写一行审计记录。从 audit.ts 与 types.ts 可见审计行包含时间戳、规范化身份、设备 UDID、端点、session_id、能力层与状态码而被拒绝的请求无令牌、过期、身份未放行、限流命中等写入attempts.jsonl且身份以加盐哈希存储不落原始值。运行模式与安全模型Local-USB 模式默认daemon 只绑 loopback不需要 Tailscale调用方技能拿到全功能面。适合单人开发。Tailnet 模式--tailnetdaemon 额外绑定 Tailscale 接口永不0.0.0.0。要求本机tailscaled在运行且 daemon 能读/var/run/tailscale.socksocket 缺失、权限拒绝或 WhoIs 不可解析时 fail-closed。远程 Agent 经 tailnet 打POST /auth/mintdaemon 通过 WhoIs 规范化身份、查 allowlist 文件、铸造会话令牌。完整操作文档在 ios-qa/docs/tailscale-acl-example.md。能力分层tailnet 模式铸造令牌默认interact可 tap/swipe/type更高层级需属主显式铸造。层级有序observeinteractmutaterestore授予高层隐含低层observe/screenshot、/elements、GET /state/*、/healthz、/session/heartbeatinteractobserve /tap、/swipe、/typemutateinteract POST /state/keyrestoremutate POST /state/restore。属主在 Mac 上执行gstack-ios-qa-mint --remote identity --capability tier铸造tailnet 自助铸造仅对已在 allowlist 中的身份成功。层级判定逻辑在 session-tokens.ts 的validate中端点与最小能力层的映射表见 types.ts。allowlist 文件~/.gstack/ios-qa-allowlist.jsonv1 schema示例{ version: 1, entries: [ { identity: youexample.com, capabilities: [restore], expires_at: null, note: Owner — full access }, { identity: ciexample.com, capabilities: [mutate], expires_at: 2026-12-31T00:00:00Z, note: CI runner — can write state but not full restore }, { identity: tag:claude-readonly, capabilities: [observe], expires_at: null, note: Agents that should only read } ] }身份经 WhoIs 规范化用户 OAuth 为userexample.com无acct:前缀带 tag 节点为tag:tagname小写节点密钥为node:nodekey-hex少见建议用 tag。文档还建议第二道防线——Tailscale ACL 本身限制谁甚至能到达 daemon 端口例如只放行ciexample.com访问ios-qa-mac:9999末尾默认drop。令牌生命周期参数与 session-tokens.ts 的常量一致daemon 铸造的会话令牌默认 TTL 1 小时、--tailnet-session-ttl上限 24 小时POST /session/heartbeat可滑动续期封顶在原最大值boot token 存活约 5 秒。限流/auth/mint每身份 10 次/60 秒滑动窗口第 11 次返回 429每个 tailnet 请求 body 1MB 硬上限超出 413对应 index.ts 的readBody默认maxBytes 1_048_576screenshot 响应 10MB 上限。审计行样例{ts:2026-05-18T14:23:00Z,identity:ciexample.com,device_udid:00008101-XXXX,endpoint:/tap,session_id:abc...,capability:interact,request_id:req_001,status:200}Recording 模式--recordingDebugOverlay 在角落渲染一个小巧斜向 AGENT DEMO 水印让录屏中一眼可辨设备正被 Agent 驱动。Demo 模式当用户说 demo、show me、I want to see it working 时进入DEMO MODE其规则覆盖一切其他规则Agent 必须把所有动作走可见 UI/tap、/swipe、/type绝不用POST /state/*跳步——观众要看到 Agent 逐字输入、逐个点击。设备上的 DebugOverlay 归属芯片显示 Driven by Claude Code (demo) 或远程 Agent 身份。demo 模式下截图帧率提升到 4fps让录制有实时感。故障模式与恢复症状可能原因处置到 daemon 的curl: connection refuseddaemon 崩了重跑/ios-qaspawn-race 锁会 fail closed/auth/mint返回403 identity_not_allowed身份不在 allowlist在 Mac 上跑gstack-ios-qa-mint --remote identity/state/restore返回409 schema_mismatch快照来自更旧的应用构建丢弃快照重新捕获代理返回503 device_disconnectedUSB 路由丢失或应用重启daemon 作废陈旧隧道并做一次全新 bootstrap若持续则重连/解锁 iPhone/auth/mint返回429 rate_limited同一身份 10 mints/min等 60 秒查审计日志有无异常/state/restore返回413 body_too_large快照 1MB调大--max-body或裁剪快照daemon 侧的隧道恢复逻辑index.ts值得展开代理请求遇到可恢复 socket 错误ECONNREFUSED、ECONNRESET、EPIPE等统一归一为 503device_disconnected当上游返回 401应用重启后新内存 bearer 拒绝旧令牌或 503/504 的device_disconnected/upstream_timeout时daemon 作废隧道、重新 bootstrap然后有条件地重放请求——只有 401证明旧 bearer 在分发前就被拒绝重放安全或 GET/HEAD/OPTIONS 读请求才重放变更请求绝不重放避免 double-tap 或状态跳转两次。清理/ios-clean 与 Release 护栏用 ios-clean 技能在 Release 构建前移除 DebugBridge SPM 依赖与所有#if DEBUG接线。但要清楚这是一条便利路径结构性的 Release 护栏才是安全关键路径——即Package.swift的.when(configuration: .debug)条件加上 CI 里swift build -c releasenm -j符号扫描的组合检查见 Package.swift.template 注释中记录的防护分级。可验证性测试覆盖这套链路的关键行为大多有对应测试便于读者按图索骥验证上文结论daemon-integration.test.tsdaemon 端到端集成session-tokens.test.ts令牌铸造、校验、限流tunnel-bootstrap.test.ts设备选择、boot token 重启恢复auth-mint.test.ts 与 allowlist.test.tstailnet 身份放行proxy-classify.test.ts端点能力分层判定single-instance.test.ts、tailscale-localapi.test.ts、audit.test.ts锁、socket 探测、审计写入gen-accessors.test.ts访问器生成的缓存与解析行为。小结ios-qa 的工程设计可以概括为三条主线信任边界最小化StateServer 只活在设备 loopbackdaemon 独占 tailnet 面且 fail-closed凭证短命化boot token ~5 秒、会话令牌默认 1 小时、全内存不落地操作可审计变更请求写 audit、拒绝写 attempts、身份哈希化。配合 Phase 0-3 的工作流与能力分层它把远程 Agent 操作一台真机 iPhone从一个危险设想变成一套有明确权限模型和恢复手册的工程方案。【免费下载链接】gstackUse Garry Tans exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表