DuckTerm Field Notes

A 21h 54m goal-driven engineering story

把 iOS 独生子,养成 Android 三好生

一次近乎全自动的跨平台适配实验:从 Ghostty 黑屏、Mosh 失语,到 Chrome 权限侦探、Google Play 上架、RevenueCat 商品闭环,以及三台设备互相“作证”。

  • Goal-driven
  • iOS → Android
  • Ghostty + Mosh
  • Play + RevenueCat
  • Evidence first
从 iOS 终端跨桥到 Android 终端 左侧手机与右侧平板通过一座由代码、测试和证据构成的桥连接。 iOS / Ghostty $ ssh duck@host PARITY_READY scroll · ime · tui Android / Ghostty $ mosh duck@host SAME_SESSION_OK pixelcopy · pip · pad GOAL fail-closed first 403 ≠ 世界末日 evidence or it didn’t happen
21h54m逻辑 Goal,用时不是单进程 uptime
4,441工具调用形态,不是独立测试数
3API35、Pixel API30、Xiaomi Pad
30/30最终 connected suite 快照,不与旧快照相加
259Goal 窗口内的视觉检查调用

事情起于一个看起来很轻的要求:iOS 已经有的那些好东西,Android 也该有吧?如果把这句话交给传统项目管理,它会长成一排 Jira;交给这次 Goal,它长成了 Ghostty 原生终端、Mosh 真 UDP、Pad 大屏、PiP、Widget、通知、Google Play 与 RevenueCat——外加一只逐渐学会看截图、开浏览器、查 403、还会怀疑自己证据的鸭子。

01 / THE CONTRACT

Android 不是 iOS 穿了件绿色外套

所谓“从 iOS 适配到 Android”,最容易做错的方式,是照着页面截图逐像素复刻。真正需要迁移的是产品契约:用户输入一个中文字符只能到远端一次;renderer 失败不能断 SSH;后台回来要看到新 frame;订阅恢复要认得当前 plan;Pad 不是一台被横着放大的手机。

于是每项能力都被翻译成三层:共享语义、平台实现、证据边界。iOS 可以用自己的 viewport primitive,Android 可以通过 Ghostty ABI 3;但“手指向下浏览更旧历史、点一下回到 live”必须一致。

iOS 已有能力

共享产品契约

Android 原生实现

平台差异策略

三设备与真实服务验证

证据通过?

定位根因并缩小声明

小批复审与原子提交

iOS 已有能力共享产品契约 Android 原生实现平台差异策略 三设备与真实服务验证证据通过? 定位根因并缩小声明 小批复审与原子提交 失败回到验证环,不以“看起来差不多”放行通过才进入小批复审与原子提交
图 1:不是复制实现,而是迁移契约。失败会回到验证环,而不是用“看起来差不多”放行。
⌨️

输入契约

IME composition 留在本地,commit exactly once;物理键、Ctrl、Alt、sticky modifier 都不能串台。

🖼️

渲染契约

wide cell、selection、snapshot、scrollback 与 cursor 必须在真实 frame 上连贯。

🛟

失败契约

GL 失败切 xterm,但 SSH transport、ring history 和用户输入继续活着。

💳

商业契约

月付、年付、终身在 Play 和 RevenueCat 中映射一致,缺配置时宁可不卖也不猜 SKU。

跨平台不是“长得一样”,而是“出事时也守同一份承诺”。

02 / THE NATIVE LINE

先让 Ghostty 失败,再允许它成功

Android Ghostty 的第一原则有点反直觉:一开始绝不乐观。同步常量永远先给 `ok=false`,异步自测必须证明 native core、ABI、VT、EGL、shader、texture upload、draw 和 readback 都成立,才从 xterm 切到 Ghostty。

这避免了首帧“先黑一下再说”的经典移动端玄学。健康证明还有 2.5 秒上限:GPU 真要进入沉思,用户最多看 2.5 秒 spinner,然后得到一台能工作的 xterm,而不是一块禅意十足的永久空白。

health proof succeedsbefore timeouttimeout or proof failsruntime GL failuresame transport and ringreplaynative renderer active

Pending

Ghostty

Xterm

StableSession

Native VT plus full GL path

图 2:renderer 是可替换表面,不是 transport。即便 GL 在运行中失败,session 仍继续。
GHOSTTY_HEALTH
core.ok=true
gl.maxRgbDelta=2

$ printf '中文 😀 é'
中文 😀 é

same session
FORCED_GL_FAIL
renderer unhealthy
→ xterm fallback

$ echo STILL_ALIVE
STILL_ALIVE

connect count: 1
MOSH_ROAM
Wi-Fi off
DURING_OK
Wi-Fi on
AFTER_OK

same PID · same UDP

终端胶片为文章复原示意,不是运营后台原图;marker、结果与边界来自真实设备证据。

那些“顺手”一起补齐的硬骨头

输入法 composition、xterm 键序、触摸 selection、ActionMode copy、native scrollback、background compositor、PixelCopy probe、CPU snapshot、Pad keyboard focus mode……每一项单看都像 P3,叠在一起才决定“这是一台终端,还是一张会闪的黑色矩形”。

03 / THE BROWSER DETECTIVE

浏览器里有权限,但没有一把现成的钥匙

Play Console 的权限躺在已经登录的 Chrome profile 里;内置 Playwright 是干净 profile,什么也看不到;CDP 还没开。最省事的回答是“请用户手工操作”。Goal 选择了另一条路:先找出哪个浏览器上下文有权限,再把用户参与压缩成一次必要 consent。

浏览器权限侦探示意图 Agent 通过截图识别 Chrome consent,用户授权后 CDP 接管页面,随后连接 gcloud、Play API 和 RevenueCat。 chrome:// remote debugging 允许远程调试? 用户允许 CONTROL PLANE ✓ CDP snapshot + click ✓ gcloud API enablement ✓ Play publisher SA ✓ RevenueCat v2 secrets stay outside repo snapshot then click
图 3:CDP 控页面,不能控制自己的授权弹窗。截图与 OS 自动化负责到达 consent,用户负责信任。

没有现成桥,就在临时目录里生成一个:MCP SDK + `chrome-devtools-mcp` + 薄 stdio client,再用持久 PTY 连续发 `snapshot → click → snapshot`。它没有被包装成“平台能力”,完成后被清理;真正可复用的结论是专用 profile、origin allowlist、固定版本和默认脱敏。

一句实用笑话:403 不一定是 Google 不爱你。它可能是 OAuth scope、GCP IAM、Play App 权限、Billing permission,或者权限还在路上。先分层,再重试;别拿刷新按钮当念珠。
RevenueCat APIPlay Publisher APIgcloudChromeGoal AgentRevenueCat APIPlay Publisher APIgcloudChromeGoal Agent用户发现已有登录态和目标账号上下文明确允许 remote debuggingCDP 页面控制可用启用 API 并创建两套最小权限身份在 Play Console 绑定应用权限幂等创建 catalog 并上传 internal AAB创建 Android app 与商品映射track 与 artifact 状态entitlement 与 offering 状态只请求业务事实和不可逆发布决策用户
图 4:浏览器、Cloud IAM、Play 权限和 RevenueCat 是四个控制面。把它们混成一个“登录成功”只会收获更多 403。
04 / THE EVIDENCE LAB

截图黑了,先别给 GPU 写悼词

Android 真机验证最危险的时刻,是第一张截图看起来非常有说服力。SurfaceView 可能在系统截图里全黑;Xiaomi screencap 可能是 0 字节;screenrecord 可能只录到没有硬件层的世界。反过来,远端 tmux 看到了 marker,也只证明 transport 活着,不能证明本地 compositor 画出来了。

三设备证据实验室 API35 模拟器、Pixel 4 和 Xiaomi Pad 分别承担新系统、旧系统真机和 OEM 大屏验证。 API35 emulator 新 API · contract Pixel 4 API30 physical IME · FCM · SQLite Xiaomi Pad API33 · MIUI master detail Pad · Surface · rotation
图 5:三台设备不是同一测试乘三,而是三个互补假设:新 API、旧真机、OEM 大屏。

于是证据被设计成多 oracle:唯一 marker、UI tree、PixelCopy、CPU raster、PID、dumpsys、logcat、远端 pane。它们必须互相证伪,而不是排队鼓掌。

注入唯一 marker

UI tree 与控件状态

Screenshot 或 PixelCopy

PID · dumpsys · logcat

远端 SSH/Mosh/tmux 状态

多个 oracle 一致?

分类测试工件

更换 nonce 与 oracle 重跑

写下有边界的结论

图 6:证据不是越多越好,而是来源越独立越好。冲突时先怀疑采集链,再怀疑产品,也怀疑自己的第一次判断。
看见的现象差点得出的错结论最后使用的正交证据
Pad screenshot 全黑Ghostty compositor 又挂了PixelCopy + CPU bitmap + generation
marker 出现两次输入 exactly-once 失效新 nonce 证明是超时批次与重跑重叠
Wi-Fi 断开再恢复Mosh 完成跨 NAT roam只声明同 session 恢复;真正 roam 看网络 identity
Metro 冷启动 ANR2.5 秒 renderer defer 卡死embedded/cached bundle 冷启动无复现
05 / THE LONG GOAL

“全自动”的真相:不是没人,而是人只出现在值钱的地方

这次 Goal 的逻辑时长是 21 小时 54 分 42 秒,但机器 uptime 更短。它不是一个神秘进程坚持不睡觉,而是靠 Git、tmux、可轮询工具、计划状态、review handoff 和外部系统状态持续恢复上下文。

本轮用户实际参与很少:登录 RevenueCat,在 Chrome/Google 开发者控制台完成少量必须由账号持有人确认的点击,并在 commit/push 等不可逆动作上给出明确授权。已有登录态发现、绝大多数 Play 表单填写与事实核对、gcloud/权限打通、API 生成、设备验证、购买/恢复测试、截图录屏、错误恢复和整理,均由 Goal 自己闭环。

稳定 Goal 目标

阶段计划与证据门槛

Git 原子提交

tmux 与可轮询工具

设备和外部服务状态

独立 reviewer verdict

进程或开机变化后继续

图 7:长 Goal 的持久性来自可恢复状态,不来自某个永不退出的进程。

先读机器,再写代码

浏览器、CLI、设备、登录态、已有 iOS 契约和仓库 dirty state 都先被识别。

先保证退路

Ghostty、Mosh、Billing、OTA/native skew 都先定义失败方向,再开启新能力。

让真实设备和服务说话

connected tests 之后还有真实 SSH、UDP、FCM、Play、RevenueCat 和像素层。

每条高风险线单独过哨兵

P1/P2 不积压到最后;reviewer 接受代码,再接受设备证据。

精确白名单提交

并行工作树不夹带,临时 profile、媒体、fixture 和 secret 及时清理。

06 / THE SURPRISE LEDGER

最终 diff 没告诉你的十件事

🕵️

自己找到权限入口

从现有 Chrome 上下文判断哪个账号能操作,而不是要求用户重新交代环境。

📸

没有 CDP 就先截图

OS 层到达 consent,用户只做一次必要授权,随后切回 DOM 自动化。

🧰

缺工具就造最小工具

现场生成 MCP、Play catalog、AAB upload 和 RevenueCat reconcile 客户端。

🔐

两个 SA,不造万能钥匙

Billing 与 publisher 分离,GCP IAM 与 Play App 权限分层。

🧯

强制失败也不丢 session

renderer 崩了换表面,transport 和 history 继续工作。

🎞️

视频黑了会换证据

不用坏掉的采集链给产品下结论。

🧪

DEV seam 不伪造产品成功

调试注入仍走产品 manager/native gate,DEV Pro 不当购买证据。

🧭

主动缩小声明

Wi-Fi 恢复就是恢复,不冒充不同 NAT 的 roam。

🧹

清理也是交付

截图、视频、profile、fixture、DB、旋转和输入法状态都恢复。

🪶

把复杂写成可复用

JSONL 取证、CDP、设备证据、Play/RC bootstrap 都沉淀成手册。

07 / THE REUSABLE PLAYBOOK

下一个 App,可以直接抄走什么?

Goal-driven Porting, v1

  1. 写契约,不写“照 iOS 做”。 明确成功、失败、回退和用户可见语义。
  2. 先找 authority。 浏览器登录态、Cloud IAM、商店权限、设备和服务各是谁说了算。
  3. 先 fail-closed。 新 native、billing、OTA skew 都要有能工作的旧路径。
  4. 缺工具就做薄桥。 只解决最后一公里,默认临时、可观察、可销毁。
  5. 测试按假设分设备。 emulator、旧真机、OEM 大屏各有任务,不是数量游戏。
  6. 唯一 marker + 多 oracle。 像素、结构、系统、transport 和远端至少两路互证。
  7. 把 4xx 当地图。 scope、IAM、App permission、Billing permission 分层定位。
  8. 每个危险边界单独 review。 小批关闭 P1/P2,再跑真实服务。
  9. 需要真人时才出现。 本轮实际只有登录、少量 consent/console 点击和 git side-effect 授权;其他项目若触及 MFA、法律事实或 production,再把这些不可替代的边界留给人。
  10. 清理并写下边界。 不留 secret、fixture、夸大的“通过”和不可复现的英雄故事。

最后一个反直觉结论

真正有传播力的 AI 工程故事,不是“它连续干了 22 小时”,而是“它知道什么时候自己干,什么时候找证据,什么时候承认截图不可信,什么时候必须让人点一下”。自动化的上限,不由点击次数决定,而由边界感决定。

最好的 Agent,不是替你做所有决定;而是把你的参与,压缩到真正需要拍板的那几次。

架构图放大查看