手機(jī)版:從「想在手機(jī)上用」到裝進(jìn) iPhone 主屏)
給 AI 管家做個(gè)手機(jī)版從「想在手機(jī)上用」到裝進(jìn) iPhone 主屏我的 AI 管家跑在家里那臺(tái)機(jī)器上。它能替我干活——查數(shù)據(jù)、跑腳本、寫(xiě)東西、盯任務(wù)——但有個(gè)前提我得坐在電腦前。一旦離開(kāi)電腦它就從「助手」變成了「黑盒」我知道它在干活卻看不見(jiàn)它干到哪一步、也接不上話。我想解決的不是功能問(wèn)題是位置問(wèn)題。下面這一天里發(fā)生的事方案定稿 → 功能一次做全 → 裝進(jìn) iPhone 主屏 → 上線中間 22 輪迭代。踩的坑我全寫(xiě)在這兒了包括幾個(gè)「只有真機(jī)才會(huì)暴露」的。本文由 AI 輔助創(chuàng)作人工審閱后發(fā)布。一、起因問(wèn)題不在功能在位置具體一點(diǎn)說(shuō)我當(dāng)時(shí)的困擾是四條直接在手機(jī)瀏覽器里訪問(wèn)它適配很差——那不是給手機(jī)做的頁(yè)面看著難受點(diǎn)著更難受。外網(wǎng)根本訪問(wèn)不了——它只在家里那臺(tái)機(jī)器上出門就斷聯(lián)。它自己沒(méi)有登錄、沒(méi)有權(quán)限體系——直接把端口懟到公網(wǎng)上等于把整個(gè) API 裸奔給全世界這點(diǎn)后面還會(huì)說(shuō)它是整個(gè)設(shè)計(jì)的前提。用聊天軟件雖然能發(fā)消息但看不到歷史、也切不了會(huì)話——想翻回上一條結(jié)論做不到。所以需求很清楚一個(gè)手機(jī)瀏覽器打開(kāi)的輕量前端能切會(huì)話、能發(fā)消息、能看歷史并且要能安全地在外面用。聽(tīng)起來(lái)像個(gè)小項(xiàng)目。但它踩的坑比功能本身多得多。二、動(dòng)手之前先把底摸清我一開(kāi)始克制住了「先寫(xiě)代碼」的沖動(dòng)花時(shí)間把環(huán)境摸了一遍。事后看這一步省下了最多的返工。① 它的 API 是干凈的 REST SSE。列會(huì)話、看歷史、發(fā)消息、停止生成、上傳文件、審批工具授權(quán)都有對(duì)應(yīng)的接口發(fā)消息是流式的SSE服務(wù)器推著字往外出。這意味著前端可以寫(xiě)得很薄——不用為了它專門改后端。② 也是最關(guān)鍵的一條它沒(méi)有鑒權(quán)。它的鑒權(quán)狀態(tài)接口明確返回「未啟用、無(wú)用戶」。也就是說(shuō)誰(shuí)連上這個(gè)端口誰(shuí)就能看到我的全部會(huì)話、也能替我發(fā)指令。這一條直接決定了整件事的形態(tài)前端頁(yè)面可以隨便做但必須有一道門擋在前面而且這道門不能做在頁(yè)面里頁(yè)面里的密碼框等于把「進(jìn)門的鑰匙藏在門墊下面」——接口本身還是裸的。③ 手上有現(xiàn)成的隧道和現(xiàn)成的門禁。之前為了別的事情已經(jīng)搭好了兩樣?xùn)|西一條從家里通到云上服務(wù)器的反向隧道和一套「nginx 層口令門禁」——沒(méi)輸口令連 HTML 都不下發(fā)種下 cookie 之后 30 天內(nèi)免輸。于是方案就很樸素了只寫(xiě)前端剩下的全部復(fù)用。不新買機(jī)器、不開(kāi)新域名、不動(dòng)后端。三、方案把口令擋在 nginx 那一層最終的鏈路是這樣手機(jī)瀏覽器 │ HTTPS ▼ 云服務(wù)器 nginx ── ① 口令門禁擋住整個(gè)路徑下的一切 ── ② 前端靜態(tài)頁(yè)就是幾個(gè)靜態(tài)文件 ── ③ /api/ 反向代理關(guān)掉緩沖、拉長(zhǎng)超時(shí) ▼ 反向隧道 ? 家里那臺(tái)機(jī)器 ? AI 管家自己的 API三條設(shè)計(jì)決定都是在「省事」和「安全」之間取了交集復(fù)用已有的路徑不開(kāi)新域名、不申請(qǐng)新證書(shū)門禁做在 nginx 層server 級(jí)攔截 cookie頁(yè)面本身不需要登錄邏輯前端的每一個(gè)請(qǐng)求天然被護(hù)在里面前端所有請(qǐng)求走同源頁(yè)面和 API 在同一個(gè)域名下不引入跨域也不需要在 JS 里管理任何憑證。截圖里的口令框是空的頁(yè)面標(biāo)題打了碼。門禁第一版上線前我把「不輸口令會(huì)怎樣」當(dāng)驗(yàn)收項(xiàng)跑了匿名訪問(wèn)只能拿到一個(gè)口令頁(yè)拿不到任何真實(shí)內(nèi)容——門禁不是裝飾它就是那道鎖。四、這一天先把功能一次做全功能我是一次做全的切會(huì)話、發(fā)消息、看歷史、傳圖傳文件、語(yǔ)音輸入、工具授權(quán)審批、模型與智能體切換。事后看這個(gè)決定是對(duì)的——這些功能互相咬合比如審批卡片是長(zhǎng)在消息流里的分期反而更麻煩。會(huì)話名已打碼。會(huì)話列表有兩個(gè)坑坑一列表里混著不該出現(xiàn)的東西。API 返回的會(huì)話列表里混著定時(shí)任務(wù)的會(huì)話和子代理的會(huì)話——這些不是「我在跟它聊天」而是系統(tǒng)自己在跑。前端必須按字段把它們?yōu)V掉否則列表會(huì)被每天的定時(shí)任務(wù)刷滿。這個(gè)過(guò)濾一開(kāi)始還漏了一種形式有的定時(shí)會(huì)話 id 里帶前綴類似平臺(tái):用戶:cron:xxx而我的判斷條件是「以 cron 開(kāi)頭」——漏了。收緊之后列表從 75 條變成 55 條干凈了??佣@個(gè)我印象最深排序整個(gè)失效而且一聲不吭。我一開(kāi)始的排序代碼長(zhǎng)這樣list.sort((a,b)(b.updated_at||0)-(a.updated_at||0));看起來(lái)沒(méi)問(wèn)題但服務(wù)端給的updated_at是ISO 格式的字符串類似2026-09-29T04:07:12.948Z。字符串相減得到的是NaN。而Array.sort的規(guī)定是比較函數(shù)返回NaN時(shí)當(dāng)作「兩者相等」處理。結(jié)果就是排序被靜默跳過(guò)列表老老實(shí)實(shí)按接口返回的順序最舊的在前顯示——不報(bào)錯(cuò)、不警告、什么都不提示。我盯著它看了半天一度以為是自己排序規(guī)則寫(xiě)反了。修法不難把時(shí)間先歸一化成數(shù)字再比但教訓(xùn)挺值錢弱類型語(yǔ)言里的「靜默失敗」比報(bào)錯(cuò)難查一百倍。順帶還有個(gè)體驗(yàn)細(xì)節(jié)既然要求「最近的放最前」那么被置頂?shù)臅?huì)話就不該強(qiáng)行頂?shù)阶钌厦妗F(xiàn)在置頂只保留一個(gè)灰色小標(biāo)簽信息不丟順序不亂。新會(huì)話的自動(dòng)命名要跟服務(wù)端配合新會(huì)話原本永遠(yuǎn)叫「新會(huì)話」。查了服務(wù)端的實(shí)現(xiàn)才知道它的自動(dòng)起名有個(gè)前提建會(huì)話時(shí)名字要先等于首條消息的前 10 個(gè)字——服務(wù)端據(jù)此生成標(biāo)題。照這個(gè)邏輯接上之后新會(huì)話第一次發(fā)言完名字就自動(dòng)變成了像樣的標(biāo)題。這種「讀一眼對(duì)方源碼再動(dòng)手」的事猜是猜不出來(lái)的。五、真機(jī)才是考場(chǎng)功能在電腦上全綠之后我發(fā)到手機(jī)上試。第一次反饋就三件事輸入框不對(duì)齊、頂欄被壓住、鍵盤一彈輸入?yún)^(qū)就廢。我在無(wú)頭瀏覽器里怎么都復(fù)現(xiàn)不出來(lái)只能看著真機(jī)截圖一個(gè)個(gè)量。① 輸入框不是「差點(diǎn)沒(méi)對(duì)齊」是被壓成了 19px真機(jī)截圖是 1170×2532也就是 390×844 的 CSS 尺寸我把上下邊框的位置量出來(lái)輸入框只有約19 個(gè) CSS 像素高——正常應(yīng)該是 42px。它的上下內(nèi)邊距本身就要占 18px等于內(nèi)容區(qū)被壓到了零文字被擠出框外。根因不在 CSS在調(diào)用時(shí)機(jī)內(nèi)容是先寫(xiě)進(jìn)輸入框、再顯示聊天視圖的。而寫(xiě)進(jìn)輸入框時(shí)會(huì)去自動(dòng)調(diào)整高度讀一下內(nèi)容高度、把高度設(shè)成那個(gè)值——那會(huì)兒容器還是隱藏狀態(tài)沒(méi)有布局讀出來(lái)的高度恒為 0于是高度被設(shè)成了 0靠瀏覽器兜底撐成 19px。三處一起修調(diào)整順序先顯示再回填、加一道「不可見(jiàn)就不測(cè)量」的保險(xiǎn)、再補(bǔ)上box-sizing吃掉的那 2px 邊框CSS 里順手兜一層最小高度。② 安全區(qū)只加了一半頂欄的標(biāo)題和右上角的控件被 iPhone 的狀態(tài)欄壓住了。原因很樸素安全區(qū)的內(nèi)邊距只加在了列表頁(yè)聊天頁(yè)漏了——于是聊天頁(yè)的頂欄老老實(shí)實(shí)頂?shù)搅藙⒑5紫?。?鍵盤彈起來(lái)輸入?yún)^(qū)被蓋住iOS 不會(huì)因?yàn)槟惆演斎雲(yún)^(qū)固定position: fixed就把它頂?shù)芥I盤上面去它只會(huì)蓋住你。得自己算拿「窗口高度 ? 可視視口高度」得到真實(shí)遮擋高度再把輸入?yún)^(qū)頂上去。④ 底部那 47px不是內(nèi)邊距是瀏覽器不給這一條最值得說(shuō)。輸入?yún)^(qū)下方始終留著一條空白。我先按內(nèi)邊距查量完發(fā)現(xiàn)不對(duì)輸入?yún)^(qū)規(guī)規(guī)矩矩貼著布局視口的底邊只是那個(gè)「布局視口」只有 797px而屏幕是 844px——差的 47px 不在頁(yè)面里頁(yè)面畫(huà)不到那兒。這是 iOS Safari 的老毛病地址欄收起之后它不重算頁(yè)面的初始包含塊于是頁(yè)面一直卡在小尺寸上。我的處理是承認(rèn)這件事不跟瀏覽器爭(zhēng)這塊地改底色。把頁(yè)面畫(huà)布的底色改成和輸入?yún)^(qū)一樣的顏色那條縫就從「灰色空白」變成「輸入?yún)^(qū)底色的延伸」看不出接縫。深色淺色各一套。說(shuō)句實(shí)話這是「視覺(jué)補(bǔ)齊」不是真鋪滿。輸入框的底邊仍然在離屏幕底 47px 的地方——那塊地不歸網(wǎng)頁(yè)。哪天瀏覽器肯給滿屏這段邏輯自動(dòng)就多余了不用改代碼。順便我在這兒埋了個(gè)診斷點(diǎn)頁(yè)面每次打開(kāi)都會(huì)把「它實(shí)際拿到的視口高度」回報(bào)到服務(wù)器。這樣「到底是不是瀏覽器不給」不用猜——看日志里那個(gè)數(shù)字是 797 還是 844 就知道了。六、把它變成「App」圖標(biāo)、清單、啟動(dòng)圖頁(yè)面能用之后下一個(gè)訴求是能不能像 App 一樣放在主屏上圖標(biāo)丑的真因不是審美問(wèn)題iOS 支持「添加到主屏幕」但這個(gè)頁(yè)面當(dāng)時(shí)既沒(méi)有主屏圖標(biāo)、也沒(méi)有應(yīng)用清單——于是 iOS 只能拿網(wǎng)頁(yè)截圖當(dāng)圖標(biāo)。所以主屏上那個(gè)東西特別丑它不是設(shè)計(jì)得丑是它壓根不是一個(gè)圖標(biāo)。補(bǔ)齊的東西不多一張 180×180 的主屏圖標(biāo)、一份應(yīng)用清單聲明成獨(dú)立應(yīng)用、給個(gè)名字、16 張啟動(dòng)圖8 種機(jī)型 × 深淺兩套——這樣從主屏冷啟不會(huì)再白屏閃一下。圖標(biāo)的三個(gè)版本和「不作拉伸」的做法圖標(biāo)我做了三版給用戶挑最后定的是「滿底橙、點(diǎn)陣花」那一版不要任何框圖標(biāo)本體就是那塊橙。這里有個(gè)技術(shù)細(xì)節(jié)我挺喜歡用戶給的源圖只有126×112上面的點(diǎn)直徑才3~8 像素直接放大會(huì)糊成馬賽克。所以我沒(méi)有拉伸原圖而是把那49 個(gè)點(diǎn)逐個(gè)量出來(lái)中心 1 點(diǎn) 8 層環(huán) × 每層 6 點(diǎn)每一層比內(nèi)層整體旋轉(zhuǎn)約 33°——這個(gè)旋轉(zhuǎn)正是源圖那種「漩渦感」的來(lái)源再用矢量圓重新畫(huà)成 1024 的母版。與源圖逐點(diǎn)比對(duì)誤差0.39 像素等于逐點(diǎn)復(fù)刻但邊緣是干凈的。iOS 的規(guī)矩圖標(biāo)只在「添加」那一刻抄這一步有個(gè)必須知道的規(guī)矩**iOS 是在你點(diǎn)「添加到主屏幕」的那一刻把圖標(biāo)、名字、啟動(dòng)圖抄下來(lái)的。**之后服務(wù)器上怎么改主屏那個(gè)圖標(biāo)都不會(huì)自動(dòng)更新。所以每次換圖標(biāo)用戶都得手動(dòng)走一遍長(zhǎng)按舊圖標(biāo) → 移除 → 重新打開(kāi)頁(yè)面 → 添加到主屏幕。另外 iOS 是按 URL 緩存主屏圖標(biāo)和啟動(dòng)圖的URL 不變可能繼續(xù)用舊圖——為此給這些資源加了個(gè)版本號(hào)參數(shù)?vbloom來(lái)強(qiáng)制刷新。七、用起來(lái)之后才出現(xiàn)的三個(gè)真問(wèn)題到這一步「做出來(lái)」的部分結(jié)束了。真正有意思的部分從這里才開(kāi)始只有每天都用它才會(huì)遇到的問(wèn)題。① 鎖屏回來(lái)屏幕上只剩一句Load failed用完手機(jī)鎖屏再打開(kāi)頁(yè)面就一句紅色的Load failed什么都不剩??雌饋?lái)像「任務(wù)掛了」但我去核對(duì)了服務(wù)端代碼客戶端斷開(kāi)只是取消訂閱那一輪任務(wù)本身照跑結(jié)果照樣落庫(kù)。換句話說(shuō)用戶截圖里那一輪其實(shí)早就跑完了內(nèi)容一條沒(méi)丟——只是頁(yè)面沒(méi)去把它取回來(lái)也不知道該去取。順便說(shuō)這條「斷開(kāi) ≠ 失敗」的認(rèn)知是整件事里最重要的一條。它決定了修復(fù)的方向——不是「重試請(qǐng)求」而是「接回來(lái)」現(xiàn)在的設(shè)計(jì)是兩步斷線時(shí)不再只丟紅字改成一張?zhí)摼€卡片「連接中斷了。手機(jī)鎖屏 / 切后臺(tái)容易斷任務(wù)本身還在跑接回就能補(bǔ)齊?!?一個(gè)【繼續(xù)接回】按鈕但多數(shù)情況根本不用點(diǎn)回到前臺(tái)、從主屏重開(kāi)、網(wǎng)絡(luò)恢復(fù)都會(huì)自動(dòng)接回先問(wèn)服務(wù)端狀態(tài)還在跑就接上增量跑完了就直接取結(jié)果自動(dòng)重試三次不成功才輪到手動(dòng)按鈕。關(guān)鍵差別在「補(bǔ)齊」接回不只是接續(xù)后面的輸出而是用服務(wù)端落庫(kù)的歷史覆蓋頁(yè)面——所以鎖屏幾分鐘回來(lái)看到的是一份完整回答而不是半截。還有個(gè)安全閥如果拉回來(lái)的內(nèi)容比屏幕上已有的還短說(shuō)明服務(wù)端還沒(méi)落完就等一會(huì)兒重試服務(wù)端一條都沒(méi)有時(shí)絕不覆蓋本地——免得把已經(jīng)讀到的字擦掉。② 語(yǔ)音發(fā)完輸入框里還留著字用戶反饋說(shuō)話發(fā)送之后輸入框里又冒出剛才那段文字。根因是識(shí)別引擎沒(méi)被停掉我把「發(fā)送」和「停止識(shí)別」的順序搞反了——發(fā)送之后識(shí)別引擎還會(huì)遲到一次結(jié)果那個(gè)回調(diào)把已經(jīng)發(fā)出去的文字又寫(xiě)回了輸入框更糟的是還存成了草稿重開(kāi)這個(gè)會(huì)話又回填一次。修法就是發(fā)送時(shí)先靜音識(shí)別、再停引擎遲到的結(jié)果一律丟棄。這條我特意沒(méi)有「憑感覺(jué)改」先寫(xiě)了個(gè)復(fù)現(xiàn)腳本拿未修復(fù)的版本跑——真的紅了輸入框里冒出「你好世界。」跟用戶描述一模一樣再換修復(fù)版全綠。③ 重進(jìn)正在跑的會(huì)話手機(jī)端毫無(wú)反應(yīng)用戶又報(bào)切回列表、再點(diǎn)進(jìn)那個(gè)正在跑的任務(wù)頁(yè)面什么都不動(dòng)。這次是三個(gè) bug 疊在一起上一輪對(duì)話沒(méi)收干凈導(dǎo)致「正在流式輸出」這個(gè)狀態(tài)永久卡在 true于是「接回」的調(diào)用被入口守衛(wèi)擋了回來(lái)而重渲染消息時(shí)又用整塊重寫(xiě)的方式把流式輸出寫(xiě)進(jìn)了一個(gè)已經(jīng)被摘出頁(yè)面的舊卡片里。修完之后加了兜底12 秒沒(méi)動(dòng)靜就自動(dòng)檢查一次「是不是僵尸流」自己修復(fù)。驗(yàn)收數(shù)字也很直觀——修復(fù)前 90 秒一個(gè)字沒(méi)出修復(fù)后1~2 秒就看到 315 個(gè)字在滾。八、通知讓「跑完了」這件事找到我我最真實(shí)的使用場(chǎng)景是在手機(jī)上發(fā)一條長(zhǎng)任務(wù)然后把手機(jī)鎖上去干別的事。所以「跑完了得讓我知道」是個(gè)剛需。這個(gè)功能我改了兩次第一版走的是第三方推送 App能響、能穿透專注模式。但很快發(fā)現(xiàn)一個(gè)硬傷它點(diǎn)開(kāi)是跳進(jìn)那個(gè) App而不是跳回我的頁(yè)面——我希望點(diǎn)開(kāi)就直接進(jìn)「AI 管家」這個(gè)網(wǎng)頁(yè)應(yīng)用落在對(duì)應(yīng)的會(huì)話里。第二版換成了 Web PushiOS 16.4 之后從主屏打開(kāi)的網(wǎng)頁(yè)應(yīng)用可以自己發(fā)通知。代價(jià)是多了三個(gè)前提系統(tǒng)版本要夠、必須先從主屏圖標(biāo)打開(kāi)再開(kāi)通知開(kāi)關(guān)在 Safari 標(biāo)簽頁(yè)里開(kāi)不了、別從后臺(tái)把它徹底劃掉劃掉會(huì)連通知一起停?,F(xiàn)在的行為是長(zhǎng)任務(wù)跑完 → 手機(jī)收到一條通知會(huì)話名 回答前 70 字 耗時(shí)→點(diǎn)開(kāi)直接進(jìn) App 并落在那個(gè)會(huì)話。太短的任務(wù)不到 60 秒不打擾?!竿ㄖ锬懿荒苤苯狱c(diǎn)『批準(zhǔn) / 拒絕』」問(wèn)這個(gè)問(wèn)題的場(chǎng)景很具體任務(wù)卡在「需要授權(quán)某個(gè)操作」上等我點(diǎn)確認(rèn)我在外面希望直接在通知上點(diǎn)一下。結(jié)論是不能而且這是平臺(tái)限制不是實(shí)現(xiàn)問(wèn)題。網(wǎng)頁(yè)通知的標(biāo)準(zhǔn)里有個(gè)「按鈕」能力Notification.actions但它在 Safari 和 iOS 上都沒(méi)有支持——只有原生 App 用自己的通知接口才能做到。所以最好的效果只能是點(diǎn)通知 → 進(jìn) App → 落在該會(huì)話的審批卡片上 → 點(diǎn)一次「允許」一共兩次點(diǎn)擊。這已經(jīng)是這條路線的天花板了。九、最值得說(shuō)的一條工程判斷審批通知等 120 秒最后這段是整件事里我最想分享的一段——因?yàn)樗皇羌夹g(shù)問(wèn)題是判斷問(wèn)題。需求原話是「能不審批就不審批只有一定要審批、而且真卡住了才通知到我手機(jī)上?!刮业谝话孀龅煤苤卑滓豢匆?jiàn)有「待審批」就推通知。然后就被自己的手機(jī)教育了。某天 23:10我收到一條「? 任務(wù)卡在等你確認(rèn)」——點(diǎn)進(jìn)去一看它 20 秒后自己就被批準(zhǔn)了。我根本沒(méi)察覺(jué)有這回事通知純屬虛驚。于是我去翻數(shù)據(jù)看「審批到底通常要等多久」當(dāng)天可配對(duì)的記錄 259 筆指標(biāo)數(shù)值批準(zhǔn)耗時(shí)中位數(shù)8 秒75% 的審批在32 秒內(nèi)完成90% 的審批在102 秒內(nèi)完成超過(guò) 120 秒才被批的只有 23 筆服務(wù)器自動(dòng)拒絕的等待上限300 秒數(shù)據(jù)把話說(shuō)清楚了絕大多數(shù)審批是在幾秒內(nèi)完成的——因?yàn)橥ǔG闆r下人就在電腦前順手點(diǎn)了。所以我給通知加了一個(gè)等待門檻默認(rèn) 120 秒。審批滿了 120 秒還沒(méi)人理才推給你正文里還會(huì)寫(xiě)明「已經(jīng)等了約 N 分鐘」「再?zèng)]人應(yīng)它就會(huì)被自動(dòng)拒絕」。這條修完我還順手糾正了自己一個(gè)錯(cuò)誤認(rèn)知我一度以為「審批通道是啞的、根本沒(méi)在用」。查完發(fā)現(xiàn)當(dāng)天真實(shí)發(fā)生了 363 筆審批——因?yàn)樗墓ぞ呤匦l(wèi)有一份內(nèi)置的默認(rèn)清單包含執(zhí)行命令、讀寫(xiě)文件這類危險(xiǎn)操作即便審批級(jí)別設(shè)成「自動(dòng)」危險(xiǎn)操作該攔還是會(huì)攔。**這次修正背后的那句話比代碼值錢通知不該報(bào)「有事件發(fā)生」該報(bào)「真的卡住了」。**前者只是把系統(tǒng)的忙碌轉(zhuǎn)嫁給人后者才是「需要人」。十、代價(jià)和邊界誠(chéng)實(shí)清單寫(xiě)到這里都是成果但這些是必須一起說(shuō)清的代價(jià)后端 API 本身沒(méi)有鑒權(quán)所以口令門禁就是那把鎖。它不是賬號(hào)體系——誰(shuí)拿到口令誰(shuí)就能看到全部會(huì)話、也能替我發(fā)指令。這一點(diǎn)我一開(kāi)始就知道也接受它是單用戶設(shè)計(jì)但它意味著口令本身就是最高機(jī)密。**這套東西跑在一個(gè)「可寫(xiě)層」上不是持久卷。**重啟沒(méi)事數(shù)據(jù)都在但如果哪天把容器刪了重建工作區(qū)里的記憶、項(xiàng)目、配置會(huì)一起沒(méi)。要升級(jí)就先備份。**常駐進(jìn)程的配置冷啟動(dòng)時(shí)可能被重寫(xiě)掉。**我加的后臺(tái)推送進(jìn)程和通知進(jìn)程一度只存在于「運(yùn)行時(shí)」的配置里——容器啟動(dòng)腳本會(huì)用模板無(wú)條件重寫(xiě)那個(gè)配置文件一斷電重啟就靜默消失。發(fā)現(xiàn)后補(bǔ)進(jìn)了模板現(xiàn)在斷電恢復(fù)才真的可靠預(yù)期 3~6 分鐘。**真機(jī)驗(yàn)證不可替代。**安全區(qū)、鍵盤、視口這幾個(gè) bug在無(wú)頭瀏覽器里一個(gè)都復(fù)現(xiàn)不出來(lái)那些環(huán)境變量恒為零、布局視口恒等于可視視口。所以每個(gè)真機(jī) bug 我都補(bǔ)了一條「機(jī)制斷言」進(jìn)驗(yàn)收腳本防止以后再犯——但發(fā)現(xiàn)它們?nèi)匀恢荒芸空鏅C(jī)。十一、小結(jié)回頭看這個(gè)項(xiàng)目的功能部分其實(shí)不復(fù)雜一個(gè)薄前端幾個(gè)接口。真正花時(shí)間的全是**「用起來(lái)之后」才暴露的東西**鎖屏斷線、語(yǔ)音回填、視口不給滿屏、通知要等夠 120 秒才值得發(fā)。如果只允許留一句話我會(huì)留這條「能用」和「好用」之間隔著的不是功能清單是一堆只有真用起來(lái)才會(huì)撞上的坑。而這些坑沒(méi)有一個(gè)是靠「想清楚」能提前繞開(kāi)的——它們只會(huì)在你把它放進(jìn)兜里、鎖上屏、走出門之后才一個(gè)個(gè)冒出來(lái)。(本文由 AI 輔助創(chuàng)作人工審閱后發(fā)布。文中截圖均為真實(shí)使用界面會(huì)話名稱與賬號(hào)信息已打碼脫敏。)