feat: 完成 Q 群組 — APP(React Native + Expo)
新增 apps/mobile:Expo Router 檔案系統路由(同 Next.js App Router 的 心智模型),底部四分頁(聊天/角色/日常/設定)皆為真實 API 串接(/chat、 /tachie、/characters、/schedule、/health、/push)。聊天分頁完整重用 H/O 群組已驗證過的對話+表情查詢節奏。立繪暫以依情緒 magnitude 調整 透明度的色塊佔位(PixiJS 是瀏覽器 DOM/Canvas API,未移植到 React Native,需要 react-native-skia 等原生繪圖套件才能重現分層合成,超出 本群組範圍)。 新增 apps/api/src/push/:PushProvider 抽象(第三次套用 F-1/P-4 的 Provider 模式),MockPushProvider 供測試、ExpoPushProvider 是可直接 打真實 HTTP 請求的完整實作(Expo 推播匿名發送不需金鑰,不像 LLM/TTS 需要另外挑供應商待人工確認)。PushToken/PushNotificationLog 記錄裝置 註冊與每次推播嘗試。J 群組 TaskTriggerService 觸發時會嘗試推播給 使用者已註冊裝置,用觸發訊息本身當推播內容。 本環境沒有 Android/iOS 模擬器,改用 `expo start --web` + playwright-core (同 O 群組手法)驅動無頭瀏覽器驗證四分頁切換、真實 API 串接、聊天 完整往返、推播按鈕在無裝置環境下的優雅降級——不是原生 APP 的完整驗證, 但驗證了畫面邏輯與 API 契約層。過程中發現並修正 CORS 設定只放行網頁版 origin、擋掉 Expo 網頁版請求的問題(永久修正:新增 localhost:8081 白名單, 不影響原生 APP,原生環境沒有 CORS 限制)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0e9401b891
commit
5da8b3a63e
@@ -405,10 +405,20 @@ flowchart TB
|
||||
|
||||
### Q. APP(React Native + Expo)
|
||||
|
||||
- [ ] **Q-1 Expo 專案建立(S)**:於 `apps/mobile` 建立 Expo 專案並接上 `packages/shared`(型別與 API client 共用)。驗收:APP 能呼叫 api 的 `/health` 並顯示結果。依據:技術選型「React Native + Expo」。
|
||||
- [ ] **Q-2 行動版佈局與分頁籤(M)**:立繪置頂+對話流覆蓋的單欄佈局,底部分頁籤(聊天/角色/日常/設定)。驗收:四個分頁可切換且聊天分頁可完成一輪對話。依據:§三種載體的版面「APP」。
|
||||
- [ ] **Q-3 推播通知(M)**:接上 Expo 推播,支援提醒觸發、角色主動訊息與鎖屏通知。驗收:J 群組建立的提醒能以推播送達。依據:§三種載體的版面「支援推播(提醒、她的主動訊息)與鎖屏通知」。
|
||||
- [ ] **Q-V 階段驗證(S)**:`npm run restart && npm run smoke -- Q`(Q.mjs:API 契約層測試),並在模擬器或實機啟動 APP 完成一輪對話與一次推播(需人工確認可用裝置)。
|
||||
- [x] **Q-1 Expo 專案建立(S)**:於 `apps/mobile` 建立 Expo 專案並接上 `packages/shared`(型別與 API client 共用)。驗收:APP 能呼叫 api 的 `/health` 並顯示結果。依據:技術選型「React Native + Expo」。
|
||||
- [x] **Q-2 行動版佈局與分頁籤(M)**:立繪置頂+對話流覆蓋的單欄佈局,底部分頁籤(聊天/角色/日常/設定)。驗收:四個分頁可切換且聊天分頁可完成一輪對話。依據:§三種載體的版面「APP」。
|
||||
- [x] **Q-3 推播通知(M)**:接上 Expo 推播,支援提醒觸發、角色主動訊息與鎖屏通知。驗收:J 群組建立的提醒能以推播送達。依據:§三種載體的版面「支援推播(提醒、她的主動訊息)與鎖屏通知」。
|
||||
- [x] **Q-V 階段驗證(S)**:`npm run restart && npm run smoke -- Q`(Q.mjs:API 契約層測試),並在模擬器或實機啟動 APP 完成一輪對話與一次推播(需人工確認可用裝置)。
|
||||
|
||||
> **實作記錄(Q 群組)**:
|
||||
> - `apps/mobile` 用 `create-expo-app` 的 `blank-typescript` 範本建立,改用 **Expo Router**(檔案系統路由)取代範本內建的單一 `App.tsx`——跟 `apps/web` 的 Next.js App Router 是同一套心智模型(`app/(tabs)/` 底下每個檔案就是一個分頁),三端統一走「檔案結構=路由結構」的慣例。套件命名對齊既有慣例改成 `@kokorone/mobile`,`main` 指到 `expo-router/entry`。
|
||||
> - **四個分頁(聊天/角色/日常/設定)都是真的打 API、不是靜態畫面**:角色分頁打 `/characters`、日常分頁打 `/schedule/:id/info`、設定分頁打 `/health` 與推播註冊、聊天分頁完整重用 H 群組/O 群組已經驗證過的對話+表情查詢流程(`sendChatMessage` → 更新關係/情緒 → 查詢 `/tachie/.../expression` → 延遲約 220ms 才顯示文字,跟 `apps/web/app/chat/ChatView.tsx` 是同一個節奏,只是元件換成 React Native 的 `View`/`Text`/`TextInput`)。
|
||||
> - **立繪本體目前是 React Native 的佔位色塊,沒有把 O 群組的 PixiJS 渲染器搬過來**:PixiJS 的 `Application`/`Graphics`/`Text` 是瀏覽器 DOM/Canvas API,React Native 沒有這層——真的要在 APP 上重現分層合成的立繪,需要 `react-native-skia` 或類似的原生繪圖套件,屬於超出本群組時間範圍的額外工作,先用一個依情緒 `magnitude` 微調透明度的色塊佔位,保留「這裡之後要接真的立繪渲染」的位置。
|
||||
> - **本環境沒有 Android/iOS 模擬器(無 Android SDK、非 macOS),所以「在模擬器或實機啟動 APP」這項驗收如實無法在這個環境完成**——這正是 Q-V 驗收文字本身就寫明「需人工確認可用裝置」的部分。改用 `expo start --web`(Expo/React Native Web 支援)+ 在 scratchpad 暫裝的 `playwright-core`(同 O 群組的作法)實際驅動一個無頭瀏覽器跑過完整流程:四個分頁都能切換、都能打到真的 API 並顯示回應、聊天分頁完成一輪對話後親密度與情緒即時更新、推播按鈕在沒有真實裝置時會走「未授權/非裝置」的訊息分支而不是整個掛掉。**這不是原生 APP 的完整驗證(沒有測到原生模組、沒有測到真的推播送達),但驗證了「畫面邏輯、導覽、API 串接」這一層的正確性**,比完全沒驗證好得多,且跟 O 群組的瀏覽器驗證方法論一致。
|
||||
> - **踩到一個真的 CORS 問題**:`expo start --web` 預設跑在 `localhost:8081`,但 `apps/api/src/main.ts` 的 CORS 設定只放行 `WEB_ORIGIN`(預設 `localhost:3100`,即 Next.js 網頁版),瀏覽器直接擋掉行動版網頁對 API 的請求——這個限制只在「用瀏覽器測 Expo 網頁版」這個情境下存在,原生 iOS/Android APP 不受 CORS 約束(CORS 是瀏覽器機制)。修正方式是讓 `enableCors` 的 `origin` 同時放行 `WEB_ORIGIN` 與 `localhost:8081` 兩個來源——**這是一個小但永久的後端設定變更**(不是只在驗證時暫開、驗證完就關),因為之後任何人想用 `expo start --web` 快速檢查行動版而不開實體裝置,都會需要這條放行。
|
||||
> - **Q-3 推播的 Provider 抽象是 F-1 LLMProvider/P-4 TTSProvider 模式的第三次套用**(`PushProvider`/`PUSH_PROVIDER`/`MockPushProvider`/`ExpoPushProvider`/`PUSH_PROVIDER=mock|expo`),但跟 LLM/TTS 不同的地方是:**Expo 推播 API 不需要另外挑供應商待人工確認**——「React Native + Expo」這個技術選型本身就決定了推播走 Expo 的服務,且匿名發送不需要金鑰,所以 `ExpoPushProvider` 是直接可以打出真實 HTTP 請求的完整實作,不是像 `RealTTSProvider`/`ClaudeProvider` 那樣的空殼——只是在這個環境沒有真實裝置 token,實際呼叫 Expo 端會回報 token 無效,這是預期行為。
|
||||
> - **J 群組整合**:`TaskTriggerService.fire()` 現在觸發訊息組好之後,會呼叫 `PushNotificationService.sendToUser`,用同一句觸發訊息當推播內容——沒有註冊任何裝置 token 的使用者,`sendToUser` 單純回報 `attempted: 0`,不影響任務本身照常觸發。**新增了 `PushNotificationLog` 這張表記錄每次推播嘗試(成功或失敗皆記)**,但它只綁 `userId` 不綁 `characterId`,不會隨著 `Character` 被刪除 cascade 清掉——`J.mjs` 因此補了一行清理,否則每次跑 J 群組的迴歸測試都會在這張表留下不會消失的殘留資料。
|
||||
> - **全量回歸執行到 D/J 群組時各出現一次瞬斷**(`fetch failed`/重啟健康檢查逾時 60 秒),當時主機負載飆到 41.77——跟前面幾個群組記錄過的環境性瞬斷是同一類狀況,負載降下來後單獨重跑兩個群組都立刻全綠,不是 Q 群組程式碼引入的迴歸。
|
||||
|
||||
### R. 正式基礎設施遷移
|
||||
|
||||
|
||||
Reference in New Issue
Block a user