Repository navigation
Conversation
Av1anJay
left a comment
There was a problem hiding this comment.
Hermes Agent Review — 精簡模式設定說明
Verdict: Changes requested(1 個行為缺陷 + 若干在地化/測試問題)
檢視範圍:origin/main...90193c4(10 檔、+968/−88),已實際 checkout PR 分支逐檔閱讀並執行下列檢查。
檢查項目與結果
gh pr checks 109(head90193c4):全綠 — Verify(flutter analyze→ No issues found;531 tests passed;Web route geometry 2 tests passed)、Android/Wear/iOS/macOS/Windows/Linux/Web build 全 pass。- 本機無 Flutter/Dart toolchain(
flutter、dart皆未安裝),因此未重跑flutter test/dart analyze,改用 CI 結果 + 靜態檢查。 - ARB ↔ 產生檔一致性(自寫腳本):en/zh/zh_TW 各 996 個 key,三份 ARB key 集合完全相同;
app_localizations.dart抽象 getter 805 個、AppLocalizationsEn/Zh/ZhTw各 805 個實作,無缺漏;39 個新*Compactkey 在 3 個 locale class 全部有實作,字串與 ARB 完全相符(0 mismatch)。 - 39 個新 key 在
lib/(排除產生檔)全部有被引用,無死字串。 - 重複 key 掃描:無;placeholder 差異:無。
git diff --check:通過。
缺陷(建議修)
-
lib/screens/settings_screen.dart:929— 精簡模式的廣告說明把「廣告已關閉」狀態吃掉。非精簡分支依_adToggleLocked/enableAds分成三種文案(鎖定、已開啟adsEnabledDescription、未開啟的求情文案),精簡分支只判斷_adToggleLocked。結果:使用者關掉廣告後,精簡模式仍顯示「顯示廣告以支持開發。」,與實際狀態相反。建議保留enableAds判斷,例如_adToggleLocked ? adsLockedDescriptionCompact : (enableAds ? adsDescriptionCompact : <另一句關閉狀態的精簡文案>)。 -
lib/l10n/app_zh.arb:83/app_zh_TW.arb:83(同步影響app_en.arb:83與產生檔) —compactModeDescription從「…關閉後會顯示較完整的說明;桌面版首頁固定使用精簡版面。」改成「縮短說明,精簡介面。」,而這正是唯一告訴使用者「關掉就能看到完整說明」的字串。改完後整個 App 沒有任何地方說明如何取回完整說明,也沒交代桌面版首頁固定精簡。這與 PR 目標(保留必要資訊)相衝突,建議至少補回「關閉後顯示完整說明」語意。
在地化用語(非阻擋,但建議一起修)
app_zh.arb:14/app_zh_TW.arb:14—normalUpdateIntervalDescriptionCompact用「刷新」。全檔 996 個字串中「刷新」只出現這 1 次,其餘一律用「更新/重新取得」(如normalUpdateIntervalDescription的「重新取得即時到站資訊」)。建議改「到站資訊更新間隔;越短越耗電。」app_zh.arb:34、app_zh_TW.arb:34-35— 「鎖屏」是中國用語,同一份檔案既有的backgroundTripDescription/backgroundTripIosDescription都用「鎖定螢幕」。建議統一為「鎖定螢幕」。
測試缺口
- 本 PR 沒有任何測試檔變更,但新增了 39 個字串 × 3 locale 與約 20 處分支。PR 描述引用的 34 項測試全是既有測試,
grep -rn "Compact" test/只找到enableCompactMode的持久化斷言,沒有任何測試驗證精簡模式會顯示*Compact文案。像上面第 1 點這種「少判斷一個狀態」的錯,現有測試完全抓不到。建議在test/settings_navigation_test.dart開關精簡模式後斷言至少 2–3 個代表性字串(一般設定、外觀、廣告)。
覆蓋範圍不一致
personalization_screen.dart只補了 3 個字串(背景圖、配色來源、首頁套用範圍),同頁其他長說明(personalizationAmoledDescription、personalizationHomeGradientDescription、personalizationPerPageDescription)在精簡模式下仍是全文。PR 描述說「涵蓋…個人化」,實際只做了一半。settings_screen.dart:403— 精簡模式直接隱藏資料庫「啟動更新:{mode}」整行。若目的是縮短,建議改成短句而非整行消失,否則使用者無法得知自動更新設定。
命名與格式(nit)
adsDescriptionCompact、adsLockedDescriptionCompact、adDensityDescriptionCompact三個 key 沒有對應的 base key(其餘 36 個都有xxx/xxxCompact成對),破壞命名慣例,之後很難確認「完整版」是哪一條。- 新增行有 6 行超過 80 欄(如
personalization_screen.dart:258、:791、app_localizations_en.dart:126),dart format會重排;CI 沒有 format gate,所以不擋,但建議跑一次。 personalization_screen.dart夾帶了幾處與功能無關的純重排(:442、:958、:967、:1094),讓 diff 多出噪音。
做得好的地方
_buildSettingDescription/_buildSettingSubtitle統一收斂分支,compactDescription ?? description的 fallback 讓未提供精簡文案的項目不會爆掉。- 三份 ARB 與產生檔完全同步,沒有漏 key、沒有空字串、沒有 placeholder 走鐘 — 這是最容易出錯的部分,處理得很乾淨。
localizedAppUpdateChannelDescription/localizedAppUpdateCheckModeDescription用compact:具名參數擴充,沒有破壞既有呼叫端。
| contentPadding: EdgeInsets.zero, | ||
| title: Text(l10n.showAdsTitle), | ||
| subtitle: _adToggleLocked | ||
| subtitle: _compactMode |
There was a problem hiding this comment.
🔴 狀態判斷退化:非精簡分支依 _adToggleLocked 與 controller.settings.enableAds 分成三種文案,精簡分支只看 _adToggleLocked。使用者關閉廣告後,精簡模式仍顯示「顯示廣告以支持開發。」,與實際狀態相反。建議保留 enableAds 判斷(並補一條「已關閉」的精簡文案)。
| "themeModeDark": "深色", | ||
| "compactModeTitle": "精簡模式", | ||
| "compactModeDescription": "減少首頁與部分卡片的輔助說明,讓常用功能更集中,適合熟悉操作後使用。關閉後會顯示較完整的說明;桌面版首頁固定使用精簡版面。", | ||
| "compactModeDescription": "縮短說明,精簡介面。", |
There was a problem hiding this comment.
app_zh.arb:83、app_en.arb:83 與三個產生檔需同步。
| "themeModeDescriptionCompact": "選擇淺色、深色或跟隨系統。", | ||
| "mapProviderDescriptionCompact": "切換底圖,不影響公車資料來源。", | ||
| "wearSyncCategoryDescriptionCompact": "選擇同步至手錶的最愛分類。", | ||
| "normalUpdateIntervalDescriptionCompact": "到站資訊刷新間隔;越短越耗電。", |
There was a problem hiding this comment.
💡 用語不一致:全檔僅此處用「刷新」,其餘 995 條都用「更新/重新取得」(cf. normalUpdateIntervalDescription)。建議「到站資訊更新間隔;越短越耗電。」
| "autoFavoriteDescriptionCompact": "常搭站牌自動加入「常用」最愛。", | ||
| "smartNotificationDescriptionCompact": "常用時段發送到站提醒,需通知與背景定位。", | ||
| "keepScreenAwakeDescriptionCompact": "查看路線時保持螢幕亮起。", | ||
| "backgroundTripDescriptionCompact": "鎖屏繼續乘車提醒,需通知與定位權限。", |
There was a problem hiding this comment.
💡 「鎖屏」為中國用語;同檔既有 backgroundTripDescription / backgroundTripIosDescription 均用「鎖定螢幕」。第 35 行同理,建議一併統一。
| localizedDatabaseAutoUpdateMode( | ||
| l10n, | ||
| controller.settings.databaseAutoUpdateMode, | ||
| if (!_compactMode) |
There was a problem hiding this comment.
💡 精簡模式把「啟動更新:{mode}」整行移除,使用者將無法得知資料庫自動更新設定。若目的是縮短,建議換成短句而非整行消失。
Av1anJay
left a comment
There was a problem hiding this comment.
Hermes Agent Review — 精簡模式設定說明(第二輪,head ae15ac9)
Verdict: Comment(無阻擋問題,可直接合併;下列為非阻擋建議)
前一輪 90193c4 的 REQUEST_CHANGES 已逐項確認修好:
- ✅ 廣告狀態退化 → 精簡分支現在依
_adToggleLocked/enableAds給出三種文案(adsLockedMessageCompact/adsEnabledDescriptionCompact/adsDisabledStatus),關閉廣告不再顯示「已開啟」。 - ✅
compactModeDescription已補回「關閉後顯示完整說明;桌面首頁固定精簡」,三語同步。 - ✅ 「刷新」→「更新」、「鎖屏」→「鎖定螢幕」,用語與全檔一致。
- ✅ 測試缺口已補:新增精簡/完整切換(zh_TW + en)、廣告開啟/關閉/鎖定三態、窄螢幕大字 × 精簡與否。
- ✅ 資料庫「啟動更新:{mode}」不再於精簡模式整行消失。
- ✅ 個人化頁補齊 AMOLED/首頁漸層/每頁背景三條精簡文案。
本次實際檢查
gh pr checks 109(headae15ac9):全綠 — Verify(flutter analyze→ No issues found;537 tests passed;Web route geometry 2 passed),Android/iOS/Wear/Linux/Windows/macOS/Web build 全 pass,Publish Nightly skipped。- 本機無 Flutter/Dart toolchain(
flutter、dart皆未安裝,機器僅 ~1GB RAM),未重跑flutter test/dart analyze,改以 CI 結果 + 靜態腳本查核,不臆測結果。 - ARB ↔ 產生檔一致性:en/zh/zh_TW 各 1000 key、key 集合完全相同、無重複 key;抽象類 809 getter + 191 method,三個 locale 實作數相同、0 缺漏;本次動到的 44 個 key 在 ARB 與產生檔字串完全相符(0 mismatch)。
- 舊 key 清除乾淨:
adsDescriptionCompact/adsLockedDescriptionCompact/adDensityDescriptionCompact在lib/、test/已無殘留引用。 - 39 條
*Compact皆有被引用(除localized_labels.dart內以compact:間接取用者);無死字串。 git diff --check通過;diff 內無 debug print/TODO/FIXME/密鑰樣式。
非阻擋建議
- (inline)
ad_density_setting.dart:34— 副標題已在地化,標題'廣告濃度'仍是硬編碼中文,英文語系會出現中英混排。 - (inline)
ad_density_setting.dart:36— 精簡模式把「等級 + 貓圖 + 分級回饋」整段換成通用提示,使用者調等級後失去任何確認感;建議精簡版仍保留當前等級字樣。 personalization_screen.dart:450— 與功能無關的純重排(且該行 83 欄),建議還原以縮小 diff。- (inline)測試:
personalizationColorSourceDescriptionCompact是本次唯一沒有任何斷言的新字串(僅在配色對話框內使用)。 - 格式:
personalization_screen.dart:264、:344、:797超過 80 欄,dart format會重排;CI 無 format gate 故不擋。
做得好的地方
_buildSettingSubtitle/_buildSettingDescription統一收斂,compactDescription ?? description的 fallback 讓未提供精簡文案的項目不會壞掉。localizedAppUpdateChannelDescription/localizedAppUpdateCheckModeDescription以具名compact:擴充,既有呼叫端零破壞。- 廣告三態在精簡模式下語意正確,且新增測試真的會抓到上一輪那個 bug。
Reviewed by Hermes Agent(head ae15ac9)
| children: [ | ||
| ListTile( | ||
| contentPadding: EdgeInsets.zero, | ||
| title: const Text('廣告濃度'), |
There was a problem hiding this comment.
💡 這個 PR 已經替同一個 ListTile 的副標題接上 AppLocalizations,但標題仍是硬編碼 '廣告濃度'。英文語系使用者會看到「中文標題 + 英文說明」的混排。建議順手加一個 adDensityTitle(en: Ad density)並改用 AppLocalizations.of(context).adDensityTitle。
| fit: BoxFit.contain, | ||
| excludeFromSemantics: true, | ||
| subtitle: settings.enableCompactMode | ||
| ? Text(AppLocalizations.of(context).adDensityLevelHint) |
There was a problem hiding this comment.
💡 精簡模式把「貓圖 + 分級回饋(感謝支持/哇 感謝你的支持…)」整段換成通用提示 adDensityLevelHint,結果是使用者切換 1~4 級後,畫面上完全沒有「目前是哪一級、有沒有生效」的文字回饋(只剩 SegmentedButton 的選中狀態)。若目的是縮短,建議保留等級字樣,例如 '等級 {level}:數值越高,顯示的廣告越多。',而不是整段語意替換。
| }); | ||
|
|
||
| for (final locale in [const Locale('zh', 'TW'), const Locale('en')]) { | ||
| _settingsTest('compact descriptions switch with the setting in $locale', ( |
There was a problem hiding this comment.
💡 新測試覆蓋了語言/天氣/介面縮放、個人化四條與廣告三態,但 personalizationColorSourceDescriptionCompact 是本次新增字串中唯一沒有任何斷言的(只在配色對話框內使用)。建議在既有「開精簡 → 進個人化」段落順手開一次配色對話框斷言它,避免之後改名或漏接。
Av1anJay
left a comment
There was a problem hiding this comment.
Hermes Agent Review — 精簡模式設定說明(第三輪,head 1c12fcc)
Verdict: Approve — 無阻擋問題,可合併。以下兩點為非阻擋的維護性建議。
本次 delta(ae15ac9..1c12fcc,10 檔、+123/−125)
把廣告區塊改回「保留幽默文案 + 精簡模式附加狀態短句」的做法:
- 鎖定態:
cat_laugh.png+adsLockedMessageCompact(「切換過於頻繁,已鎖定為開啟。」)+ 原有adsLockedMessage(「再玩啊哈哈」)。 - 關閉態:
cat_cry/cat_sad.png+adsDisabledStatus(「已關閉廣告。」)+ 隨機求情句。 - 移除
adsEnabledDescriptionCompact/adDensityLevelHint兩個 key,ad_density_setting.dart回到 main 的版本(git rev-parse確認與main:blob 完全相同)。
實際檢查與結果
gh pr checks 109(head1c12fcc):全綠。 Verify job117877201825逐步 success,log 實測:flutter analyze→No issues found! (31.1s);flutter test→ 539 tests passed;Web route geometry → 2 tests passed。Android/Wear OS/iOS/macOS/Windows/Linux/Web build 全 pass,Publish Nightly skipped。- 本機 toolchain:我抓了
flutter 3.41.6(與 CIFLUTTER_VERSION相同)到暫存目錄,成功執行flutter pub get與flutter gen-l10n。但這台機器只有 ~1GB RAM,flutter analyze/flutter test都在編譯階段 thrash 十幾分鐘後被我中止,沒有得到本機結果 — 因此測試結論一律引用 CI 對同一 SHA 的實跑,不臆測。 - l10n 產生檔無漂移(本機實測):
cp -r lib/l10n /tmp/l10n_before && flutter gen-l10n && diff -rq→ 完全一致,lib/l10n/*.dart與 ARB 同步,未手改產生檔。 - ARB 一致性(自寫腳本):en/zh/zh_TW 各 998 key、key 集合完全相同、無重複 key、placeholder 差異 0、空字串 0。
- 移除的 key 無殘留:
grep -rn "adsEnabledDescriptionCompact\|adDensityLevelHint" lib/ test/→ 無命中。 - 新 key 引用:40 個
*Compactkey 在lib/(排除產生檔)全部有被引用,無死字串。 - 呼叫端完整性:
_buildSettingDescription/_buildSettingSubtitle/_buildHistoryAction的每個呼叫點都正確帶上compactDescription;localizedAppUpdateChannelDescription/localizedAppUpdateCheckModeDescription的compact:具名參數沒有漏接的呼叫端。 dart format --output=none --set-exit-if-changed:settings_screen.dart、ad_density_setting.dart、localized_labels.dart、兩個測試檔皆已格式化;personalization_screen.dart有 4 處會被重排,但同樣的 4 處在main上就存在(我把git show main:...丟進 formatter 比對確認),屬既有問題,不是本 PR 引入。git diff main...HEAD --check:通過。
非阻擋建議
-
test/ad_settings_test.dart:112—AdDensitySetting已回到 main 版本、完全不讀enableCompactMode,但測試仍用for (final compactMode in [false, true])展開。結果 4 個 test case 其實只有 2 種唯一行為,compact=true/false是同義重複,test 名稱卻聲稱在驗精簡模式。建議移除這層迴圈(或改回單層),避免以後有人誤以為這裡有精簡分支。 -
lib/screens/personalization_screen.dart:459—personalizationColorSourceDescriptionCompact是本次新增 40 個字串中唯一完全沒有任何斷言的(只在配色對話框內使用)。上一輪已提過,本輪仍未補。建議在既有「開精簡 → 進個人化」段落順手開一次配色對話框斷言它,成本很低。
做得好的地方
- 廣告三態在精簡模式下狀態正確且幽默文案完整保留:鎖定/關閉各有獨立短句,且關閉時不再誤顯示「已開啟」。這是前兩輪最主要的缺陷,現在處理得很乾淨。
- 果斷移除
adsEnabledDescriptionCompact/adDensityLevelHint兩個為了「縮短」而犧牲文案特色的 key,讓ad_density_setting.dart完全回到 main,diff 淨變化為零 — 比留著半套精簡邏輯好。 - ARB 與產生檔完全同步、無重複 key、無殘留引用;
flutter gen-l10n重跑零漂移。
Reviewed by Hermes Agent — checks: CI@1c12fcc(analyze + 539 tests + 2 web tests,全綠)、flutter gen-l10n 漂移比對、ARB key/placeholder 稽核、死 key 掃描、dart format 檢查、git diff --check
| body: SingleChildScrollView( | ||
| padding: EdgeInsets.all(20), | ||
| child: AdDensitySetting(), | ||
| for (final compactMode in [false, true]) { |
There was a problem hiding this comment.
💡 非阻擋:這層迴圈已經是同義重複。 ad_density_setting.dart 在本 PR 已回到 main 版本(git rev-parse HEAD:lib/widgets/ad_density_setting.dart == main: blob),完全不讀 enableCompactMode,所以 compactMode=false 與 compactMode=true 跑的是完全相同的行為。4 個 case 只剩 2 種唯一行為,但 test 名稱寫著 with compact=$compactMode,會讓後人誤以為這裡有精簡分支要顧。建議移除這層 for(或至少在 AdDensitySetting 真的再分支時才加回來)。
| AppLocalizations.of( | ||
| context, | ||
| ).personalizationColorSourceDescription, | ||
| settings.enableCompactMode |
There was a problem hiding this comment.
💡 非阻擋:personalizationColorSourceDescriptionCompact 仍無任何測試斷言。 它是本次新增 40 個 *Compact key 中唯一完全沒被測試碰到的(grep -rc 在 test/ 為 0),而且只在這條對話框路徑上使用,屬最容易在之後改名/漏接時靜默壞掉的一條。建議在既有「開精簡 → 進個人化」的測試段落順手開一次配色對話框斷言它即可,成本很低。
Av1anJay
left a comment
There was a problem hiding this comment.
Hermes Agent Review — head ea81541(Node.js CVE-2026-111497 硬化)
Verdict: Comment(自審限制,GitHub 不允許對自己的 PR 按 APPROVE/REQUEST_CHANGES)— 但合併前必須先解衝突,因為目前 head 完全沒有 CI 覆蓋。
這個 commit 從哪來
ea81541 是今天新推的 commit,與 PR 標題「shorten descriptions in compact mode」無關:它是 10 檔、+105/−7 的 Node.js 執行環境硬化(.node-version、engines、preinstall 守門、wrangler 部署改走 npm run deploy:web)。PR 描述(仍在講 42 項測試那版)完全沒有提到這件事,reviewer 從 PR 頁面看不出範圍變了。
🔴 阻擋:head 沒有任何 CI 結果,且 PR 目前 CONFLICTING
gh pr view 109→mergeable: CONFLICTING、mergeStateStatus: DIRTY。repos/.../actions/runs?head_sha=ea81541…→ total_count = 0;branch 上最後一次 Build 是1c12fcc(2026-10-19,success)。GitHub 在無法計算 merge commit 時不會觸發pull_requestworkflow,所以這個 commit 的 workflow 改動(setup-node、npm run test:node、deploy 改寫)一次都沒跑過。- 我實測
git merge origin/main→ 2 個衝突:lib/screens/settings_screen.dart:1182–1207— main 的eb5eb86(手動檢查更新改版)把 app-updates 區塊重寫成_buildSettingDescription(l10n.appUpdateManualCheckDescription)+_manualAppUpdateResult,本 PR 同處是compactDescription:版本。test/settings_navigation_test.dart:162–362— 同一段測試。
解衝突時不能只挑一邊:_buildSettingDescription的簽名已被本 PR 改成 optional namedcompactDescription,要保留 main 的新 UI 結構再把精簡文案接上去。
⚠️ 建議:把 ea81541 拆成獨立 PR
理由不只是「整潔」:這個安全修正現在被一段完全無關的在地化衝突卡住,而它本身其實已經可以合併。拆出來能立刻 merge,也讓 CI 真的跑到 workflow 改動。
✅ 這個 commit 本身:實測通過
(本機抓了 node-v24.22.1,與 .node-version 一致)
| 檢查 | 結果 |
|---|---|
npm run test:node(Node 24.22.1) |
3/3 pass |
npm run test:node(無 node_modules) |
pass — 測試不依賴任何套件 |
npm ci(Node 24.22.1) |
success,37 packages |
npm ci(Node 22.23.1,未修補) |
EBADENGINE, exit 1 — 擋住了 |
npm run deploy:web / npm run serve:web(Node 22.23.1) |
兩者都在 predeploy/preserve 被擋,exit 1 |
node scripts/stamp-web-build.mjs(Node 24.22.1,假 build/web) |
正確寫入 sw.js + version.json,連跑兩次輸出完全相同(idempotent) |
npx wrangler deploy --dry-run |
success,正確讀到 workers/web/index.mjs + assets |
npm install --package-lock-only |
lock 無漂移(唯一差異是我 clone 目錄名) |
actionlint 1.7.7 兩份 workflow |
clean |
版本判定真值表(11 組,含 24.22.1-rc.1、24.22.1 尾空格、25.0.0、27.0.0) |
全部符合預期 |
git diff --check |
clean |
| Flutter 相關 | 未執行 — 本機無 Flutter toolchain(機器僅 ~1GB RAM),不臆測結果 |
💡 非阻擋建議(詳見 inline)
.npmrc:1—engine-strict=true讓preinstall那句寫得很好的中文錯誤訊息變成永遠看不到的死碼。實測 Node 22.23.1 下npm ci直接吐 npm 的EBADENGINE / Required: {"node":"^22.23.4 || …"},> preinstall根本沒執行。scripts/check-node-version.mjs:26— 版本下限同時存在於package.json engines、這個 Map、README、測試字面值共 4 處,下次 Node 發新修補版很容易只改一處。scripts/check-node-version.mjs:26— 模組頂層副作用讓test/node_version_test.mjs的import在舊版 Node 上直接 throw,整個測試檔載入失敗,「blocks vulnerable runtimes」這個 case 永遠不可能在它描述的環境下被驗證。package.json:9—preserve:web只擋 npm 入口;直接flutter run -d web-server或npx wrangler deploy仍完全繞過守門。README.md:160— 停用wrangler dev後,workers/web/index.mjs(AASA、assetlinks、快取標頭、SPA fallback)沒有本地驗證途徑,repo 內也沒有任何針對它的測試。
做得好的地方
- 用
engines+engine-strict+preinstall+predeploy四層防線,而不是只在 README 寫一行「請升級」— 而且npm ci實測真的會被擋。 - 版本判定沒用字串比較,而是逐段數字比較;prerelease(
-rc.1)與v前綴都正確落到 reject 分支。 .node-version由actions/setup-node的node-version-file讀取,CI 與本機同一來源,不寫死兩份。stamp-web-build.mjs改成import './check-node-version.mjs',讓部署路徑無論怎麼呼叫都受保護,且 idempotent(我實測重跑不影響BUILD_VERSION)。- README 老實交代了
workerd內嵌舊 Brotli 這個無法靠升級修掉的限制,並說明 App 端brotli是純 Dart 實作 — 沒有假裝修好了。
Reviewed by Hermes Agent — checks: npm run test:node、npm ci(24.22.1 / 22.23.1)、deploy:web/serve:web 守門實測、stamp-web-build idempotency、wrangler deploy --dry-run、lock 漂移比對、actionlint 1.7.7、版本真值表、git merge origin/main 衝突定位、GitHub Actions run 查詢
| @@ -0,0 +1 @@ | |||
| engine-strict=true | |||
There was a problem hiding this comment.
engine-strict=true 會讓 scripts/check-node-version.mjs 裡那句解釋 CVE 的錯誤訊息變成永遠看不到的死碼。
實測(Node 22.23.1):
npm error code EBADENGINE
npm error notsup Required: {"node":"^22.23.4 || ^24.22.1 || ^26.12.1"}
npm error notsup Actual: {"npm":"10.9.8","node":"v22.23.1"}
> preinstall 根本沒被執行 — npm 在解析階段就 abort 了。也就是說最需要看到「這是 CVE-2026-111497,請用 22.23.4+/24.22.1+/26.12.1+」的使用者,只會看到 npm 的原文訊息。
兩個選項:
- 移除
.npmrc的engine-strict,讓engines只當 warning、由preinstall給出可讀訊息(安裝仍會被擋,只是擋在 preinstall)。 - 保留
engine-strict,但接受preinstall的訊息只對「已通過 engines 但語意不符」的邊角情況生效 — 那就別在 README 宣稱使用者會看到友善提示。
| } | ||
| } | ||
|
|
||
| requireSafeNode(); |
There was a problem hiding this comment.
💡 兩個問題集中在這一行。
1. 版本下限重複 4 份:package.json 的 engines.node、這個 patchedVersions Map、README:155、test/node_version_test.mjs:7,12。下次 Node 為新 CVE 再發修補版(例如 24.23.x),很容易只改 package.json 而漏掉這裡,然後 engines 放行、preinstall 卻擋下來,使用者完全無法理解。建議直接從 package.json 解析 engines.node 推導下限,或反過來由這個 Map 產生 engines 並加一個測試斷言兩者一致。
2. 模組頂層副作用:requireSafeNode() 在 import 時就執行,於是 test/node_version_test.mjs 的 import { requireSafeNode } 在未修補的 Node 上會直接 throw,整個測試檔載入失敗 — blocks vulnerable, EOL, unsupported and prerelease runtimes 這個 case 永遠不可能在它描述的環境下被驗證。把這行移到 if (import.meta.main) 之類的進入點判斷(或拆成 cli.mjs),測試才能真正測到函式行為。
| "scripts": { | ||
| "serve:web": "wrangler dev", | ||
| "preinstall": "node scripts/check-node-version.mjs", | ||
| "preserve:web": "node scripts/check-node-version.mjs", |
There was a problem hiding this comment.
💡 守門只覆蓋 npm 入口。npm run serve:web → preserve:web 會擋(實測 Node 22.23.1 下 exit 1,正確),但:
flutter run -d web-server直接跑(README 也是這樣教)完全繞過。npx wrangler deploy繞過predeploy:web,也就繞過stamp-web-build.mjs裡的檢查。
既然 README 已經寫「Web 開發與部署命令會拒絕舊版 Node.js」,建議要嘛把 serve:web 的實作也納入(例如包一層 node scripts/check-node-version.mjs && flutter run …,而不是靠 preserve 這種容易被 npm 版本差異影響的隱式 hook),要嘛把 README 的措辭縮小到「npm scripts」。
| 安裝、Web 開發與部署命令會拒絕舊版 Node.js;只更新 npm 套件無法修補 Node 內建的解碼器。 | ||
| App 的 `brotli` 套件為純 Dart 實作,未連結此次受影響的原生 C 解碼器。 | ||
| `npm run serve:web` 改用 Flutter Web 開發伺服器(`http://127.0.0.1:8080`)。 | ||
| Wrangler 依賴的本機 `workerd` 仍內嵌舊版 Brotli,因此暫停以 `wrangler dev` |
There was a problem hiding this comment.
💡 停用 wrangler dev 之後,workers/web/index.mjs 沒有任何本地驗證途徑,而 repo 內也沒有任何針對它的測試(grep -rln "workers/web" test/ scripts/ 無命中)。
這支 Worker 不是單純靜態檔案伺服器,它處理:/.well-known/apple-app-site-association(AASA,改壞會讓 iOS Universal Links 與 App Clip 靜默失效)、assetlinks.json、sw.js/firebase-messaging-sw.js 的 no-store、字型的長快取,以及 SPA fallback。這些現在只能靠部署到正式站後人工驗證。
建議至少補一個 node --test 的單元測試,直接 import worker 的 fetch handler 打幾個 request(/sw.js、/.well-known/apple-app-site-association、未知路徑)斷言 header 與 body — 純 Node、不需要 workerd,也不會碰到 Brotli。否則這次為了安全而移除本機 dev server 的代價,是讓一條原本就沒測試的關鍵路徑變得更不可驗證。
啟用精簡模式後,設定仍顯示完整的長篇說明,造成頁面過長。改為保留簡短易懂的說明,例如「常搭站牌自動加入『常用』最愛」,交代必要權限及清除紀錄的影響;關閉精簡模式後仍顯示完整說明,並保留桌面首頁固定精簡的提示。
涵蓋一般設定、個人化、Wear OS 與更新選項,同時縮小設定頁留白,補齊繁體中文與英文文案及本地化產生檔。廣告切換與濃度保留原有幽默文案、求情提示和貓圖;精簡模式另以短句標示廣告已關閉或已鎖定,維持狀態正確。資料庫摘要保留目前自動更新模式。
驗證:
flutter test --no-pub test/settings_navigation_test.dart test/ad_settings_test.dart test/primary_surfaces_localization_test.dart:42 項通過。涵蓋中英文精簡/完整說明切換、個人化文案、資料庫更新摘要、廣告三種狀態與幽默提示,以及精簡模式、窄螢幕和大字版面的貓圖與廣告濃度吐槽。dart analyze lib/screens/settings_screen.dart lib/widgets/ad_density_setting.dart test/settings_navigation_test.dart test/ad_settings_test.dart:無問題。git diff --check:本次修改通過。