事情起于一个看起来很轻的要求:iOS 已经有的那些好东西,Android 也该有吧?如果把这句话交给传统项目管理,它会长成一排 Jira;交给这次 Goal,它长成了 Ghostty 原生终端、Mosh 真 UDP、Pad 大屏、PiP、Widget、通知、Google Play 与 RevenueCat——外加一只逐渐学会看截图、开浏览器、查 403、还会怀疑自己证据的鸭子。
Android 不是 iOS 穿了件绿色外套
所谓“从 iOS 适配到 Android”,最容易做错的方式,是照着页面截图逐像素复刻。真正需要迁移的是产品契约:用户输入一个中文字符只能到远端一次;renderer 失败不能断 SSH;后台回来要看到新 frame;订阅恢复要认得当前 plan;Pad 不是一台被横着放大的手机。
于是每项能力都被翻译成三层:共享语义、平台实现、证据边界。iOS 可以用自己的 viewport primitive,Android 可以通过 Ghostty ABI 3;但“手指向下浏览更旧历史、点一下回到 live”必须一致。
输入契约
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。
跨平台不是“长得一样”,而是“出事时也守同一份承诺”。
先让 Ghostty 失败,再允许它成功
Android Ghostty 的第一原则有点反直觉:一开始绝不乐观。同步常量永远先给 `ok=false`,异步自测必须证明 native core、ABI、VT、EGL、shader、texture upload、draw 和 readback 都成立,才从 xterm 切到 Ghostty。
这避免了首帧“先黑一下再说”的经典移动端玄学。健康证明还有 2.5 秒上限:GPU 真要进入沉思,用户最多看 2.5 秒 spinner,然后得到一台能工作的 xterm,而不是一块禅意十足的永久空白。
core.ok=true
gl.maxRgbDelta=2
$ printf '中文 😀 é'
中文 😀 é
renderer unhealthy
→ xterm fallback
$ echo STILL_ALIVE
STILL_ALIVE
Wi-Fi off
DURING_OK
Wi-Fi on
AFTER_OK
终端胶片为文章复原示意,不是运营后台原图;marker、结果与边界来自真实设备证据。
那些“顺手”一起补齐的硬骨头
输入法 composition、xterm 键序、触摸 selection、ActionMode copy、native scrollback、background compositor、PixelCopy probe、CPU snapshot、Pad keyboard focus mode……每一项单看都像 P3,叠在一起才决定“这是一台终端,还是一张会闪的黑色矩形”。
浏览器里有权限,但没有一把现成的钥匙
Play Console 的权限躺在已经登录的 Chrome profile 里;内置 Playwright 是干净 profile,什么也看不到;CDP 还没开。最省事的回答是“请用户手工操作”。Goal 选择了另一条路:先找出哪个浏览器上下文有权限,再把用户参与压缩成一次必要 consent。
没有现成桥,就在临时目录里生成一个:MCP SDK + `chrome-devtools-mcp` + 薄 stdio client,再用持久 PTY 连续发 `snapshot → click → snapshot`。它没有被包装成“平台能力”,完成后被清理;真正可复用的结论是专用 profile、origin allowlist、固定版本和默认脱敏。
截图黑了,先别给 GPU 写悼词
Android 真机验证最危险的时刻,是第一张截图看起来非常有说服力。SurfaceView 可能在系统截图里全黑;Xiaomi screencap 可能是 0 字节;screenrecord 可能只录到没有硬件层的世界。反过来,远端 tmux 看到了 marker,也只证明 transport 活着,不能证明本地 compositor 画出来了。
于是证据被设计成多 oracle:唯一 marker、UI tree、PixelCopy、CPU raster、PID、dumpsys、logcat、远端 pane。它们必须互相证伪,而不是排队鼓掌。
| 看见的现象 | 差点得出的错结论 | 最后使用的正交证据 |
|---|---|---|
| Pad screenshot 全黑 | Ghostty compositor 又挂了 | PixelCopy + CPU bitmap + generation |
| marker 出现两次 | 输入 exactly-once 失效 | 新 nonce 证明是超时批次与重跑重叠 |
| Wi-Fi 断开再恢复 | Mosh 完成跨 NAT roam | 只声明同 session 恢复;真正 roam 看网络 identity |
| Metro 冷启动 ANR | 2.5 秒 renderer defer 卡死 | embedded/cached bundle 冷启动无复现 |
“全自动”的真相:不是没人,而是人只出现在值钱的地方
这次 Goal 的逻辑时长是 21 小时 54 分 42 秒,但机器 uptime 更短。它不是一个神秘进程坚持不睡觉,而是靠 Git、tmux、可轮询工具、计划状态、review handoff 和外部系统状态持续恢复上下文。
本轮用户实际参与很少:登录 RevenueCat,在 Chrome/Google 开发者控制台完成少量必须由账号持有人确认的点击,并在 commit/push 等不可逆动作上给出明确授权。已有登录态发现、绝大多数 Play 表单填写与事实核对、gcloud/权限打通、API 生成、设备验证、购买/恢复测试、截图录屏、错误恢复和整理,均由 Goal 自己闭环。
先读机器,再写代码
浏览器、CLI、设备、登录态、已有 iOS 契约和仓库 dirty state 都先被识别。
先保证退路
Ghostty、Mosh、Billing、OTA/native skew 都先定义失败方向,再开启新能力。
让真实设备和服务说话
connected tests 之后还有真实 SSH、UDP、FCM、Play、RevenueCat 和像素层。
每条高风险线单独过哨兵
P1/P2 不积压到最后;reviewer 接受代码,再接受设备证据。
精确白名单提交
并行工作树不夹带,临时 profile、媒体、fixture 和 secret 及时清理。
最终 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 都沉淀成手册。
下一个 App,可以直接抄走什么?
Goal-driven Porting, v1
- 写契约,不写“照 iOS 做”。 明确成功、失败、回退和用户可见语义。
- 先找 authority。 浏览器登录态、Cloud IAM、商店权限、设备和服务各是谁说了算。
- 先 fail-closed。 新 native、billing、OTA skew 都要有能工作的旧路径。
- 缺工具就做薄桥。 只解决最后一公里,默认临时、可观察、可销毁。
- 测试按假设分设备。 emulator、旧真机、OEM 大屏各有任务,不是数量游戏。
- 唯一 marker + 多 oracle。 像素、结构、系统、transport 和远端至少两路互证。
- 把 4xx 当地图。 scope、IAM、App permission、Billing permission 分层定位。
- 每个危险边界单独 review。 小批关闭 P1/P2,再跑真实服务。
- 需要真人时才出现。 本轮实际只有登录、少量 consent/console 点击和 git side-effect 授权;其他项目若触及 MFA、法律事实或 production,再把这些不可替代的边界留给人。
- 清理并写下边界。 不留 secret、fixture、夸大的“通过”和不可复现的英雄故事。
最后一个反直觉结论
真正有传播力的 AI 工程故事,不是“它连续干了 22 小时”,而是“它知道什么时候自己干,什么时候找证据,什么时候承认截图不可信,什么时候必须让人点一下”。自动化的上限,不由点击次数决定,而由边界感决定。
最好的 Agent,不是替你做所有决定;而是把你的参与,压缩到真正需要拍板的那几次。