事情起於一個看起來很輕的要求: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」必須一致。
flowchart LR
I["iOS 已有能力"] --> C{"共享產品契約"}
C --> A["Android 原生實作"]
A --> D["平台差異策略"]
D --> V["三設備與真實服務驗證"]
V --> R{"證據通過?"}
R -- "否" --> F["定位根因並縮小聲明"]
F --> V
R -- "是" --> S["小批複審與原子提交"]
輸入契約
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,而不是一塊禪意十足的永久空白。
stateDiagram-v2
[*] --> Pending
Pending --> Ghostty: health proof succeeds before timeout
Pending --> Xterm: timeout or proof fails
Ghostty --> Xterm: runtime GL failure
Xterm --> StableSession: same transport and ring replay
Ghostty --> StableSession: native renderer active
StableSession --> [*]
note right of Ghostty
Native VT plus full GL path
end note
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、固定版本和默認脫敏。
sequenceDiagram
actor U as 使用者
participant A as Goal Agent
participant B as Chrome
participant G as gcloud
participant P as Play Publisher API
participant R as RevenueCat API
A->>B: 發現已有登錄態和目標帳號上下文
U->>B: 明確允許 remote debugging
B-->>A: CDP 頁面控制可用
A->>G: 啟用 API 並建立兩套最小權限身份
A->>B: 在 Play Console 綁定應用程式權限
A->>P: 冪等建立 catalog 並上傳 internal AAB
A->>R: 建立 Android app 與商品映射
P-->>A: track 與 artifact 狀態
R-->>A: entitlement 與 offering 狀態
A-->>U: 只請求業務事實與不可逆發布決策
截圖黑了,先別給GPU 寫悼詞
Android 真機驗證最危險的時刻,是第一張截圖看起來非常有說服力。SurfaceView 可能在系統截圖裡全黑;Xiaomi screencap 可能是0 位元組;screenrecord 可能只錄到沒有硬體層的世界。反過來,遠端tmux 看到了marker,也只證明transport 活著,不能證明本地compositor 畫出來了。
於是證據設計成多oracle:唯一marker、UI tree、PixelCopy、CPU raster、PID、dumpsys、logcat、遠端pane。它們必須互相證偽,而不是排隊鼓掌。
flowchart TB
M["注入唯一 marker"] --> U["UI tree 與控制項狀態"]
M --> X["Screenshot 或 PixelCopy"]
M --> S["PID · dumpsys · logcat"]
M --> T["遠端 SSH/Mosh/tmux 狀態"]
U --> C{"多個 oracle 一致?"}
X --> C
S --> C
T --> C
C -- "否" --> A["分類測試工件"]
A --> N["更換 nonce 與 oracle 重跑"]
N --> M
C -- "是" --> B["寫下有邊界的結論"]
| 看見的現象 | 差點得出的錯結論 | 最後使用的正交證據 |
|---|---|---|
| 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 自己閉環。
flowchart TB
O["穩定 Goal 目標"] --> P["階段計劃與證據門檻"]
P --> G["Git 原子提交"]
P --> T["tmux 與可輪詢工具"]
P --> E["設備和外部服務狀態"]
P --> R["獨立 reviewer verdict"]
G --> C["進程或開機變更後繼續"]
T --> C
E --> C
R --> C
C --> P
先讀機器,再寫程式碼
瀏覽器、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,不是替你做所有決定;而是把你的參與,壓縮到真正需要拍板的那幾次。