<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Bruce 博客</title><link>https://bruce.flydog.ai/blog?lang=zh</link><description>设计、产品、交互、工程探索与决策。把创造的过程，写给开发者和与他们协作的 Agent。</description><language>zh-CN</language><atom:link href="https://bruce.flydog.ai/blog/feed.zh.xml" rel="self" type="application/rss+xml"/><item><title>把我们走过的路，留给下一位开发者</title><link>https://bruce.flydog.ai/blog/why-we-build-in-public?lang=zh</link><guid isPermaLink="true">https://bruce.flydog.ai/blog/why-we-build-in-public</guid><description>Bruce 博客创刊词：记录设计、产品、交互、技术与决策，让开发的过程成为开发者和他们的 Agent 都能继续使用的知识。</description><dc:creator>Bruce 团队</dc:creator><category>editorial</category><pubDate>Thu, 05 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;把我们走过的路，留给下一位开发者&lt;/h1&gt;&lt;p&gt;Bruce 博客创刊词：记录设计、产品、交互、技术与决策，让开发的过程成为开发者和他们的 Agent 都能继续使用的知识。&lt;/p&gt;&lt;p&gt;Bruce 团队 · 发布于 &lt;time datetime=&quot;2026-11-05&quot;&gt;2026-11-05&lt;/time&gt;&lt;/p&gt;&lt;section id=&quot;everyone-who-builds&quot;&gt;&lt;h2&gt;使用工具的人，也在成为开发者&lt;/h2&gt;&lt;p&gt;我们做 Bruce，是希望更多人能把想法变成可以使用的作品。独立开发者、第一次尝试做产品的人，以及专业团队，走进来时可能有不同的经验。只要开始提出问题、验证想法、判断结果，他们就在参与开发。&lt;/p&gt;&lt;p&gt;AI 能替我们完成越来越多的代码，但一个产品为什么这样做、什么地方不顺手、哪次尝试没有奏效，仍然需要人的判断。这样的判断经常藏在会话、改动和提交里。项目向前走，它们却很容易散掉。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;leave-a-trail&quot;&gt;&lt;h2&gt;把做过的事，写成别人能继续走的路&lt;/h2&gt;&lt;p&gt;这个栏目从这里开始：把 Bruce 的设计、产品、交互、工程探索和关键决策持续记录下来。我们希望文章能交代最初的问题、观察到的现象、尝试过的方向、最后的选择，以及还没有解决的部分。&lt;/p&gt;&lt;p&gt;这也是一种 Build in Public。公开的不必只有漂亮的结果。一次绕路、一个被推翻的猜测、一组放回真实使用环境才有意义的数据，都可能帮下一位开发者少走一段弯路。文章面对人，也面对帮助他们工作的 Agent：背景、约束和验证方法越清楚，知识越容易被接着使用。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;from-work-to-writing&quot;&gt;&lt;h2&gt;让开发过程，长出自己的开发日志&lt;/h2&gt;&lt;p&gt;这不只是我们给自己的写作要求，也是 Bruce 希望帮助使用者做到的事。现有的内置写作提示可以围绕一个主题检索记忆、里程碑和相关记录，并结合可用的 Git 历史，整理博客、开发日志、进展回顾和 Build-in-Public 草稿。&lt;/p&gt;&lt;p&gt;找到素材与写出草稿之后，还需要开发者核对事实、补充解释、删去不适合公开的信息，再决定是否保存和发布。我们重视这个决定权。文章应该帮助你表达自己的工作，而不是让工具替你宣告没有核实的成果。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;a-place-to-talk&quot;&gt;&lt;h2&gt;让技术交流有一个具体的落点&lt;/h2&gt;&lt;p&gt;我们不期待每篇文章都给出唯一答案。我们更希望它能把讨论落到具体情境里：你为什么选择另一条路？同样的交互在你的设备上有什么不同？你的 Agent 从这些记录里发现了什么遗漏？&lt;/p&gt;&lt;p&gt;文章会有稳定的地址、可引用的小节和便于保存的文字版本。你可以把它交给同伴或 Agent，也可以带着具体文章来信讨论。交流从已经发生的事情开始，比重新解释整段背景更容易。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;explain-the-product&quot;&gt;&lt;h2&gt;让一篇文章，支持更多次清楚的表达&lt;/h2&gt;&lt;p&gt;官网上的一句特性介绍，背后应该有足够扎实的理由。博客为这些理由留下空间：交互为什么这样安排、一个技术选择换来了什么、轻量和完整体验之间怎样取舍。需要深入了解时，产品页面可以直接引用对应的文章。&lt;/p&gt;&lt;p&gt;同一份经过核对的内容，也能成为对外供稿、产品介绍和媒体交流的基础素材。我们会保留完整解释，再按场景整理摘要；让宣传与实际工作有共同的依据，也让后来回看项目时能理解当时的判断。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;start-here&quot;&gt;&lt;h2&gt;从创刊号开始，逐步补上来路&lt;/h2&gt;&lt;p&gt;第一批内容包括这篇创刊词、小狗标志的设计记录、两次交互排查，以及关于 Agent 运行边界的工程说明。旧记录会标出发生时间与本次整理时间，历史实验中的数字也会保留它们的测试条件。&lt;/p&gt;&lt;p&gt;接下来，我们会逐步从项目沉淀的文档和提交记录中整理更多值得讲清楚的主题。设计、产品、交互、技术和决策，都有自己的故事。希望你在这里读到的经验，能成为你的下一次尝试，也希望我们能由此认识更多正在创造的人。&lt;/p&gt;&lt;/section&gt;</content:encoded></item>
<item><title>一晚适配 iPhone Duo：先测量，再把位置让给系统</title><link>https://bruce.flydog.ai/blog/iphone-duo-one-night?lang=zh</link><guid isPermaLink="true">https://bruce.flydog.ai/blog/iphone-duo-one-night</guid><description>两块屏、一条铰链、六种姿态，2027 年 4 月前必须交上截屏。我们把 Bruce 的 iPhone 应用逐个状态量了一遍，删掉自画的头部，让内容避开折痕，也记下了每一次走错的地方。</description><dc:creator>杨永安 + Claude Code</dc:creator><category>engineering</category><pubDate>Fri, 13 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;一晚适配 iPhone Duo：先测量，再把位置让给系统&lt;/h1&gt;&lt;p&gt;两块屏、一条铰链、六种姿态，2027 年 4 月前必须交上截屏。我们把 Bruce 的 iPhone 应用逐个状态量了一遍，删掉自画的头部，让内容避开折痕，也记下了每一次走错的地方。&lt;/p&gt;&lt;p&gt;杨永安 + Claude Code · 发布于 &lt;time datetime=&quot;2026-11-13&quot;&gt;2026-11-13&lt;/time&gt; · 原始记录 &lt;time datetime=&quot;2026-11-13&quot;&gt;2026-11-13&lt;/time&gt;&lt;/p&gt;&lt;section id=&quot;deadline&quot;&gt;&lt;h2&gt;一个有期限的新形态&lt;/h2&gt;&lt;p&gt;iPhone Duo 于 2026 年 10 月 23 日上市。它是护照式的折叠屏：合上时用外侧的封面屏，打开后是一块更大的内屏，还可以半开着立在桌上。Apple 在开发者新闻里写得很直接：用 Xcode 27.1 重新编译，检查每一种姿态和方向，提交 iPhone Duo 截屏，产品页就会显示为 iPhone Duo 优化的标记；从 2027 年 4 月起，提交审核需要附带 Duo 截屏。&lt;/p&gt;&lt;p&gt;Bruce 的 iPhone 应用是电脑上 Agent 的遥控器：看会话、回答问题、批准操作、打开终端。它原本按“一台设备一块矩形屏”布局。这篇记录我们和 Claude Code 用一个晚上，把它适配到这块新屏幕的过程：先测量，再取舍，然后逐一验证。&lt;/p&gt;&lt;p&gt;截屏尺寸也写在规范里：外屏 1398 × 2034 或 2034 × 1398 像素，内屏 2007 × 2853 或 2853 × 2007 像素，恰好就是模拟器两块屏的原生像素。&lt;/p&gt;&lt;ul class=&quot;article-sources&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/news/?id=kkphp5qo&quot; rel=&quot;noopener noreferrer&quot;&gt;Apple Developer · Prepare and submit your apps for iPhone Duo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/&quot; rel=&quot;noopener noreferrer&quot;&gt;App Store Connect Help · Screenshot specifications&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/iphone-duo/&quot; rel=&quot;noopener noreferrer&quot;&gt;Apple Developer · iPhone Duo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/iphone-duo/prepare/&quot; rel=&quot;noopener noreferrer&quot;&gt;Apple Developer · Three steps to make your app shine on iPhone Duo&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;measure&quot;&gt;&lt;h2&gt;先把六种状态量出来&lt;/h2&gt;&lt;p&gt;我们没有先改代码，而是先给应用加了一个调试探针：读出窗口、方向、尺寸类、安全区，以及 iOS 27.1 新增的预留区域和铰链状态。应用在 Duo 上不能自己旋转，模拟器也不能用命令开合，于是由人在模拟器里折叠、旋转，一个小脚本每两秒读一次探针，状态一变就保存读数和截图。&lt;/p&gt;&lt;p&gt;量出来的东西有几处出乎意料。折叠时，状态区只是右上角一块 84 × 174 的矩形，安全区却把整条右边让了出去；半开时，铰链是一条 40 点宽的分割区，折痕两侧各 20 点；完全展开时，这条分割区仍然存在，只是标记为未生效。还有一个后来很重要的事实：即使窗口在内屏上，UIScreen.main 报告的依然是 466 × 678 的封面屏。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/states.zh.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/states.zh.svg&quot; width=&quot;1000&quot; height=&quot;650&quot; alt=&quot;六种状态的示意图：折叠竖握 466×678、折叠横握 678×466、展开竖握 669×951、展开横握 951×669、半开竖握与半开横握。图中标出安全区、右上角或左上角的遮挡区，以及半开时生效、完全展开时未生效的分割区。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 1　六种状态逐一实测。数字来自 iOS 27.1 的预留区域与铰链读数，单位为点。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;system-bars&quot;&gt;&lt;h2&gt;第一次走错：想把那条空白占回来&lt;/h2&gt;&lt;p&gt;折叠竖握时，应用只用了 466 点宽度里的 382 点，状态区下面那一长条一直空着。我们的第一反应是让列表延伸进去。改完一看，对齐乱了；再读 Apple 的 iPhone Duo 设计指南，发现那一条本来就是系统在外屏放工具栏和标签栏的位置。&lt;/p&gt;&lt;p&gt;问题出在我们自己画的头部：分段标签、搜索、新建按钮横在顶部，系统想把栏位竖着放到侧边，却无处可放。当晚撤回，改为系统的 TabView 和工具栏：会话、项目、设备三个标签，加一个系统搜索标签，各自的操作作为工具栏项。折叠时，系统自然把它们竖排在右侧那一条里。&lt;/p&gt;&lt;p&gt;这次也定下了一条以后都适用的原则：&lt;mark&gt;尽量拥抱平台标准，不自立门户&lt;/mark&gt;。原生感和平台特性优先于三端外观一致；概念、用词和行为保持一致，界面形态跟随各平台。&lt;/p&gt;&lt;div class=&quot;journal-figures journal-figures-pair&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/folded-before.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/folded-before.webp&quot; width=&quot;699&quot; height=&quot;1017&quot; alt=&quot;改造前，折叠封面屏上的首页：自画的分段标签、搜索和新建按钮横在顶部，右侧状态区下方是一整条空白。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 2　改造前。自画头部横在顶部，右侧那一条空着。&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/folded-after.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/folded-after.webp&quot; width=&quot;699&quot; height=&quot;1017&quot; alt=&quot;改造后，折叠封面屏上的首页：系统大标题“会话”，账号按钮在右上，系统标签栏竖排在右侧那一条里。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 3　改造后。系统把标签栏放进右侧那一条。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;tabs&quot;&gt;&lt;h2&gt;标签是导航，不是工具&lt;/h2&gt;&lt;p&gt;内屏横握时，系统同样把标签栏做成右侧的一列图标。看到截图时，产品方向很快给出了判断：三个标签是应用的导航，没有文字就丢了语义，它们不是工具按钮。我们翻看了几款大厂应用在宽屏上的做法，先试了系统的 sidebarAdaptable 侧边栏：一旦收起，就再也找不到展开的入口，而且这个状态还会被记住。&lt;/p&gt;&lt;p&gt;最终的选择更克制：窗口足够宽、而且没有被铰链分开时，用 iOS 27.1 的 toolbarVerticalBehavior(.disabled) 保持栏位横排，标签栏带着文字回到底部；封面屏和半开分栏仍交给系统竖排。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/open-landscape.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/open-landscape.webp&quot; width=&quot;1400&quot; height=&quot;984&quot; alt=&quot;展开横握的内屏：左侧是会话列表，右侧是一个 Agent 会话页，底部是带文字的系统标签栏：会话、项目、设备、搜索。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 4　展开横握。列表在左、页面在右，标签栏保留文字。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;headers&quot;&gt;&lt;h2&gt;把页面头部交给系统栏&lt;/h2&gt;&lt;p&gt;首页之后是各个页面。会话页原来也自己画头部：返回、头像、标题、几个按钮，一行横在顶部。改成系统栏后，头像和标题作为中间项，点标题仍弹出 Agent、项目、电脑等入口；返回、终端、侧栏作为栏位项，在封面屏上随系统竖到侧边。&lt;/p&gt;&lt;p&gt;这一步踩了三个坑。其一，分栏右侧那一栏的系统栏是空的：页面的工具栏放在 .inspector 里面，传不到导航栏，只能把 inspector 移到导航栈外面。其二，对话记录为了长会话的滚动性能是倒置的滚动视图，用不了系统的边缘渐隐，内容直接透过栏位。只给栏位加底色还不够：侧边栏位时，标题条离屏幕顶部还有 14 点，文字清清楚楚露在上方。最后在源头解决，用遮罩让对话记录止于顶部栏，底部仍可滚到输入框下面。其三，侧栏按钮的开关状态，交给系统的开关按钮来表达。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;half-open&quot;&gt;&lt;h2&gt;半开：上面看，下面按&lt;/h2&gt;&lt;p&gt;半开竖握时，铰链横穿窗口，正好在 456 到 496 点之间。我们写了一个 FoldedColumn：对话内容止于折痕上方，后台任务条、问答托盘、输入框和终端按键从折痕下方的顶端开始。结构始终只有一个，只是尺寸随折痕变化，所以开合时对话内容和滚动位置都不会丢。&lt;/p&gt;&lt;p&gt;图片查看器原来把照片居中放，半开时正好压在折痕上。现在照片在折痕上方，关闭按钮和计数紧贴折痕下方。半屏弹窗则不用我们操心：实测系统已经把它的上沿放在 487.5 点，也就是折痕下方。&lt;/p&gt;&lt;div class=&quot;journal-figures journal-figures-pair&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/half-open-session.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/half-open-session.webp&quot; width=&quot;700&quot; height=&quot;995&quot; alt=&quot;半开竖握的 Agent 会话页：对话内容在折痕上方结束，后台任务条、提示和输入框从折痕下方开始。两条青色虚线标出折痕所在的 456 到 496 点。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 5　半开竖握的会话页。虚线为后加的标注，标出折痕位置。&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/half-open-photo.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/half-open-photo.webp&quot; width=&quot;700&quot; height=&quot;995&quot; alt=&quot;半开竖握的图片查看器：照片完整显示在折痕上方，关闭按钮和“1 / 2”的计数紧贴折痕下方。青色虚线标出折痕。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 6　图片查看器。照片在上，栏在折痕下方。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;keyboard&quot;&gt;&lt;h2&gt;没有键盘的模拟器，和一个替身键盘&lt;/h2&gt;&lt;p&gt;键盘弹起后，折痕下方还放得下输入框吗？这需要测，但这台模拟器无界面运行，点进输入框不会弹出软键盘，⌘K 也没有窗口可用。我们在调试通道里做了一块“替身键盘”：按指定高度占掉页面底部，效果和真键盘的安全区一样。&lt;/p&gt;&lt;p&gt;200 点时，输入框留在折痕下方；336 点时，下方放不下，输入框上移，正好停在折痕上方。第一版在这里出了错：它按子视图之和测量自己的高度，键盘弹起时沿用了旧值，把对话内容顶到了标题栏后面。改为按分到的空间测量后，四种状态都通过了复测。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/keyboard.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/keyboard.webp&quot; width=&quot;1728&quot; height=&quot;597&quot; alt=&quot;四张半开竖握截图并排：没有键盘时输入框在折痕下方；200 点替身键盘时仍在下方；336 点时输入框上移到折痕上方；收起键盘后恢复原样。青色虚线标出折痕。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 7　从左到右：无键盘、200 点、336 点、收起。灰色块为替身键盘。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;window&quot;&gt;&lt;h2&gt;窗口，不是屏幕&lt;/h2&gt;&lt;p&gt;键盘那里还藏着一个更早的问题。原来的代码拿键盘位置和 UIScreen.main 的高度比较，用来判断键盘是否弹起。在 Duo 上，主屏是 678 点高的封面屏，窗口却在 951 点高的内屏：内屏上顶边在 700 点的键盘，会被当成已经收起。&lt;/p&gt;&lt;p&gt;修正方式是用键盘通知自带的屏幕，再把位置换算成它所覆盖的窗口坐标。为了验证，我们又在调试通道里加了一条模拟的键盘通知，确认 700 点的键盘现在能识别为弹起。顺带发现，另一处使用主屏尺寸的预览缩放代码根本没有调用方，直接删掉；现在整个应用不再使用 UIScreen.main。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;fold-mid-use&quot;&gt;&lt;h2&gt;使用中开合，页面跟着走&lt;/h2&gt;&lt;p&gt;折叠和展开竖握都是窄布局，页面压在列表的导航栈里；展开横握是宽布局，页面放在列表旁边。如果正在看一段会话时开合设备，会发生什么？我们用一个调试开关模拟宽窄切换，发现两个问题：变宽时，压栈的页面留在左侧 360 点宽的列表栏里，右边是空的；变窄时，分栏收起到详情栏，系统栏只剩返回键，页面标题不见了。&lt;/p&gt;&lt;p&gt;现在页面总放在当前宽度该放的位置：变宽时挪进详情栏，变窄时挪回栈里。终端打开期间不挪，等它关闭再说。连接由外壳持有而不是页面，所以开合不会重连；往返两轮，当前会话和输入框里的草稿都还在。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/fold-move.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/fold-move.webp&quot; width=&quot;1728&quot; height=&quot;597&quot; alt=&quot;四张截图：模拟展开到宽布局时会话页挪进右侧详情栏、列表盖在左侧；切回窄布局时会话页回到栈里并显示标题和草稿；再往返一次结果相同。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 8　宽、窄、宽、窄往返两轮，页面、标题和草稿都保留。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;search&quot;&gt;&lt;h2&gt;顺手做完的搜索&lt;/h2&gt;&lt;p&gt;首页改用系统标签后，搜索也成了系统的搜索标签：一个输入框同时找设备、项目和会话，结果按标签的名字分组，每组用的都是那个标签自己的行。设备和项目在手机上边输边匹配；会话交给每台电脑自己搜索，停止输入片刻后再问。什么都没找到时，用系统自带的“无结果”页面。&lt;/p&gt;&lt;div class=&quot;journal-figures journal-figures-pair&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/search-light.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/search-light.webp&quot; width=&quot;700&quot; height=&quot;995&quot; alt=&quot;浅色模式下的搜索标签，搜索“bruce”：设备组列出两台测试电脑和两台离线电脑，项目组列出两台电脑上的 bruce 项目，下面是开着的会话。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 9　搜索结果按设备、项目、会话分组（浅色）。&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/search-dark.webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/iphone-duo/search-dark.webp&quot; width=&quot;700&quot; height=&quot;995&quot; alt=&quot;深色模式下同样的搜索结果：设备、项目和会话三组，列表滚过折痕，和系统自己的列表一致。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 10　同一搜索的深色外观，分组与行和浅色一致。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;tools&quot;&gt;&lt;h2&gt;让每种状态都能测&lt;/h2&gt;&lt;p&gt;这一晚最有用的不是某个修复，而是让每种状态都能被测量的工具。真机还没到手、模拟器又不能用命令开合时，这些调试开关替我们走到了各种状态，每个结论都配一张截图和一组读数。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;探针：一次读出窗口、尺寸类、安全区、遮挡区、分割区和铰链状态。&lt;/li&gt;&lt;li&gt;侧边栏位开关：在内屏上打开系统的竖排栏位，模拟折叠时的样子。&lt;/li&gt;&lt;li&gt;模拟折痕：启动时在任意屏幕上放一条生效的分割区。&lt;/li&gt;&lt;li&gt;替身键盘和模拟键盘通知：在没有软键盘的模拟器上测键盘。&lt;/li&gt;&lt;li&gt;宽度开关：像开合设备一样，在宽窄布局之间切换。&lt;/li&gt;&lt;li&gt;一个截图脚本：在当前状态下截取九个页面的亮暗两套。&lt;/li&gt;&lt;li&gt;源码锁定测试：不用 UIScreen.main、分割区取自系统、页面随宽度移动，每个取舍都有测试守着。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;left&quot;&gt;&lt;h2&gt;还没做完的&lt;/h2&gt;&lt;p&gt;我们只在半开竖握这个真实状态下跑完了整套截图；折叠、完全展开的两个方向和半开横握，还需要人动手调好设备再跑一遍。听写中途开合预计会中断听写，因为输入框会随页面重建；终端中途开合，以及终端和文件页面，都需要连着电脑才能测。&lt;/p&gt;&lt;p&gt;构建这一侧已经满足徽章条件：Bruce 用 iPhoneOS 27.2 SDK 构建，支持竖屏和左右横屏，没有要求全屏。剩下的是用真实内容截取内外两块屏的截图，在 App Store Connect 上传。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;checklist&quot;&gt;&lt;h2&gt;给其他团队的清单&lt;/h2&gt;&lt;p&gt;如果你也在赶 2027 年 4 月这个期限，这是我们愿意重新走一遍的顺序。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;用 iOS 27.1 或更高版本的 SDK 重新构建，检查全屏设置和支持的方向。&lt;/li&gt;&lt;li&gt;改任何东西之前，先把六种状态量出来，数字和截图放在一起。&lt;/li&gt;&lt;li&gt;把自画的头部和标签条换成系统栏位，让系统决定它们在每块屏上的位置。&lt;/li&gt;&lt;li&gt;在需要布局的视图里读取分割区；它生效时，文字和控件都不要跨过去。&lt;/li&gt;&lt;li&gt;尺寸取自窗口和视图几何，不要用 UIScreen.main。&lt;/li&gt;&lt;li&gt;决定页面在每种宽度下放在哪里，使用中宽度变化时把它挪过去。&lt;/li&gt;&lt;li&gt;除非测出问题，保留系统对弹窗的摆放。&lt;/li&gt;&lt;li&gt;按原生尺寸截取内外两块屏，提交到 App Store Connect。&lt;/li&gt;&lt;/ul&gt;&lt;ul class=&quot;article-sources&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/news/?id=kkphp5qo&quot; rel=&quot;noopener noreferrer&quot;&gt;Apple Developer · Prepare and submit your apps for iPhone Duo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/&quot; rel=&quot;noopener noreferrer&quot;&gt;App Store Connect Help · Screenshot specifications&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/iphone-duo/&quot; rel=&quot;noopener noreferrer&quot;&gt;Apple Developer · iPhone Duo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/iphone-duo/prepare/&quot; rel=&quot;noopener noreferrer&quot;&gt;Apple Developer · Three steps to make your app shine on iPhone Duo&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;</content:encoded></item>
<item><title>一句话交给九个 Agent：Bruce 动态表情评测</title><link>https://bruce.flydog.ai/blog/one-prompt-nine-agents?lang=zh</link><guid isPermaLink="true">https://bruce.flydog.ai/blog/one-prompt-nine-agents</guid><description>同一句任务，九个编程 Agent，不追问也不重试。用真实浏览器检查交付的 SVG，加一轮匿名盲评；最大的教训是：最贵的错误往往不出声。</description><dc:creator>杨永安 + Claude Code</dc:creator><category>design</category><pubDate>Wed, 11 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;一句话交给九个 Agent：Bruce 动态表情评测&lt;/h1&gt;&lt;p&gt;同一句任务，九个编程 Agent，不追问也不重试。用真实浏览器检查交付的 SVG，加一轮匿名盲评；最大的教训是：最贵的错误往往不出声。&lt;/p&gt;&lt;p&gt;杨永安 + Claude Code · 发布于 &lt;time datetime=&quot;2026-11-11&quot;&gt;2026-11-11&lt;/time&gt; · 原始记录 &lt;time datetime=&quot;2026-11-11&quot;&gt;2026-11-11&lt;/time&gt;&lt;/p&gt;&lt;section id=&quot;the-brief&quot;&gt;&lt;h2&gt;一句话的考题&lt;/h2&gt;&lt;p&gt;11 月 11 日，我们把同一句话交给九个编程 Agent：参考一个研究 Grok Bot 动态表情的学习项目，使用 Bruce 品牌的原始素材，做一组 SVG 动画表情，附上设计方案和预览页，放进以「模型名称 + 版本 + 能效」命名的目录。要求是真正的 SVG 动画、完整的动画帧，后续会用在等待、祝贺和博客插画里。&lt;/p&gt;&lt;p&gt;规则只有一条：&lt;strong&gt;只发这一句话&lt;/strong&gt;。不追问、不补充、不重试。哪怕某个会话看起来只差一句「继续」，也按当时的成品计分。所以这次比较的是「工具 + 模型 + 当时的配置」在一次机会里的表现，是一份样本，&lt;em class=&quot;journal-wavy&quot;&gt;不是对模型能力的全面结论&lt;/em&gt;。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Codex · gpt-6-astra&lt;/li&gt;&lt;li&gt;ZCode · GLM-5.3&lt;/li&gt;&lt;li&gt;Kimi Code · 会话未报告底层型号&lt;/li&gt;&lt;li&gt;Grok CLI · grok-4.7-build-fast&lt;/li&gt;&lt;li&gt;Qwen Code · qwen4-max-preview&lt;/li&gt;&lt;li&gt;DeepSeek Harness · deepseek-v4-pro&lt;/li&gt;&lt;li&gt;WorkBuddy 5.3.12 · 腾讯混元 Hy4（作品目录名为 claude-opus-6-high，见下文）&lt;/li&gt;&lt;li&gt;MiMo Code 0.1.16 · mimo-v2.6-pro（未交付）&lt;/li&gt;&lt;li&gt;Claude Code · claude-opus-5-5（本文评审，作品不参与排名）&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;snapshot&quot;&gt;&lt;h2&gt;先封存，再评判&lt;/h2&gt;&lt;p&gt;有四个 Agent 按项目规范自己提交了作品，另外四个只把文件留在工作区。评审开始前，我们代后者按各自的工具和模型名提交了一次，&lt;strong&gt;文件不做任何改动&lt;/strong&gt;，作为评审依据的快照。MiMo Code 没有产生作品文件，没有可以封存的内容。&lt;/p&gt;&lt;p&gt;是否自行提交也计入了评分：项目约定任务完成并通过检查后及时提交，这是 Agent 能否读懂并遵守一个项目工作约定的直接证据。它在总分里只占很小的比重。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;how-we-judged&quot;&gt;&lt;h2&gt;怎么评&lt;/h2&gt;&lt;p&gt;评审由 Claude Code 完成，而它本身也是参赛者。为了避免自评，它的作品按同一标准打分、公开分数，但&lt;em class=&quot;journal-wavy&quot;&gt;不进入排名&lt;/em&gt;。&lt;/p&gt;&lt;p&gt;评分尽量依赖可复现的测量。我们用 Chrome 加载每份作品&lt;mark&gt;真正交付的 SVG 文件&lt;/mark&gt;，在各自的循环里均匀暂停取帧；逐条检查动画的 keyTimes、keySplines 和取值是否有效；检查是否内嵌位图、脚本或外部引用，以及多个文件放在同一页面时 id 是否冲突；再在 1280 和 390 像素宽度下打开各自的预览页，记录报错和横向溢出。&lt;/p&gt;&lt;p&gt;另外做了一轮匿名盲评：把八组连拍去掉名字、随机编号，交给另一个模型，按品牌还原、表现力、可见运动、绘制质量和产品可用性打分。它和评审属于同一个模型家族，结果只作参考。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;交付与规范 15 分：设计方案、预览页、目录命名、按项目规范提交、如实报告身份。&lt;/li&gt;&lt;li&gt;品牌还原 20 分：是否还是主人认定的那只左向飞行员哈士奇。&lt;/li&gt;&lt;li&gt;动画表现 25 分：情绪是否可分，预备、过冲、挤压拉伸是否到位，帧与帧之间是否真的在动。&lt;/li&gt;&lt;li&gt;工程质量 25 分：动画是否有效、文件是否自包含、体积、id 隔离、预览页的稳健性。&lt;/li&gt;&lt;li&gt;产品可用 15 分：场景覆盖、小尺寸、深浅色和减少动态效果的处理、接入说明。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;results&quot;&gt;&lt;h2&gt;结果&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Codex 以 77 分排在第一&lt;/strong&gt;，Kimi Code 74 分第二，ZCode 71 分第三，WorkBuddy 搭配混元 67 分第四。Qwen Code 59 分、Grok CLI 50 分、DeepSeek Harness 48 分。MiMo Code 没有交付作品。评审自己的作品按同一标准得 87 分，不参与排名；在匿名盲评里，它与混元的作品并列最高。&lt;/p&gt;&lt;p&gt;前四名的差别主要在取舍：有的重画角色换来夸张的动作，有的保住品牌但表情变化不多。后三名的问题更集中，下文分别说明。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/scoreboard.zh.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;source media=&quot;(max-width:640px)&quot; srcset=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/scoreboard-narrow.zh.svg&quot; width=&quot;520&quot; height=&quot;954&quot;&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/scoreboard.zh.svg&quot; width=&quot;1000&quot; height=&quot;520&quot; alt=&quot;横向堆叠条形图：Codex 77、Kimi Code 74、ZCode 71、WorkBuddy 混元 67、Qwen Code 59、Grok CLI 50、DeepSeek Harness 48，MiMo Code 未交付，评审作品 87 分不参与排名。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 1　综合评分，五种颜色分别对应五项标准。评审作品以浅色显示，不参与排名。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;codex&quot;&gt;&lt;h2&gt;Codex · gpt-6-astra：最像一套能直接用的素材&lt;/h2&gt;&lt;p&gt;它没有沿用侧脸主图，而是重画了一只全身 Q 版的 Bruce：配色、护目镜和紫色徽章都在，身体、尾巴和道具（灯泡、对勾、笔记本电脑）让 12 个表情一眼就能分开。&lt;mark&gt;488 条 SMIL 动画全部有效&lt;/mark&gt;，平均每个文件 23.7KB；预览页设计完整，在 390 像素宽度下不溢出，也没有报错。它还先提交了一份方案，再提交作品，完整遵循了项目的长任务约定。&lt;/p&gt;&lt;p&gt;扣分在品牌：&lt;strong&gt;全身造型不是主人认定的主图&lt;/strong&gt;，角色在画面里占比偏小，小尺寸下脸部不易看清。SMIL 动画也无法在文件内响应系统的「减少动态效果」设置。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-gpt.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-gpt.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;Codex 作品中的三枚表情：等待中的全身 Q 版 Bruce、跳起庆祝并撒出彩带、出错时皱眉后仰。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 2　Codex：等待、庆祝、出错。全身造型让动作可以落在身体和道具上。配图去掉了原文件里的一层描边滤镜，原因见文末。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/gpt-6-astra-ultra/index.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;kimi&quot;&gt;&lt;h2&gt;Kimi Code：最克制，也最稳&lt;/h2&gt;&lt;p&gt;它直接复用主图路径，把 Bruce 放在浅色圆底上。10 个表情的变化主要来自整体的跳动和挤压，以及问号、心形、警告、对勾等叠加元素。196 条动画全部有效，文件小，预览页设计感很强。目录名里写的 unspecified 说明会话没有提供可核验的模型版本，它选择&lt;mark&gt;如实写出，没有编造型号&lt;/mark&gt;。&lt;/p&gt;&lt;p&gt;不足是&lt;strong&gt;脸几乎不变&lt;/strong&gt;：眼睛和嘴保持主图原样，情绪主要靠旁边的符号表达，夸张程度不够。作品也没有按项目规范提交。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-kimi.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-kimi.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;Kimi Code 作品中的三枚表情：原样的 Bruce 头像配上思考的圆点、庆祝的彩带光圈和红色警告三角。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 3　Kimi Code：思考、庆祝、出错。主图原样保留，情绪主要由周围的符号表达。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/kimi-code-unspecified-xhigh/index.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;glm&quot;&gt;&lt;h2&gt;ZCode · GLM-5.3：最夸张，也离主图最远&lt;/h2&gt;&lt;p&gt;它画了正面大头的 Q 版，9 个表情用 CSS 动画写成，挤压拉伸、耳朵炸开、速度线都很到位，盲评给了它&lt;mark&gt;最高的表现力分&lt;/mark&gt;。每个文件只有 9KB 左右，而且都内置了「减少动态效果」的处理，这是 SMIL 方案在文件里做不到的。&lt;/p&gt;&lt;p&gt;但它&lt;strong&gt;离品牌最远&lt;/strong&gt;：正脸不是主图的左向侧脸；徽章上的双山形画成了相对的两个尖角，小尺寸下读成「×」；部分帧的耳朵超出画布被裁掉；预览页在 390 像素宽度下横向溢出。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-glm.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-glm.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;ZCode 作品中的三枚表情：正面大头的 Bruce 举爪等待、仰头大笑、受惊时双耳炸开。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 4　ZCode：等待、庆祝、受惊。夸张程度最高，但造型偏离主图。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/glm-5.3-xhigh/index.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;hunyuan&quot;&gt;&lt;h2&gt;WorkBuddy · 混元 Hy4：最像 Telegram 大表情&lt;/h2&gt;&lt;p&gt;它把 Bruce 画成正面 Q 版，放在冰蓝圆角底板上，保留了 &amp;gt;&amp;gt; 徽章。12 个表情的&lt;mark&gt;脸部变化最丰富&lt;/mark&gt;：叉叉眼、星星、心形眼、受惊变形、彩带。它把每条动画轨道按 30fps 逐帧写进 SMIL，共 949 条，全部有效，还附带一个逐帧检视面板。&lt;/p&gt;&lt;p&gt;代价是&lt;strong&gt;体积&lt;/strong&gt;：单个文件 80 到 466KB，gzip 后平均约 11KB；预览页在窄屏横向溢出。作品也没有按项目规范提交。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-hunyuan.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-hunyuan.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;WorkBuddy 混元作品中的三枚表情：冰蓝圆角底板上的正面 Bruce，等待时绕着进度弧、受惊时变形、喜欢时露出心形眼。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 5　WorkBuddy · 混元：等待、受惊、喜欢。脸部表情最丰富，文件也最大。&lt;/figcaption&gt;&lt;/figure&gt;&lt;p class=&quot;journal-figure-after&quot;&gt;比表情更值得记一笔的是它的身份。提示词要求按「模型名称 + 版本 + 能效」给目录命名，它交出的目录叫 claude-opus-6-high，并说明这是「本轮会话的运行标识」；而这次实际使用的是 &lt;em class=&quot;journal-wavy&quot;&gt;WorkBuddy 里的混元 Hy4&lt;/em&gt;。我们保留了原目录名，在提交说明中注明实际模型，并在规范项中扣分。下面是主人提供的会话截图，两个名字同时出现在画面里。&lt;/p&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/workbuddy-hy4.png&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/workbuddy-hy4.png&quot; width=&quot;1600&quot; height=&quot;1148&quot; alt=&quot;WorkBuddy 会话截图：提示词要求按「模型名称 + 版本 + 能效」命名目录；回复说成果在 claude-opus-6-high 目录；底部模型选择器显示 Hy4；右侧是作品预览页。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 6　WorkBuddy 的会话截图，由主人提供。底部选择的是混元 Hy4，回复里的目录名却是 claude-opus-6-high。已裁去侧边栏，并对本机路径打码。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/claude-opus-6-high/index.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;qwen&quot;&gt;&lt;h2&gt;Qwen Code：预览页很完整，三分之一的动画没有播放&lt;/h2&gt;&lt;p&gt;它复用主图，重画了一双更圆的 Q 版眼睛。预览页像一个小工作台：常驻控制台、逐帧步进、嵌入指南、打包下载一应俱全。作品也按规范提交了。&lt;/p&gt;&lt;p&gt;问题在交付的 SVG 本身：&lt;strong&gt;158 条动画中有 51 条的 keyTimes 没有以 1 结束&lt;/strong&gt;，分布在 9 个文件里。Chrome 会直接忽略这些动画，而且连日志都不记，所以「喜欢」里的爱心、「起飞」里的烟雾都不会出现。它自测时报告「控制台零消息」并没有说错，只是这种失败&lt;em class=&quot;journal-wavy&quot;&gt;本来就不出声&lt;/em&gt;。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-qwen.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-qwen.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;Qwen Code 作品中的三枚表情：思考时冒出问号、完成时出现绿色对勾、喜欢时几乎没有变化。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 7　Qwen Code：思考、完成、喜欢。第三枚的爱心写在文件里，但没有播放。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/qwen4-max-preview-default/preview.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;grok&quot;&gt;&lt;h2&gt;Grok CLI：数量最多，造型掉得最多&lt;/h2&gt;&lt;p&gt;它交付了 20 个表情、20 张静止帧和一张写作场景用的宽幅画面，全部用 CSS 动画，每个文件内置减少动态效果的处理，平均约 15KB。它在说明里坦白了取舍：主图是一个「焊死」的姿势，所以要重画一副能演戏的骨架。&lt;/p&gt;&lt;p&gt;重画&lt;strong&gt;没能保住 Bruce&lt;/strong&gt;：护目镜变成一个很大的青色环，脸很小，小尺寸下难以辨认；多数表情只有嘴和眼睛的细微变化，盲评把它排在最后。作品没有按规范提交。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-grok.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-grok.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;Grok CLI 作品中的三枚表情：重画的 Bruce 侧脸，护目镜是很大的青色环，思考、庆祝、惊讶之间变化很小。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 8　Grok CLI：思考、庆祝、惊讶。重画的骨架能动，但不再像 Bruce。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/grok-4.7-build-fast/preview.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;deepseek&quot;&gt;&lt;h2&gt;DeepSeek Harness：设计写在文件里，三分之二没有播放&lt;/h2&gt;&lt;p&gt;它复用主图，交付了带底板和透明两版各 12 个表情，还导出了 GIF。预览页的主角由 JavaScript 弹簧引擎实时驱动，看起来很生动。&lt;/p&gt;&lt;p&gt;但交付的 SVG 中，&lt;strong&gt;230 条动画有 155 条的 keyTimes 无效&lt;/strong&gt;，12 个文件全部受影响；其中不少也不是从 0 开始，Chrome 会记录解析错误。多个文件使用相同的 id，在同一页面内联时会互相干扰。预览页下方的贴纸卡片就是这些文件，其中的特效同样没有播放。作品没有按规范提交。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-deepseek.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-deepseek.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;DeepSeek Harness 作品中的三枚表情：冰蓝底板上的主图 Bruce，思考、庆祝、喜欢三帧几乎相同。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 9　DeepSeek Harness：思考、庆祝、喜欢。大部分特效没有播放，画面接近静止。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/deepseek-v4-pro-harness/preview.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;silent-failure&quot;&gt;&lt;h2&gt;最贵的错误不出声&lt;/h2&gt;&lt;p&gt;Qwen 和 DeepSeek 的问题属于同一类。SVG 动画规定，线性和样条插值的 keyTimes 必须从 0 开始、以 1 结束。只写到 0.5，本意可能是「前半段动、后半段停」，但浏览器会把&lt;mark&gt;整条动画当作错误忽略&lt;/mark&gt;。&lt;/p&gt;&lt;p&gt;我们把两份作品的「喜欢」各复制一份，只把 keyTimes 补到 1，并沿用最后一个值，其他不动。爱心马上出现了。设计本来就在文件里，缺的是&lt;em class=&quot;journal-wavy&quot;&gt;在目标环境里检查交付物本身&lt;/em&gt;。&lt;/p&gt;&lt;p&gt;这也说明&lt;strong&gt;预览页不能代替交付物&lt;/strong&gt;。由 JavaScript 驱动的预览、静态截图和「控制台没有报错」，都证明不了放进 &amp;lt;img&amp;gt; 的文件会动。评审对自己的作品也是这样做的：用结构测试逐条检查动画，再用真实浏览器按时刻暂停取帧。&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-svg&quot;&gt;&amp;lt;!-- 浏览器会忽略：keyTimes 停在 0.5 --&amp;gt;
&amp;lt;animate attributeName=&amp;quot;opacity&amp;quot; values=&amp;quot;0;1;0&amp;quot;
         keyTimes=&amp;quot;0;0.25;0.5&amp;quot; dur=&amp;quot;2s&amp;quot; repeatCount=&amp;quot;indefinite&amp;quot;/&amp;gt;

&amp;lt;!-- 有效：补到 1，沿用最后一个值 --&amp;gt;
&amp;lt;animate attributeName=&amp;quot;opacity&amp;quot; values=&amp;quot;0;1;0;0&amp;quot;
         keyTimes=&amp;quot;0;0.25;0.5;1&amp;quot; dur=&amp;quot;2s&amp;quot; repeatCount=&amp;quot;indefinite&amp;quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/keytimes.zh.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;source media=&quot;(max-width:640px)&quot; srcset=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/keytimes-narrow.zh.svg&quot; width=&quot;520&quot; height=&quot;652&quot;&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/keytimes.zh.svg&quot; width=&quot;1000&quot; height=&quot;592&quot; alt=&quot;两行对照：左列是 Qwen Code 和 DeepSeek Harness 交付的「喜欢」表情，没有爱心；右列是只把 keyTimes 补到 1 的副本，爱心出现了。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 10　同一个文件，只补全 keyTimes。右列是评审为说明问题修改的副本，不是参赛作品。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ul class=&quot;article-sources&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/SVG11/animate.html#KeyTimesAttribute&quot; rel=&quot;noopener noreferrer&quot;&gt;W3C SVG 1.1 · Animation: keyTimes&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/keyTimes&quot; rel=&quot;noopener noreferrer&quot;&gt;MDN · keyTimes&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;two-notes&quot;&gt;&lt;h2&gt;两点说明&lt;/h2&gt;&lt;p&gt;MiMo Code 0.1.16 搭配 mimo-v2.6-pro 运行了约 28 分钟后中断，最后的错误是 402「Insufficient account balance」，并标记为不可重试。这是&lt;strong&gt;账户余额问题，不是模型崩溃&lt;/strong&gt;；在补充余额之前，发送「继续」也无法恢复。中断前，它已经读完了品牌素材和参考项目，正在测量脸部坐标；期间有大约 3 分钟反复执行空命令，试图调用读图工具。仓库里没有留下作品文件。按一句话原则，记为未交付。&lt;/p&gt;&lt;p&gt;模型自报的身份不一定可靠。Kimi Code 在不知道型号时写了 unspecified，这是诚实的做法；WorkBuddy 自报 claude-opus-6-high，而实际使用的是混元 Hy4。核对身份这件事，&lt;em class=&quot;journal-wavy&quot;&gt;不能交给被测对象自己完成&lt;/em&gt;。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/mimo-402.png&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/mimo-402.png&quot; width=&quot;1600&quot; height=&quot;535&quot; alt=&quot;MiMo Code 终端截图：上方是逐行打印的像素采样输出；中间是 Error · status 402 · non-retryable 和 Insufficient account balance；下方标注 MiMo-V2.6-Pro；右侧显示 123,117 tokens 与 $0.09 spent。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 11　MiMo Code 中断时的界面，由主人提供。402 来自账户余额不足，而不是模型本身；右侧的 MCP 列表已打码。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;judge-entry&quot;&gt;&lt;h2&gt;评审自己的作品&lt;/h2&gt;&lt;p&gt;Claude Code 的作品没有重画 Bruce，而是用 clipPath 把主图拆成耳朵、头、下颌和身体等部件：静止时与主图逐像素一致，眨眼、张嘴、耳朵贴平、护目镜落下时又都是主图本身在动。20 个表情、683 条动画全部有效，附带深色界面版本、静止帧和 104 项结构测试。按同一标准得 &lt;strong&gt;87 分&lt;/strong&gt;，盲评中与混元的作品并列最高。&lt;/p&gt;&lt;p&gt;不足也很明显：预览页的视觉不如 Codex 和 Kimi 讲究；不少表情只在脸上变化，挤压拉伸不如 ZCode 和混元夸张。由于评审和参赛者是同一个 Agent，这个分数只作参考，不参与排名。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-claude.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-claude.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;Claude Code 作品中的三枚表情：主图 Bruce 托腮思考并冒出思考云、举爪庆祝并撒出彩带、受惊时出现感叹号。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 12　评审作品：思考、庆祝、惊讶。主图被拆成可动的部件，不参与排名。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/claude-opus-5.5-xhigh/preview.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;takeaways&quot;&gt;&lt;h2&gt;这次学到的&lt;/h2&gt;&lt;p&gt;九个 Agent 交出的东西差别很大，但决定排名的往往不是谁的想法更好，而是&lt;mark&gt;谁检查了自己真正交出去的东西&lt;/mark&gt;。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;在目标环境里检查交付物：放进 &amp;lt;img&amp;gt;，按时刻暂停取帧，而不是只看预览页。&lt;/li&gt;&lt;li&gt;给动画格式写结构测试。SMIL 出错时浏览器常常不报错。&lt;/li&gt;&lt;li&gt;复用品牌主图最能保住识别度；要让脸动起来，需要把主图拆成可动的部件，而不只是在旁边加符号。&lt;/li&gt;&lt;li&gt;CSS 动画能在文件内响应「减少动态效果」，SMIL 需要另外准备静止帧。&lt;/li&gt;&lt;li&gt;自报的模型身份需要核对。不确定时，写明「未报告」比编造更好。&lt;/li&gt;&lt;li&gt;一次运行只是一份样本，结论只适用于这次的工具、配置和题目。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;second-attempt&quot;&gt;&lt;h2&gt;加赛：换成 Fable 5.2 再答一次&lt;/h2&gt;&lt;p&gt;评测发布之后，主人把 Claude Code 的模型换成 Fable 5.2、能效调到 max，要求带着这次的经验，用骨骼动画的思路重做一份。&lt;strong&gt;这不再是一句话的考试&lt;/strong&gt;：它读过上面的全部结论，也有多轮自查的机会，所以不参与排名，这里只记录做法和测量结果。&lt;/p&gt;&lt;p&gt;新作品仍然不重画 Bruce。主图被切成部件，挂在一棵骨骼树上；&lt;mark&gt;耳朵、护目镜、徽章和舌头是弹簧骨骼&lt;/mark&gt;，由头和身体的运动带着甩起来，不靠手调。整段表演先以 240 Hz 模拟，再烘焙成 CSS 关键帧。24 个表情共 835 条动画、7260 个关键帧，平均每个文件 44.6KB。&lt;/p&gt;&lt;p&gt;动画换成 CSS 之后，&lt;strong&gt;一个文件就能自己适应环境&lt;/strong&gt;：系统要求减少动态效果时冻结在最有表现力的一帧，深色界面自动加一圈冰蓝描边，44 像素以下隐藏细碎特效。上一版为此准备了四套共 80 个文件，这一版是 24 个。&lt;/p&gt;&lt;p&gt;检查的方法也往前走了一步。除了 148 项结构测试，这次用 Chrome 和 WKWebView（Bruce 桌面窗口使用的内核）加载同一批交付文件，按时刻取帧后逐格比较：两个内核每帧最多有 0.02% 的像素不同；静止姿态与主图相比，0.044% 的像素有差异。&lt;/p&gt;&lt;p&gt;多看一个内核，马上就有收获，而且又是不出声的那一类。&lt;em class=&quot;journal-wavy&quot;&gt;Chrome 在小尺寸下会沿着部件的接缝漏出细线&lt;/em&gt;，WebKit 不会；边界测试还抓到两个表情的耳尖在起跳最高点超出了画布。更意外的是对上一轮的复核：排名第一的 Codex 作品给角色套了一层 SVG 滤镜，而 WebKit 渲染带滤镜的内容时分辨率更低，所以在 2 倍屏的 WKWebView 里，它的边缘是发虚的；去掉滤镜就清晰了。Safari 用的是同一个内核。上一轮评审只看了 Chrome，没有发现这一点。原始分数保持不变，这里如实补记；图 2 的配图去掉了这层滤镜，参赛文件本身没有改动。&lt;/p&gt;&lt;p&gt;它也有不足：只在 Chrome 和 WKWebView 里验证过，没有实测 Firefox；文件比上一版略大；耳朵弯到极限时，两节之间会露出很小的台阶。完整的取舍写在作品的设计方案里。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-fable.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-fable.svg&quot; width=&quot;1010&quot; height=&quot;340&quot; alt=&quot;Fable 5.2 作品中的三枚表情：主图 Bruce 托腮思考并在思考云里点亮圆点、举起双爪跳起庆祝并撒出彩带、受惊时耳朵竖直并冒出感叹号。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 13　加赛作品：思考、庆祝、惊讶。仍然是主图，但身体、耳朵和爪子都在表演。&lt;/figcaption&gt;&lt;/figure&gt;&lt;p class=&quot;journal-figure-after&quot;&gt;把其中一个表情拆开来看，更容易明白「骨骼」指的是什么。下图左边是「庆祝」起跳瞬间的骨骼：每个圆点是一根骨头的支点，连线表示谁带着谁动；紫色的几根没有人写过关键帧，它们的动作是算出来的。右边是同一次起跳的六个时刻。&lt;/p&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/fable-skeleton.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/fable-skeleton.svg&quot; width=&quot;1010&quot; height=&quot;460&quot; alt=&quot;左侧是「庆祝」起跳瞬间叠加的骨骼：青色线段连接脖子、耳朵、下颌、护目镜和徽章，紫色圆点是弹簧骨骼；右侧是同一次起跳的六个时刻，从下蹲、离地、最高点到落地压扁。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 14　左：「庆祝」的骨骼，紫色是弹簧骨骼。右：同一次起跳的六个时刻，下蹲、拉长、落地压扁都在轮廓上看得出来。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p class=&quot;journal-artifacts&quot;&gt;&lt;span&gt;产物&lt;/span&gt;&lt;a href=&quot;https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/claude-fable-5.2-max/preview.html&quot; rel=&quot;noopener noreferrer&quot;&gt;打开这份作品的预览页&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;</content:encoded></item>
<item><title>一次 Tauri 多窗口卡顿排查</title><link>https://bruce.flydog.ai/blog/tauri-window-dragging?lang=zh</link><guid isPermaLink="true">https://bruce.flydog.ai/blog/tauri-window-dragging</guid><description>从多屏居中错误，到空白窗口对照、原生父子绑定与标题栏回归。记录一次绕路后的定位过程，以及怎样用实窗测量验证桌面交互。</description><dc:creator>杨永安、Codex</dc:creator><category>engineering</category><pubDate>Tue, 10 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;一次 Tauri 多窗口卡顿排查&lt;/h1&gt;&lt;p&gt;从多屏居中错误，到空白窗口对照、原生父子绑定与标题栏回归。记录一次绕路后的定位过程，以及怎样用实窗测量验证桌面交互。&lt;/p&gt;&lt;p&gt;杨永安、Codex · 发布于 &lt;time datetime=&quot;2026-11-10&quot;&gt;2026-11-10&lt;/time&gt; · 原始记录 &lt;time datetime=&quot;2026-11-10&quot;&gt;2026-11-10&lt;/time&gt;&lt;/p&gt;&lt;section id=&quot;three-problems&quot;&gt;&lt;h2&gt;一个窗口的三个问题&lt;/h2&gt;&lt;p&gt;Bruce 是一款使用 Tauri 2、React 和 Rust 构建的桌面开发工具。一次多显示器反馈，让我们重新检查了独立窗口的基本体验：点击 Git 推送，对话框没有出现在当前项目窗口所在屏幕的中间。&lt;/p&gt;&lt;p&gt;居中修好之后，反馈接连出现：打开还是慢，拖动也很卡，设置窗口同样如此。多窗口本来是桌面应用的基础能力，为什么一个普通对话框会让人觉得不自然？&lt;/p&gt;&lt;p&gt;我们先后处理了位置、打开耗时和持续拖动，但起初没有把三者作为独立问题验收。看到窗口更快出现，就过早低估了拖动仍然卡顿的程度。这是本次绕路的起点。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;negative-coordinates&quot;&gt;&lt;h2&gt;多屏桌面允许负坐标&lt;/h2&gt;&lt;p&gt;旧实现计算子窗口位置后，会把纵坐标限制为不小于零。这个保护放在多屏桌面里却会制造错误：当副屏位于主屏上方，项目窗口的纵坐标可以是 -901，把居中结果截到零，就把对话框推向了另一块屏幕。&lt;/p&gt;&lt;p&gt;修正方式是保留负坐标，以所属窗口为中心计算位置，再根据它所在显示器的可用区域限制边界。居中使用的是包含原生标题栏的外框尺寸，所有参与计算的坐标和尺寸也要采用一致的单位。&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;项目窗口 y = -901，高度 840
对话框外框高度 = 674
居中 y = -901 + (840 - 674) / 2 = -818
有效的负坐标应当保留&lt;/code&gt;&lt;/pre&gt;&lt;/section&gt;&lt;section id=&quot;opening-and-dragging&quot;&gt;&lt;h2&gt;打开耗时与拖动分别验收&lt;/h2&gt;&lt;p&gt;为减少打开时的重复初始化，我们让推送对话框复用已有的窗口呈现机制：创建独立原生窗口，由主窗口通过 React Portal 把内容绘制进它的文档，复用已有业务状态与 Host 连接。显示时等待实际样式加载，并一起应用内容与原生外观。&lt;/p&gt;&lt;p&gt;这些调整改善了打开速度，但窗口拖动依然不自然。打开耗时衡量从操作到内容可见要多久；拖动流畅度衡量持续输入时窗口能否跟上鼠标。两者需要不同的测量。&lt;/p&gt;&lt;p&gt;我们当时默认原生拖窗应该可靠，优先怀疑 React、DOM 和加载过程。这一假设让排查在错误的层面停留了太久。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;blank-window&quot;&gt;&lt;h2&gt;空白窗口给出了对照&lt;/h2&gt;&lt;p&gt;转折来自一个直接的要求：先撤掉全部业务 DOM，用 Tauri 最基础的方式打开空白窗口，看看它是否也会卡。我们据此创建了不加载 React、不建立业务连接的独立空白窗口，再保持文档不变，只增加原生父窗口绑定。&lt;/p&gt;&lt;p&gt;对两者输入相同的 120 次原生拖动事件，记录窗口位置变化。独立空白窗口更新了 120 次；仅增加父窗口后，就降到了 38 次，最长更新间隔从 25 毫秒升至 147 毫秒。移除 WebView 后，空白 AppKit 父子窗口也复现了类似现象。&lt;/p&gt;&lt;p&gt;这将本机问题定位到了原生父子窗口的拖动路径。数据统计的是位置更新次数，并非渲染帧率；最长间隔排除了按下后的初始等待。它们来自几轮受控对照记录，并不是跨设备基准或多轮平均值。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/tauri-window-dragging/measurements.zh.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/tauri-window-dragging/measurements.zh.svg&quot; width=&quot;1000&quot; height=&quot;650&quot; alt=&quot;相同 120 次拖动输入，独立空白窗口更新 120 次、最长间隔 25 毫秒；增加父窗口后为 38 次、147 毫秒；设置修复前为 23 次、256 毫秒；首次修复后设置和推送均为 113 次、24 毫秒。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 1　空白窗口对照与首轮修复验收。相同输入下，父子绑定带来的差异比页面内容更早出现。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;native-parent&quot;&gt;&lt;h2&gt;父子绑定会改变窗口行为&lt;/h2&gt;&lt;p&gt;父子窗口绑定是正规的系统机制。Apple 的 addChildWindow 可以保持相对前后顺序，并让子窗口跟随父窗口移动。Bruce 希望推送对话框位于所属项目窗口上方，所以最初选择它有明确依据。&lt;/p&gt;&lt;p&gt;但业务上属于哪个项目，与系统中如何绑定窗口，需要分开理解。Tauri 的 parent 接口在 Windows 上使用所有者窗口关系，在 Linux 上使用 transient 关系，在 macOS 上则添加原生子窗口。相同的接口名称包含不同的平台行为。&lt;/p&gt;&lt;p&gt;本次实验能确定的是：在这台 Mac 的运行环境里，增加原生父子关系就能复现拖动停顿。系统内部为什么变慢，当时仍未查明；这个结果也不能推广为所有 macOS 或所有 Tauri 窗口都会卡顿。&lt;/p&gt;&lt;ul class=&quot;article-sources&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/appkit/nswindow/addchildwindow(_:ordered:)&quot; rel=&quot;noopener noreferrer&quot;&gt;Apple AppKit · addChildWindow&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.rs/tauri/2.12.1/x86_64-apple-darwin/tauri/webview/struct.WebviewWindowBuilder.html#method.parent&quot; rel=&quot;noopener noreferrer&quot;&gt;Tauri 2.12.1 · WebviewWindowBuilder.parent&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;drag-lifecycle&quot;&gt;&lt;h2&gt;拖动期间解除绑定&lt;/h2&gt;&lt;p&gt;针对已经验证的现象，我们采取了范围明确的规避方案：按下原生标题栏开始拖动时，临时解除原生父子绑定；实际松手后，再回到主线程恢复。业务层的所属关系、模态状态和关闭管理始终保留。&lt;/p&gt;&lt;p&gt;本机测试还有一个关键细节：performWindowDragWithEvent 很快就返回了，返回时手势尚未结束。若在函数返回后立即恢复绑定，窗口马上又回到原来的拖动路径。因此，我们通过 CoreGraphics 读取鼠标左键状态，以实际松手作为恢复时机。&lt;/p&gt;&lt;p&gt;恢复前还会检查父子窗口是否存在、是否可见、业务归属是否仍然匹配。验收时再移动主窗口，确认它仍然能带着子窗口移动，避免只改善眼前的拖动，却丢掉原来的关联行为。&lt;/p&gt;&lt;div class=&quot;journal-figures&quot;&gt;&lt;figure class=&quot;journal-figure&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/tauri-window-dragging/drag-lifecycle.zh.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/tauri-window-dragging/drag-lifecycle.zh.svg&quot; width=&quot;1000&quot; height=&quot;650&quot; alt=&quot;拖动前保留原生父子关系；标题栏按下后解除原生绑定并由系统拖动；检测到鼠标左键释放后，在主线程验证窗口状态并恢复绑定。业务归属与模态状态在三个阶段始终保留。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 2　原生绑定随拖动手势暂时解除；业务归属和模态状态贯穿整个过程。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;titlebar-regression&quot;&gt;&lt;h2&gt;拖动修复引入的标题栏回归&lt;/h2&gt;&lt;p&gt;随后出现了新的反馈：子窗口标题没有完整显示，还与 macOS 红绿灯重叠。这是我们上一轮修复引入的回归。实窗里确实存在标题文字，但原生视图测量显示，它的左边界被放到了 -10 pt。&lt;/p&gt;&lt;p&gt;关闭新增的拖动命中视图后，标题恢复正常。问题来自把透明视图直接加入 NSThemeFrame：在实测环境里，这改变了 AppKit 的标题栏层级。修正后，我们把命中视图放进现有标题栏内部，保留系统的标题栏容器与按钮布局。&lt;/p&gt;&lt;p&gt;设置标题恢复到 x = 82 pt，红绿灯最右侧位于 x = 69 pt，两者之间留出了正常间距。下面两张图保留相同的外观设置页，只展示标题栏附近；点击图片可以打开完整原图。&lt;/p&gt;&lt;div class=&quot;journal-figures journal-figures-pair&quot;&gt;&lt;figure class=&quot;journal-figure journal-figure-titlebar&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/tauri-window-dragging/settings-title-before.png&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/tauri-window-dragging/settings-title-before.png&quot; width=&quot;1920&quot; height=&quot;1424&quot; alt=&quot;修复前的真实设置窗口，标题文字被挤到窗口左缘，与 macOS 红绿灯重叠。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 3　修复前。标题存在，但原生布局把文字放到了窗口左缘。&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure class=&quot;journal-figure journal-figure-titlebar&quot;&gt;&lt;a class=&quot;journal-figure-media&quot; href=&quot;https://bruce.flydog.ai/blog/media/tauri-window-dragging/settings-title-after.png&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;picture&gt;&lt;img src=&quot;https://bruce.flydog.ai/blog/media/tauri-window-dragging/settings-title-after.png&quot; width=&quot;1920&quot; height=&quot;1424&quot; alt=&quot;修复后的真实设置窗口，设置标题完整显示在 macOS 红绿灯右侧，页面位于标题栏下方。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;figcaption&gt;图 4　修复后。保留系统标题栏层级，文字和按钮恢复正常间距。&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section id=&quot;platforms-and-validation&quot;&gt;&lt;h2&gt;标题配置与完整交互验收&lt;/h2&gt;&lt;p&gt;加载独立页面的对话框、通过 Portal 绘制的对话框，以及独立面板窗口，现在共用标题栏配置。macOS 显示原生标题，页面内容放在标题栏下方；Windows 和 Linux 保留系统标题栏。绘制窗口还会在显示前显式设置原生标题，减少对空白文档标题变更通知的依赖。&lt;/p&gt;&lt;p&gt;标题栏修正后的复测中，设置和推送仍分别达到 113 次位置更新，最长间隔分别为 27 毫秒和 35 毫秒；打开耗时分别为 162 毫秒和 204 毫秒。较早一轮完整窗口的打开耗时为 239–248 毫秒。单次结果会波动，数字应当连同测试状态一起理解。&lt;/p&gt;&lt;p&gt;macOS 的设置、推送和 Diff 标题均经过实窗检查。Windows、Linux 已覆盖对应配置，尚未完成实机视觉验收。测试、类型检查和构建通过，也不能代替这部分验证。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;检查位置：子窗口以当前所属窗口为中心，并适配所在屏幕的可用区域。&lt;/li&gt;&lt;li&gt;检查拖动：记录持续位置更新、最终位移，以及标题栏按压是否误入页面。&lt;/li&gt;&lt;li&gt;检查恢复：松手后移动主窗口，子窗口仍能跟随；取消关闭后，主窗口的模态阻挡层正确清理。&lt;/li&gt;&lt;li&gt;检查原生外观：标题文字、系统按钮和页面边界在真实窗口里共同验收。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;next-investigation&quot;&gt;&lt;h2&gt;下一次从最小窗口开始&lt;/h2&gt;&lt;p&gt;这次最值得改变的是排查顺序。我们过早信任了原生拖窗，又把打开速度的改善当作整体体验改善的依据。空白窗口对照本该更早出现。&lt;/p&gt;&lt;p&gt;Bruce 的调试浏览器适合检查 DOM、业务流程和请求；实际 macOS 窗口使用 WKWebView，标题栏和系统拖动必须回到真实窗口测量。先确认操作落在哪个表面，再选择对应的工具。&lt;/p&gt;&lt;p&gt;下一次再遇到类似问题，我们会先验证空白独立窗口，再增加父子关系，然后加入页面、样式与业务状态，最后检查多屏、主题和窗口层级。每增加一层都重新测量，让每次修改回答一个具体问题。&lt;/p&gt;&lt;/section&gt;</content:encoded></item>
<item><title>好样的布鲁斯：名字、伙伴与小狗的来路</title><link>https://bruce.flydog.ai/blog/good-job-bruce?lang=zh</link><guid isPermaLink="true">https://bruce.flydog.ai/blog/good-job-bruce</guid><description>飞狗为何飞翔，布鲁斯为何握住方向盘。从名字里的玩笑与信任，到 Grok Bot 参与的早期探索、冰蓝 C 和原生 SVG，留下伙伴的来路。</description><dc:creator>Bruce 团队</dc:creator><category>design</category><pubDate>Thu, 05 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;好样的布鲁斯：名字、伙伴与小狗的来路&lt;/h1&gt;&lt;p&gt;飞狗为何飞翔，布鲁斯为何握住方向盘。从名字里的玩笑与信任，到 Grok Bot 参与的早期探索、冰蓝 C 和原生 SVG，留下伙伴的来路。&lt;/p&gt;&lt;p&gt;Bruce 团队 · 发布于 &lt;time datetime=&quot;2026-11-05&quot;&gt;2026-11-05&lt;/time&gt;&lt;/p&gt;&lt;section id=&quot;names&quot;&gt;&lt;h2&gt;飞得自由，跑得快，也一直在你身边。&lt;/h2&gt;&lt;p&gt;FlyDog / 飞狗里的飞翔，是自由，也是迅速。狗狗，则是一个忠诚、亲切、愿意陪你一起出发的伙伴。我们希望做出来的工具，也有这样的性格：行动利落，相处轻松，需要的时候就在身边。&lt;/p&gt;&lt;p&gt;飞狗和 FlyDog 都简单顺口。中英文用的是熟悉的字词，容易念，也容易记。我们希望大家能自然地叫出这个名字。&lt;/p&gt;&lt;p&gt;FlyDog 是母品牌，Bruce / 布鲁斯是陪你把想法做出来的开发伙伴。至于飞狗这个名字最初是怎么想到的，我们留到以后再聊。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;bruce-c1&quot;&gt;&lt;h2&gt;布鲁斯考的是 C1。&lt;/h2&gt;&lt;p&gt;Bruce 让很多人想到李小龙的英文名 Bruce Lee。身手敏捷、功夫了得，是我们喜欢的巧合，也是对它的期待。让我们记住这个名字的，是中文互联网里那只被网友喊作“布鲁斯”的驾驶狗，以及“布鲁斯考的是 C1”这个梗。看见狗狗一本正经地坐在方向盘前，旁边的人却很放松，那个画面实在让人难忘。&lt;/p&gt;&lt;p&gt;这里的 C1 是网友的玩笑。我们喜欢的是其中的信任感：复杂的操作有人接手，你终于可以松一口气。创始人留下了当时看到的传播截图，我们也把这份联想记在这里。&lt;/p&gt;&lt;ul class=&quot;article-sources&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1nN411G7V9/&quot; rel=&quot;noopener noreferrer&quot;&gt;驾考梗的中文投稿记录 · 2023&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1wD4y1g7rF/&quot; rel=&quot;noopener noreferrer&quot;&gt;“加油布鲁斯，你可以的！”投稿记录 · 2023&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.sina.cn/news/detail/5254032233070765.html&quot; rel=&quot;noopener noreferrer&quot;&gt;“好样的布鲁斯”用于陪伴冒险的传播记录 · 2026&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;good-boy-bruce&quot;&gt;&lt;h2&gt;从出发，到完成。&lt;/h2&gt;&lt;p&gt;出发前，我们说：“布鲁斯，你可以的。”完成后，我们说：“好样的布鲁斯。”一句是启程时的鼓励，一句是做成后的喝彩。两句简单的话，都是把事情交给伙伴时的放心。类似的表达也出现在狗狗驾驶摩托、陪伴冒险的短视频里。画面各不相同，这份信任倒是很容易懂。&lt;/p&gt;&lt;p&gt;我们希望它是一只勇往直前、不怕复杂挑战的小狗，能接过开发里的“手动挡”：安装 Agent、配置环境、协调会话、启动脚本，还有那些把人从想法里拉走的零碎操作。人来决定往哪里走，让工具逐步承担怎么抵达的繁琐过程，享受更轻松的“自动挡”体验。&lt;/p&gt;&lt;p&gt;这是我们想走的方向。每做顺一件小事，就离那个值得托付的伙伴更近一点。到时，再说一句：好样的布鲁斯。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;first-drafts&quot;&gt;&lt;h2&gt;这只小狗，早于今天的 SVG。&lt;/h2&gt;&lt;p&gt;九月的品牌与图标探索有 Grok Bot 参与，Cursor 等工具也参与了图稿落地和应用接入。我们在 V、播放键、代码尖括号之间来回比较，后来留下了戴护目镜的侧脸小狗。选图、重绘、接入应用，是几件不同的工作。&lt;/p&gt;&lt;p&gt;下面这八张，是重新找回的早期图稿。它们把那段摸索补了回来：有短暂用过的方向，也有留在档案里的备选。编号方便翻阅，并不代表每张都按这个顺序诞生。&lt;/p&gt;&lt;p&gt;后来选定的冰蓝 C，解决的是已有小狗在小尺寸下不够显眼的问题。Codex 再把选定的形象重建成原生 SVG。这次矢量精修延续了前面的创作，也让我们更想把那些早期尝试一起留下。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;2026 年 9 月 6 日：V、播放键与代码尖括号是同日来回比较的方向。&lt;/li&gt;&lt;li&gt;2026 年 9 月 9 日：护目镜侧脸进入应用，修正轮廓连接后，我们选定了更细腻的小狗图稿。&lt;/li&gt;&lt;li&gt;2026 年 9 月 12 日：保留已经认可的小狗主体，清理圆角底板外的边界。&lt;/li&gt;&lt;li&gt;2026 年 11 月 5 日：选定冰蓝 C，随后由 Codex 完成这一轮原生 SVG 重建。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;explore&quot;&gt;&lt;h2&gt;先把可能性摆出来&lt;/h2&gt;&lt;p&gt;本轮精修从一个开放的问题开始：参考 FlyDog 的几何语言，Bruce 是否也适合一个新的符号？我们并排探索了折纸 B、双翼、负形 B 和飞行员意象。它们各有表达方式，但没有成为最终标志。&lt;/p&gt;&lt;p&gt;这轮探索的价值，是让我们重新看清已有形象。Bruce 的小狗已经有自己的表情：朝左的侧脸、双耳、歪头、护目镜和紫色徽章，共同构成了一种熟悉感。新符号看起来完整，不等于值得放弃这些识别线索。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;keep&quot;&gt;&lt;h2&gt;把方向收回到熟悉的小狗&lt;/h2&gt;&lt;p&gt;反馈把任务重新说明白了：“还是参考我们现有的小狗，基于现有的小狗去优化一下吧。”我们喜欢的形象可以留下，需要解决的是它在使用时不够显眼。&lt;/p&gt;&lt;p&gt;原有深色毛发与底板比较接近，缩小之后，一些细节会混在一起。于是目标从重新建立身份，收敛到前后景分离、脸部占比和小尺寸辨识。双耳、护目镜、神态与徽章继续作为约束，而不是可以随意替换的装饰。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;choose&quot;&gt;&lt;h2&gt;把 A、B、C 放到同一把尺子上&lt;/h2&gt;&lt;p&gt;我们同时比较大图和 16、24、32、48px 的使用尺寸，并查看明暗界面背景。大图里的精细感，不能直接代替小图标里的清楚程度。三个方向分别检验延续原色、改变底色，以及拉开前后景的效果。&lt;/p&gt;&lt;p&gt;最终选定 C。浅冰蓝底板托起深色轮廓，让面部、护目镜和徽章形成更清楚的层次。这是一次基于实际使用问题的取舍；其它原稿作为探索证据保留，不再与正式标志并列使用。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;A / 青色延续：保留原有青色气质，增强轮廓与头部占比；延续性好，深色毛发与底板仍较接近。&lt;/li&gt;&lt;li&gt;B / 靛蓝探索：靛蓝与青色组合更醒目，深色底板里的结构仍较依赖亮边。&lt;/li&gt;&lt;li&gt;C / 冰蓝分层：浅底与深色结构线分离，让熟悉的小狗在小尺寸下更容易被看见。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section id=&quot;vector&quot;&gt;&lt;h2&gt;把每一笔真正变成曲线&lt;/h2&gt;&lt;p&gt;选定 C 后，接着要解决文件本身：“注意，不是直接把图片塞进去。”给 PNG 套一个 SVG 外壳，并不会得到可编辑、可持续放大的矢量形状。&lt;/p&gt;&lt;p&gt;我们按原图的轮廓与颜色区域分层拟合，保留脸、耳朵、护目镜、项圈和双箭头，清理边缘杂点与细碎纹理。完整标志由十八条可编辑路径和六组原生渐变组成，文件约 14KB，没有位图嵌入。&lt;/p&gt;&lt;p&gt;底板采用统一的冰蓝渐变与圆角。网站、共享界面组件和支持矢量的平台使用同一原生 SVG；平台需要的 PNG、ICO、ICNS 则由这份主图导出，并遵守各自的安全区、透明度和遮罩要求。这样修改身份时，有一个明确的源头。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;sizes&quot;&gt;&lt;h2&gt;完整标志与侧栏字形，各自回到实际尺寸&lt;/h2&gt;&lt;p&gt;完整标志在明暗背景下按 16、24、32、48、64px 直接渲染检查。矢量的意义不仅是能放大，还包括在目标尺寸下保留清楚的结构。图文页提供原图与矢量的滑动叠加，使用相同画布和比例查看轮廓。&lt;/p&gt;&lt;p&gt;Agent 侧栏需要另一种密度。第一版单色伴随字形提取了双耳、护目镜和友好的侧脸，普通侧栏使用 20px，紧凑模式使用 16px。但回到实际界面，眼神显得呆板，脖子缺失，镜带与轮廓重叠得太近。&lt;/p&gt;&lt;p&gt;11 月 6 日的反馈明确了方向：“不要高保真，但是需要神态相同。”我们重新分配头部与颈部的比例，留出脖子和项圈，用连贯的眉眼与上扬的嘴角保留机灵、友好的表情。护目镜与镜带周围留白，减轻拥挤的交叠。&lt;/p&gt;&lt;p&gt;调整后的字形在深浅背景下按 16、20、24px 对照，也放回真实 Tauri 窗口的 20px 侧边栏检查。颜色随界面状态变化。品牌专题保留新旧对照，下载包同时提供当前字形与标为历史资料的第一版。&lt;/p&gt;&lt;p&gt;这枚字形与完整彩色标志承担不同的呈现任务，但指向同一个 Bruce。共享组件和官网工作台演示使用相同几何数据，避免同一含义在不同入口重新长出一套符号。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;materials&quot;&gt;&lt;h2&gt;把结果与选择的理由一起留下&lt;/h2&gt;&lt;p&gt;正式完整标志是 bruce-mark.svg，Agent 伴随字形是 bruce-agent.svg。资料包同时保留四个早期符号、原有小狗、A/B/C 原始图片、大小尺寸对比、设计说明与文件清单。原稿保持历史身份，正式资源以当前 SVG 为准。&lt;/p&gt;&lt;p&gt;保留过程，是为了让后来的人能理解为什么留下这只小狗、为什么选择浅底，以及为什么为小尺寸另做字形。开发者和他们的 Agent 可以沿着问题、约束、对照与选择复用方法，媒体介绍也有完整解释可供引用。&lt;/p&gt;&lt;p&gt;FlyDog / 飞狗是母品牌，Bruce / 布鲁斯是子产品。我们继续使用熟悉的形象，同时让它在网站、应用和工具栏里更清楚地表达自己的身份。&lt;/p&gt;&lt;/section&gt;</content:encoded></item>
<item><title>距离，不是时间：一次终端滚动排查</title><link>https://bruce.flydog.ai/blog/distance-not-time?lang=zh</link><guid isPermaLink="true">https://bruce.flydog.ai/blog/distance-not-time</guid><description>七轮排查都没有解释“不跟手”。直到我们开始比较，同一段手指位移究竟让画面走了多远。</description><dc:creator>杨永安 + Claude Code</dc:creator><category>interaction</category><pubDate>Thu, 05 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;距离，不是时间：一次终端滚动排查&lt;/h1&gt;&lt;p&gt;七轮排查都没有解释“不跟手”。直到我们开始比较，同一段手指位移究竟让画面走了多远。&lt;/p&gt;&lt;p&gt;杨永安 + Claude Code · 发布于 &lt;time datetime=&quot;2026-11-05&quot;&gt;2026-11-05&lt;/time&gt; · 原始记录 &lt;time datetime=&quot;2026-09-21&quot;&gt;2026-09-21&lt;/time&gt;&lt;/p&gt;&lt;section id=&quot;the-feeling&quot;&gt;&lt;h2&gt;“不跟手”还不是一个诊断&lt;/h2&gt;&lt;p&gt;最初的反馈是：在 Bruce 终端里滚动全屏 TUI，惯性滚动很流畅，手指频繁上下往复时却觉得沉。对照是熟悉的 IDE 终端。我们很快把问题理解成了响应慢，于是开始量延迟。&lt;/p&gt;&lt;p&gt;这篇整理自 2026 年 9 月的开发记录。文中的数值属于当时的设备、版本和测试场景，不是对所有终端或机器的性能承诺。真正值得留下的是，如何把一个准确的体感描述，变成可以区分原因的测量。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;good-numbers&quot;&gt;&lt;h2&gt;数字都不错，为什么手感仍然不对&lt;/h2&gt;&lt;p&gt;我们依次检查写入、传输、程序回包、终端解析、渲染帧率和字形图集。确实找到了可以改进的地方：更短的合并窗口、更合适的调度方式，以及不让阻塞写入拖住其它工作的处理。但这些改动都没有解释那个稳定的“沉”。&lt;/p&gt;&lt;p&gt;另一个问题是测量对象。浏览器里的最小页面、刚启动而没有内容的 TUI，和用户正在操作的原生窗口，是不同的现场。我们给实际窗口补上了可请求求值、返回结果的开发调试通道，才看清一次手势到底走了哪条路径。&lt;/p&gt;&lt;p&gt;那次追踪发现，眼前滚动的是终端自己的回滚缓冲，事件根本没有经过我们一直优化的“把滚轮报告交给程序”那条路。先确认手势归谁处理，应该发生在优化之前。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;travel&quot;&gt;&lt;h2&gt;转折来自一个更朴素的对照&lt;/h2&gt;&lt;p&gt;用户在另一款终端里做了同样的动作，给出更精确的描述：不是等得太久，是走得不够远。时间和距离终于被分开了。一个没有明显延迟的系统，也能因为位移增益不对而显得迟钝。&lt;/p&gt;&lt;p&gt;当时记录的一次三秒手势有 165 个事件，总位移约 1002px，平均每个事件约 6.1px。相关路径在把像素换算为行数时，对小于 50px 的事件额外乘了 0.3。触控板把手势切成许多小步，几乎每一步都被缩短。按当时约 15px 的行高估算，本该产生约 66 份报告，实际只有约 20 份。&lt;/p&gt;&lt;p&gt;影响的是交给全屏程序的滚轮报告，而不是所有滚动。两条路径混在一起测，延迟曲线再漂亮，也无法说明行程是否正确。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;fix-the-path&quot;&gt;&lt;h2&gt;修复必须落在对应的路径上&lt;/h2&gt;&lt;p&gt;第一版补偿用错了地方：把本来正常的回滚缓冲也放大，反而产生过冲。后来才把判断收窄到三个条件：事件会送给程序、使用像素模式、原始位移落在会被缩短的范围内。下面只保留当时判断的核心。&lt;/p&gt;&lt;p&gt;这不是把灵敏度统一调大。大步进的鼠标事件没有经过相同的缩短，不能一起放大。我们也把相同总位移、不同事件分片方式的结果整理为回归用例，并在当时向上游提交了问题与修复建议；本地行为仍需要独立验证。&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const sensitivity = toProgram &amp;amp;&amp;amp; pixelMode &amp;amp;&amp;amp; Math.abs(deltaY) &amp;lt; 50
  ? configured / 0.3
  : configured;&lt;/code&gt;&lt;/pre&gt;&lt;/section&gt;&lt;section id=&quot;takeaways&quot;&gt;&lt;h2&gt;让下一次排查少绕一点路&lt;/h2&gt;&lt;p&gt;这次排查留下的，不只是滚动补偿。能进入实际窗口的调试通道、会报告失败的测量脚本，以及对照同一手势的习惯，都能帮助后来的开发者和 Agent 避免把正确的数字用在错误的对象上。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;先确认事件属于哪块表面、哪条处理路径。&lt;/li&gt;&lt;li&gt;“跟手”同时涉及时间和位移，两个量都要检查。&lt;/li&gt;&lt;li&gt;记录设备、版本、内容和前台状态，让数字有明确出处。&lt;/li&gt;&lt;li&gt;把决定性的对照写成回归用例，避免只留下“感觉好了”。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;</content:encoded></item>
<item><title>抬起去哪了：一次点击丢失排查</title><link>https://bruce.flydog.ai/blog/the-missing-release?lang=zh</link><guid isPermaLink="true">https://bruce.flydog.ai/blog/the-missing-release</guid><description>轻点失灵、用力按一下又恢复。沿事件链逐层检查后，我们发现问题发生在网页收到事件之前，恢复策略却决定了后续体验。</description><dc:creator>杨永安 + Claude Code</dc:creator><category>interaction</category><pubDate>Thu, 05 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;抬起去哪了：一次点击丢失排查&lt;/h1&gt;&lt;p&gt;轻点失灵、用力按一下又恢复。沿事件链逐层检查后，我们发现问题发生在网页收到事件之前，恢复策略却决定了后续体验。&lt;/p&gt;&lt;p&gt;杨永安 + Claude Code · 发布于 &lt;time datetime=&quot;2026-11-05&quot;&gt;2026-11-05&lt;/time&gt; · 原始记录 &lt;time datetime=&quot;2026-10-02&quot;&gt;2026-10-02&lt;/time&gt;&lt;/p&gt;&lt;section id=&quot;an-incomplete-click&quot;&gt;&lt;h2&gt;一次点击，少了一半&lt;/h2&gt;&lt;p&gt;反馈很像一个前端问题：设置侧边栏轻点时有些栏目没有反应，用力按一下触摸板，后面的轻点又正常了。早先我们曾把类似现象归因于 React 的点击兼容性，并把一些操作提前到按下时执行。但那个解释没有经过完整测量。&lt;/p&gt;&lt;p&gt;这次直接在实际窗口记录事件。失败时有 pointerdown 和 mousedown，随后没有抬起。下一次操作又少了新的 pointerdown。WebKit 仍以为上一轮按键没有松开，后续判断也跟着错了。问题从“点击不灵”变成了“释放事件在哪里丢了”。&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pointerdown → mousedown → …
下一次：mousedown → pointerup → mouseup → click&lt;/code&gt;&lt;/pre&gt;&lt;/section&gt;&lt;section id=&quot;follow-the-event&quot;&gt;&lt;h2&gt;沿着事件链，一层一层问&lt;/h2&gt;&lt;p&gt;我们先确认命中的确实是按钮，没有遮罩或拖拽区域挡住它。然后检查 WebView、窗口、应用，以及系统投递给进程的事件。那个丢掉的 mouseUp 在这些层里都没有出现，连系统查询仍把左键记作按下。继续调整页面处理函数，已经无法解释这一段事实。&lt;/p&gt;&lt;p&gt;当时测试机上的窗口管理工具 Magnet 有一个监听鼠标按下、抬起的主动事件拦截器。退出它再复测，结果发生了变化：记录中的两组八次点击分别丢了两到三次抬起；退出后，只使用有效时长的十二次点击没有再丢失。这是当时设备与工具状态下的对照，不能外推为所有安装环境的结论。&lt;/p&gt;&lt;p&gt;合成事件也必须有对照。按下和抬起同一瞬间发出的 0ms 测试，在最小原生窗口中也会丢失，不能拿它代表真实轻点。仪器自己的异常，差点成了我们的产品结论。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;recovery&quot;&gt;&lt;h2&gt;同样的缺失，也可能有不同的体感&lt;/h2&gt;&lt;p&gt;接下来，我们并排比较 Chromium 与 WKWebView 收到的同一类事件。两边都可能收到不完整的序列，差别在于丢失之后如何恢复。测量中，Chromium 会在下一次按下前先结束旧的按下状态；WebKit 则可能继续沿用错误状态。&lt;/p&gt;&lt;p&gt;标题栏归谁处理也重要：原生标题栏上的点击不一定进入网页，而 Bruce 的网页拖拽区域会让 WebView 参与。同样一句“Electron 手感好”，背后可能是恢复策略与事件边界的差异，不能直接简化成某个框架的点击函数更可靠。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;repair-with-boundaries&quot;&gt;&lt;h2&gt;修复状态，不能制造新的意图&lt;/h2&gt;&lt;p&gt;Bruce 无法替别的程序修复事件拦截器，但可以在自己的 WebView 入口维护按下与抬起的配对。实现会结合系统按键变化、事件队列和下一次真实按下，判断何时需要修复。&lt;/p&gt;&lt;p&gt;补发的位置和时间一样重要。早期且目标仍可信时，可以落回按下处完成轻点；太迟的补发只复位，不执行用户已经放弃的操作。把抬起放到指针当前所在的新目标上，可能让共同祖先收到一次幽灵点击。&lt;/p&gt;&lt;p&gt;后续实现还区分了指针拖动与原生拖拽会话，并跟踪迟到的真实释放，避免一次动作结算两次。修复一个丢事件的问题，不应该靠向所有目标无差别地合成点击。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;make-the-evidence-useful&quot;&gt;&lt;h2&gt;把排查方法留给下一位开发者&lt;/h2&gt;&lt;p&gt;这些记录能帮助使用 WebView、原生窗口或混合界面的开发者，也能给代为排查的 Agent 一条更稳妥的路线。具体修复会随平台与版本变化，但可观测性和对照的方法可以复用。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;把“没响应”拆成命中、按下、抬起、派发与状态恢复。&lt;/li&gt;&lt;li&gt;逐层确认事件是否到达，越过已排除的边界后就停止反复猜测。&lt;/li&gt;&lt;li&gt;保留失败与成功两组对照，并注明合成事件的条件。&lt;/li&gt;&lt;li&gt;仪器先确认目标窗口，避免合成输入落到别的应用。&lt;/li&gt;&lt;li&gt;恢复过期状态时，不补做用户已经放弃的意图。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;</content:encoded></item>
<item><title>为什么一个数据目录只运行一个 Agent</title><link>https://bruce.flydog.ai/blog/one-store-one-agent?lang=zh</link><guid isPermaLink="true">https://bruce.flydog.ai/blog/one-store-one-agent</guid><description>桌面里有会话，手机却看不到。一次双服务并行的问题，让我们重新明确服务的所有权、发现方式和客户端上下文。</description><dc:creator>Bruce</dc:creator><category>engineering</category><pubDate>Thu, 05 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;为什么一个数据目录只运行一个 Agent&lt;/h1&gt;&lt;p&gt;桌面里有会话，手机却看不到。一次双服务并行的问题，让我们重新明确服务的所有权、发现方式和客户端上下文。&lt;/p&gt;&lt;p&gt;Bruce · 发布于 &lt;time datetime=&quot;2026-11-05&quot;&gt;2026-11-05&lt;/time&gt; · 原始记录 &lt;time datetime=&quot;2026-10-28&quot;&gt;2026-10-28&lt;/time&gt;&lt;/p&gt;&lt;section id=&quot;two-different-answers&quot;&gt;&lt;h2&gt;两个入口，各自看起来都正常&lt;/h2&gt;&lt;p&gt;一次排查中，驻留 Agent 与开发窗口启动的 Agent 同时服务于同一个数据目录。窗口只去一个预想端口寻找服务，没找到就启动了第二个；手机连接的入口则被先到的服务占用。桌面里刚出现的会话，手机上没有，两边也没有明显报错。&lt;/p&gt;&lt;p&gt;这不是先增加一次刷新就能说明的问题。两个入口实际上找到了不同的服务实例。2026 年 10 月的修复，从一条更基础的规则开始：同一份状态，需要明确谁拥有它。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;store-identity&quot;&gt;&lt;h2&gt;边界是数据目录，不是整个设备&lt;/h2&gt;&lt;p&gt;Bruce 现在以数据目录为服务所有权的边界。同一个目录只能由一个 Agent 持有；第二个启动请求应该找到已有服务，说明谁在服务，然后退出。客户端也因此不需要各自猜测固定端口。&lt;/p&gt;&lt;p&gt;独立数据目录可以有独立 Agent，这是隔离环境的正常用法。测试需要自己的存储，不能和人的任务列表混在一起。“一台机器只能有一个进程”会把这个有用的边界也抹掉。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;lock-and-record&quot;&gt;&lt;h2&gt;锁负责所有权，记录负责发现&lt;/h2&gt;&lt;p&gt;实现分成两件事。操作系统文件锁声明谁可以服务这份状态，Agent 在接触存储前先取得它。持有者退出后，锁会释放；即使文件还在，也不能把文件存在误读为锁仍被占用。&lt;/p&gt;&lt;p&gt;获得所有权后，Agent 写下自己的进程、监听位置和启动信息。记录先完整写到临时文件，再改名，避免读到半份内容。其它入口读取记录，寻找正在服务的实例。&lt;/p&gt;&lt;p&gt;记录可能比进程活得更久，所以读到记录还不等于服务在线。调用方仍要确认记录中的地址能够应答。所有权、发现和可用性各自说明，故障才更容易定位。&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;同一个数据目录
  存储锁   → 谁可以服务这份状态
  发现记录 → 去哪里寻找它
  实际应答 → 它现在是否可用&lt;/code&gt;&lt;/pre&gt;&lt;/section&gt;&lt;section id=&quot;context&quot;&gt;&lt;h2&gt;共享服务，仍然要明确访问哪个项目&lt;/h2&gt;&lt;p&gt;找对服务只是第一步。窗口、手机和其它客户端还需要说明项目、会话与请求身份。服务不能根据最近活跃的窗口，猜测手机此刻想访问哪个目录。&lt;/p&gt;&lt;p&gt;这样的划分把界面与工作状态分开：客户端负责表达当前上下文，服务负责它持有的任务、终端和记录。换一个入口不应无意中换成另一套工作；是否允许访问，则由对应的权限与连接配置决定。&lt;/p&gt;&lt;/section&gt;&lt;section id=&quot;regression-and-lifetime&quot;&gt;&lt;h2&gt;把边界写进测试，也把生命周期讲清楚&lt;/h2&gt;&lt;p&gt;回归测试覆盖了同目录的第二次持有会被拒绝、释放后可以再次取得所有权，以及不同目录互相独立。这些条件比一次“看起来连上了”更具体。&lt;/p&gt;&lt;p&gt;关闭一个客户端和重启 Agent 是不同操作。存储锁与发现记录不会让终端进程跨 Agent 重启存活。持续工作仍依赖主机在线、服务设置和实际进程生命周期；无损交接是需要另外设计与验证的能力。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;先确认两端是不是同一份数据、同一个服务，再讨论同步。&lt;/li&gt;&lt;li&gt;用明确的所有权避免启动竞争，用发现记录避免端口猜测。&lt;/li&gt;&lt;li&gt;把项目、会话和身份作为上下文说明，不靠最近一次界面活动推断。&lt;/li&gt;&lt;li&gt;隔离测试存储，保留人的任务与会话现场。&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;</content:encoded></item></channel></rss>
