<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>DevTools on Trevor's Blog</title><link>http://mings.in/tags/devtools/</link><description>Recent content in DevTools on Trevor's Blog</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Sun, 01 Nov 2026 11:13:00 +0800</lastBuildDate><atom:link href="http://mings.in/tags/devtools/index.xml" rel="self" type="application/rss+xml"/><item><title>给 AI 装上 DevTools：它能看见每一帧</title><link>http://mings.in/posts/agent-ddp/</link><pubDate>Sun, 01 Nov 2026 11:13:00 +0800</pubDate><guid>http://mings.in/posts/agent-ddp/</guid><description>同一块屏幕，截图 77 KB 让模型去猜，ddp tree 只用 4,184 字节就把结构说清楚。我在模拟器上给它做了对照实测，也撞上了它现在还做不到的地方。</description><content:encoded><![CDATA[<p>AI 写前端早就不是猜的。Chrome DevTools Protocol 摆在那儿，agent 能读 DOM、能点按钮、能量颜色，改完自己回头验收。</p>
<p>写原生 App 就不是这回事。iOS 和 Android 没有等价的协议，agent 拿到手的只有一张截图。</p>
<p>截图能看，但看不出「这个按钮的边框从 5.25 变到 15.75 用了 1.8 秒」，也看不出「这行字被一个半透明层盖住了」。它只能猜，猜完就改，改完再截一张。</p>
<p>十月二十三号，有个叫 agent-ddp 的项目开了源，想补的就是这块。</p>
<h2 id="它把界面变成几百行文本">它把界面变成几百行文本</h2>
<p>装完之后，agent 问一句屏幕上有啥，拿到的是这个：</p>
<pre tabindex="0"><code>@e1 animatedButton &#34;Selected&#34; [383, 158, 315, 137]
  z=1 layer fill #0000FFFF corner 31.5
  z=2 bitmap [467, 195, 147, 63]
  z=3 layer border 15.75 #FFA500FF corner 31.5
@e2 flashBox &#34;Flash&#34; [460, 358, 160, 115]
</code></pre><p>每个元素都有引用号 <code>@e1</code>，下一条命令可以直接用。填充色、边框、圆角、坐标、层级，全是数字。</p>
<p>我量了一下：这个界面 25 个元素、59 个绘制节点，<code>ddp tree</code> 输出 4,184 字节。同一块屏幕截图存下来是 77,397 字节。</p>
<p>差 18.5 倍，而且差的不是大小，是确定性。截图要模型自己看，文本它直接读。</p>
<p><img alt="同一块屏幕：77 KB 的截图和 4,184 字节的文本" loading="lazy" src="/images/agent-ddp/fig1.png"></p>
<h2 id="本机跑起来从装到出结果">本机跑起来：从装到出结果</h2>
<p>我用的是 Mac 上现成的模拟器，Medium Phone，API 36.1。</p>
<p><code>npm i -g agent-ddp</code> 装 CLI，一个 Rust 编译的原生二进制，没有运行时依赖。样例工程用 Gradle 编出 10 MB 的 debug APK，装进模拟器，打开。</p>
<p>第一步跑 <code>ddp doctor</code>，它把设备、应用、库、端点全查一遍：</p>
<blockquote>
<p>ok   agent-ddp library in the build
ok   control endpoint answers
ok   25 elements on screen
ok   hook compose.zSortedChildren (internal): answered 4608 times</p>
</blockquote>
<p>二十来行全绿。这一步很关键，因为它不只查「连没连上」，还列出每个隐藏 hook 被调了多少次。后面会说为什么。</p>
<p>接着我只敲了一行 <code>ddp check all</code>，2.7 秒，扫了 116 帧，报出九个问题，退出码 1。</p>
<h2 id="它报的九个问题长这样">它报的九个问题长这样</h2>
<ul>
<li>两处文字被盖住：<code>boxCovered</code> 的字在它的框里一个像素都找不到</li>
<li>一处圆角越界：<code>hatchCard</code> 的灰线画到了圆角外面</li>
<li>一处动画不停：<code>clockBar</code> 最后 500 毫秒还在变，29 次</li>
<li>两处无障碍没标签、两处点击区只有 37×47，低于 48×48</li>
<li>一处对比度 1.5:1，低于 4.5:1</li>
</ul>
<p>每一条都带证据坐标，比如那句「它的字在 [42, 1283, 258, 85] 里一个像素都不出现，那个位置屏幕是 #9E9E9EFF」。</p>
<p>我核对了截图，那块地方确实是灰色方块压在文字上。</p>
<p><img alt="一条 check all 报出的九条问题，退出码 1" loading="lazy" src="/images/agent-ddp/fig2.png"></p>
<h2 id="我最喜欢的是它拒绝我的那一下">我最喜欢的是它拒绝我的那一下</h2>
<p>界面上那个被盖住的按钮，我试着让它点：</p>
<blockquote>
<p>ddp: boxCovered is covered by pointerInput: no point of it takes a touch
next: ddp check tappable, or ddp tap boxCovered &ndash;force to touch it anyway</p>
</blockquote>
<p>它不点，并且告诉你为什么，还给出两个下一步。</p>
<p>凡是做过 UI 自动化的人都知道这有多反常。传统框架碰到这种情况会硬点下去，然后你去猜为什么没反应。</p>
<h2 id="它给自己定的三条硬规矩">它给自己定的三条硬规矩</h2>
<p>翻文档时我发现，这个项目把三句话写成了不可违反的规矩，排在所有其他规则前面。</p>
<p>第一条，零误报。一条报警只有在确定有问题时才报，拿不准就什么都不说。理由写在文档里：一次误报会教 agent 养成随手加 <code>--force</code> 的习惯，那比漏报贵得多。</p>
<p>第二条，平台引擎是唯一事实源。什么吃到点击、画了什么、谁盖住谁，这些答案必须来自 UIKit、Compose、View 分发这些平台自己的代码。它不许自己重写一套规则去模拟平台的判断。</p>
<p>第三条，每个 hook 都必须计量。凡是用了私有符号或者框架内部成员的地方，都要登记，并且每次调用都计数。hook 失效时，结论变成「未知」，不许变成「有问题」。</p>
<p><code>ddp doctor</code> 里那行 <code>answered 4608 times</code> 就是这条规矩的产物。系统升级后某个内部结构变了，计数会直接掉下去，你立刻知道有个检查悄悄失明了。</p>
<p><img alt="写在所有规则前面的三条硬规矩" loading="lazy" src="/images/agent-ddp/fig4.png"></p>
<h2 id="它写了-cdp-方案当天就自己否了">它写了 CDP 方案，当天就自己否了</h2>
<p>这个项目有份决策记录特别值得看。</p>
<p>最早的设计是让 Debug 包里的库直接说 Chrome DevTools Protocol，这样 Chrome 和现成的 CDP 客户端都能连上，agent 也不用学第二套协议。</p>
<p>同一天，它自己把这份方案否了。</p>
<p>理由很实在：为了模仿浏览器，它得在原生元素树上面假装出一个 <code>#document &gt; html &gt; body</code>，得支持 CSS 选择器，得给偏好设置起名 localStorage，还得为 Chrome 要的字段填假值。</p>
<p>每一样都是翻译层，学会它、维护它的人是你。更糟的是，这些翻译会把 App 真实的结构藏起来。</p>
<p>换掉之后，协议名改成 <code>screen.elements</code>、<code>input.touch</code>、<code>prefs.write</code> 这种原生叫法。删掉的浏览器层大约六百行。</p>
<h2 id="装进去的东西很轻">装进去的东西很轻</h2>
<p>iOS 那份是 Swift，四千三百多行；Android 那份 Kotlin，四千一百多行；CLI 是 Rust，九千七百多行。</p>
<p>Debug 包里的库自己就是服务器，跑在一个本地 WebSocket 上。Android 用的是只有 shell 用户和 root 能连的抽象 socket，iOS 只监听 127.0.0.1，还挡掉非 localhost 的 Host 头。</p>
<p>没有守护进程。每条 <code>ddp</code> 命令都是一个短进程，录下来就是一串 jsonl 文件。同一个文件，可以反复问不同的问题。</p>
<p>Release 包里这些代码一行都没有，Android 是 <code>debugImplementation</code>，iOS 整个包在 <code>#if DEBUG</code> 里面。</p>
<p><img alt="Debug 库、Rust CLI、agent 三段，中间只有一个本地 WebSocket" loading="lazy" src="/images/agent-ddp/fig3.png"></p>
<h2 id="说点不好听的">说点不好听的</h2>
<p>它 0 star。十月二十三号建仓，现在共 114 次提交、五个版本，五个开着的 issue 全是依赖升级机器人提的。</p>
<p>iOS 这半边只支持模拟器，真机还没做。看不见的东西也列了一长串：SwiftUI Canvas 里的图文、系统弹窗、部分覆盖的文字、Compose 里某些画进图层的缓存绘制。</p>
<p>还有一类我特意试了：一个按钮只在手指抬起时才响应触摸，这种它判断不了，只会说触摸能到，然后 <code>tap</code> 之后告诉你「什么都没变」。</p>
<p>这不是缺陷，是它诚实。宁可说不知道，也不猜一个答案给你。</p>
<h2 id="我的判断">我的判断</h2>
<p>这波工具真正的分水岭不是能读多少属性，是敢不敢闭嘴。</p>
<p>一个什么都敢报的检查器，agent 学会的是绕过它。一个只在该报的时候报的检查器，agent 才敢把它当验收条件写进流程。</p>
<p>九个真问题、2.7 秒、一千零九十一字节的输出，一个退出码。这套东西如果稳，agent 做 UI 就从「写代码」变成「写完自己验」。</p>
<p>你现在让 agent 改 UI，是发截图给它看，还是让它自己摸？</p>
]]></content:encoded></item></channel></rss>