Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .claude/skills/verify-on-device/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ description: >-

## 模式與平台

- **預設是重播**:dev flavor 加內附測試插件(`--fmp-dev-playback`,三首 2 秒的本機音檔、
不連網)。App 還沒有每插件的重播開關(ADR 0015 §決定 5 只做到契約測試),目前重播就是
測試插件。
- **預設是重播**:dev flavor 加內附測試插件 `fmp-test`(以 `--fmp-dev-plugin` 安裝,搜尋任何
關鍵字都有三首 2 秒的本機音檔、不連網;步驟見 `references/runtime-state.md` § 開發入口)。
App 還沒有每插件的重播開關(ADR 0015 §決定 5 只做到契約測試),目前重播就是測試插件。
- **真實連線只在**:改動本身是插件、網路層、登入,或正在錄 fixture。只做最少的操作(搜尋
一次、播一首),不批次、不迴圈。上游改版交給 Debug 頁的健康檢查,不靠每個 PR 的實機驗證。
- **平台**:Android 模擬器與 Windows 每個使用者看得到的 PR 都驗。Linux、macOS、iOS 在各自的
平台任務加進 `references/`,現在不驗。
- UI 還沒有入口的功能,用開發入口(`references/runtime-state.md` § 開發入口)。
- 操作一律走 UI(搜尋頁點一首就開始播);UI 還沒有入口的功能(安裝插件)用開發入口
(`references/runtime-state.md` § 開發入口)。

## 前置

Expand Down Expand Up @@ -64,7 +65,8 @@ terminal 從 repo 根目錄開始)。
| 播放焦點、媒體工作階段 | `dumpsys audio`(Android)、`scripts/smtc_probe.ps1`(Windows,M2 起才有東西) |

優先讀文字(語意樹、log),畫面問題(版面、溢出、主題)才截圖。**截圖與貼進回報的內容不得含
個人資訊**:Windows 的身分頁會印出資料目錄的完整路徑,含使用者名稱;這種截圖不要貼出,只回報文字觀察。
個人資訊**:log 的 `App started` 一行帶資料目錄的完整路徑(含使用者名稱),引用時改成
`<資料目錄>`;畫面上若出現使用者名稱(例如檔案路徑),那張截圖不要貼出,只回報文字觀察。
只截 App 視窗,不截整個桌面。截圖一律存到 session 暫存目錄,不進 repo。

## 3. 操作
Expand Down
25 changes: 13 additions & 12 deletions .claude/skills/verify-on-device/references/android.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,24 @@ adb shell am start -n com.personal.fmp.dev/com.personal.fmp.MainActivity

### 帶開發入口的參數

dev 入口(`--fmp-dev-plugin=`、`--fmp-dev-playback`,見 `runtime-state.md`)以 intent extra
`dart_entrypoint_args` 傳進來。`--esal` 的值是**以逗號分隔的陣列**,所以多個參數用逗號接起來,
參數值本身不能含逗號:
dev 入口(`--fmp-dev-plugin=`,見 `runtime-state.md`)以 intent extra `dart_entrypoint_args` 傳進來。
`--esal` 的值是**以逗號分隔的陣列**,所以多個參數用逗號接起來,參數值本身不能含逗號。

```bash
MSYS_NO_PATHCONV=1 adb shell am start -n com.personal.fmp.dev/com.personal.fmp.MainActivity \
--esal dart_entrypoint_args --fmp-dev-playback
```

用 `--fmp-dev-plugin=` 時,App 要讀得到插件檔:先推到 `/data/local/tmp`(App 讀不到),再以
`run-as` 複製進 App 的私有目錄:
App 要讀得到插件檔:先推到 `/data/local/tmp`(App 讀不到),再以 `run-as` 複製進 App 的私有目錄。
測試插件(重播)是 `app/test/fixtures/plugins/test_plugin/test_plugin.js`,B 站插件(真實)是
`fmp-plugins/bilibili/bilibili.js`:

```bash
MSYS_NO_PATHCONV=1 adb push <插件.js 的本機路徑> /data/local/tmp/x.js
MSYS_NO_PATHCONV=1 adb shell "run-as com.personal.fmp.dev sh -c 'mkdir -p files && cp /data/local/tmp/x.js files/x.js'"
MSYS_NO_PATHCONV=1 adb shell rm /data/local/tmp/x.js
MSYS_NO_PATHCONV=1 adb shell am start -n com.personal.fmp.dev/com.personal.fmp.MainActivity \
--esal dart_entrypoint_args --fmp-dev-plugin=/data/data/com.personal.fmp.dev/files/x.js,--fmp-dev-playback=bilibili:<BV 號>
--esal dart_entrypoint_args --fmp-dev-plugin=/data/data/com.personal.fmp.dev/files/x.js
```

裝好的插件存在資料庫,之後不帶參數啟動也在(搜尋頁的音源 chip)。兩個插件都要時各裝一次,
檔名分開(例如 `test.js`、`bilibili.js`)。

用 `adb shell mkdir` 建的目錄屬於 `shell`,App 寫不進去(`errno = 13`);一律用 `run-as`。
要重新帶參數,先 `adb shell am force-stop com.personal.fmp.dev`(只停 dev)。

Expand All @@ -62,7 +60,7 @@ MSYS_NO_PATHCONV=1 adb shell am start -n com.personal.fmp.dev/com.personal.fmp.M
- **語意樹**:`PYTHONIOENCODING=utf-8 python .claude/skills/verify-on-device/scripts/ax_flatten.py --limit 30`。
預設經 `orca emulator ax`;沒有 Orca 時加 `--adb`,改讀
`adb exec-out uiautomator dump /dev/tty`(直接輸出到 stdout,裝置上不留檔)。Flutter 的語意經
uiautomator 變成節點:多數文字在 `content-desc`,少數(例如身分頁的資料目錄)在 `text`,
uiautomator 變成節點:多數文字在 `content-desc`,少數(例如輸入框的內容)在 `text`,
腳本兩個都讀。`norm=` 餵 `orca emulator tap <x> <y>`,`center=` 餵 `adb shell input tap <x> <y>`。
- **log**:`adb logcat -s flutter`(debug build 的 console log)。結構化的 log 在 App 的
`files/logs/fmp.jsonl`:`adb shell run-as com.personal.fmp.dev cat files/logs/fmp.jsonl`。
Expand All @@ -87,3 +85,6 @@ MSYS_NO_PATHCONV=1 adb shell am start -n com.personal.fmp.dev/com.personal.fmp.M
- 模擬器卡死(畫面不動、`ax` 是 0 個節點)時,以 `-no-snapshot-load` 重開。
- 模擬器的 DNS 不快取,第一次連線多約 1 秒;測錯誤路徑關網路:
`adb shell svc wifi disable && adb shell svc data disable`(驗完開回來)。
- `flutter test integration_test/... -d emulator-5554` 跑完會**解除安裝** `com.personal.fmp.dev`(資料與複製進
`files/` 的插件一起消失)。之後要重新 `adb install -r` dev APK、重新複製插件。
- 播放中畫面一直在更新時 `uiautomator dump` 會回 `could not get idle state`:改用 `screencap` 看版面,或先暫停。
23 changes: 14 additions & 9 deletions .claude/skills/verify-on-device/references/runtime-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ dev 解析到舊版正式資料的位置(Windows 的 `Documents\FMP`、`%APPDA
- `logs/fmp.jsonl`(與輪替的 `fmp.1.jsonl`、`fmp.2.jsonl`):JSON Lines,單檔 2MB,已經過遮蔽。
每行 `{"time","level","tag","message","fields"}`,例如
`{"level":"info","tag":"app","message":"App started","fields":{"flavor":"dev","buildMode":"debug"}}`。
驗證時用 `tag` 與 `message` 找(`Installed a plugin from the development entry`、
`Development playback started`、`Look-ahead handover`、`Track audible`)。
驗證時用 `tag` 與 `message` 找(`App started` 的 `flavor`、`dataDirectory`,
`Installed a plugin from the development entry`、`Track requested`、`Look-ahead handover`、
`Track audible`、`Search failed`、`Playback stopped`)。`dataDirectory` 含使用者名稱,引用時改成
`<資料目錄>`。

## 讀狀態

Expand Down Expand Up @@ -51,16 +53,19 @@ Dart VM Service(`flutter run` 印的 URI)可讀活的物件;URI 是本機

## 開發入口(只在 dev flavor;prod 一律忽略)

原始碼:`lib/plugins/install/dev_plugin_entry.dart`、`lib/playback/dev_playback_entry.dart`。
原始碼:`lib/plugins/install/dev_plugin_entry.dart`。播放一律走 UI:搜尋頁選音源、搜尋、點一首,
就從那一首開始依序播整份結果。

| 入口 | 作用 |
|---|---|
| `--fmp-dev-plugin=<路徑>` 或環境變數 `FMP_DEV_PLUGIN` | 啟動時安裝該路徑的插件安裝檔(`.js`);兩者都有時參數優先 |
| `--fmp-dev-playback` | 安裝內附的測試插件(`fmp-test`),依序播它的三首(`tone-220`、`tone-440`、`tone-880`,每首是同一個 2 秒的本機 wav,不連網) |
| `--fmp-dev-playback=<曲目鍵>` | 播指定曲目(例如 `bilibili:<BV 號>`);重複參數播多首。插件要已安裝,或同時帶 `--fmp-dev-plugin`。要連網,屬於「真實」模式 |
| `--fmp-dev-plugin=<路徑>` 或環境變數 `FMP_DEV_PLUGIN` | 啟動時安裝該路徑的插件安裝檔(`.js`);兩者都有時參數優先。已安裝同 id 的視為更新 |

- 參數不用逗號分隔(Android 的 `--esal` 會切陣列,見 `android.md`)。
| 插件 | 安裝檔 | 模式 |
|---|---|---|
| 測試插件 `fmp-test`(音源名稱 `FMP Test Plugin`) | `app/test/fixtures/plugins/test_plugin/test_plugin.js` | 重播:搜尋任何關鍵字都回三首(第一頁兩首、「載入更多」第三首),串流是 dev 版內附的 2 秒 wav,不連網;關鍵字剛好是 `fail` 時搜尋以限流失敗,用來看錯誤提示 |
| B 站 `bilibili` | `fmp-plugins/bilibili/bilibili.js`(與 FMP 同層的 clone) | 真實:搜尋與解析都連網,照 SKILL.md 只做最少的操作 |

- Windows 直接給絕對路徑;Android 要先複製進 App 的私有目錄(`android.md`)。
- Windows 的環境變數只對該行程有效:PowerShell 用
`$env:FMP_DEV_PLUGIN='<路徑>'; Start-Process ...; Remove-Item Env:FMP_DEV_PLUGIN`。
- 這兩個入口在 PR 12 的 UI 取代之前是驗證播放與插件的入口;之後以原始碼為準。
- 身分頁的 `Dev playback: <狀態> <第幾首>/<總數>` 是播放入口的狀態。
- 裝好的插件留在 `installed_plugins`,之後不帶參數啟動也會載入。
23 changes: 16 additions & 7 deletions .claude/skills/verify-on-device/references/windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ flutter build windows --flavor dev --debug
只有 dev 的視窗標題是 `FMP Dev`)。直接執行,參數照 `runtime-state.md` 的開發入口:

```powershell
Start-Process build\windows\x64\dev\runner\Debug\fmp.exe -ArgumentList '--fmp-dev-playback'
Start-Process build\windows\x64\dev\runner\Debug\fmp.exe -ArgumentList '--fmp-dev-plugin=<app 的絕對路徑>\test\fixtures\plugins\test_plugin\test_plugin.js'
```

- **dev 是單一實例**(mutex `Local\FMP_MainInstance-dev`):別的 worktree 開著的 FMP Dev 也算。
Expand All @@ -33,20 +33,20 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File $S/msaa_tree.ps1 [-Filte
powershell.exe -NoProfile -ExecutionPolicy Bypass -File $S/msaa_tree.ps1 -Click '<名稱>' [-Role 'push button'] [-Index 0]
```

- 第一行 `nodes=<n>`。身分頁約 11 個;之後頁面變多卻只剩個位數,代表無障礙橋卡住
- 第一行 `nodes=<n>`。先在同一個畫面記一次當基準(例如搜尋頁、有結果、播放列在);之後同一個
畫面只剩個位數,或送出提示後節點數掉到個位數、不再變動,代表無障礙橋卡住
(`docs/troubleshooting.md` 的 `Failed to update ui::AXTree`)。
- 輸出的矩形是螢幕上的物理像素。`-Click` 是**真的滑鼠點擊**,先用 `-Filter` 確認名稱;它會把游標
停回視窗左上角(游標下有 tooltip 會讓樹卡住)。
- 身分頁上資料目錄那一行的語意名稱目前是空字串(畫面上看得到);要確認資料目錄就看
`userdata-dev\` 是否存在,或截圖。
- 要確認資料目錄,看 `userdata-dev\` 是否存在,或 log 的 `App started` 的 `dataDirectory`。
- 腳本只找行程 `fmp` 而且視窗標題是 `FMP Dev` 的(`-Proc`、`-Title` 改),不會點到舊版或 prod;
exit 1 是沒有視窗,2 是 `-Click` 沒對到,3 是視窗拉不到前景。

## 截圖

```bash
powershell.exe -NoProfile -ExecutionPolicy Bypass -File $S/window_shot.ps1 \
-Exe <fmp.exe 路徑> [-ArgLine '--fmp-dev-playback'] -Out <png> [-Wait 8] [-KeepRunning]
-Exe <fmp.exe 路徑> [-ArgLine '--fmp-dev-plugin=<插件.js>'] -Out <png> [-Wait 8] [-KeepRunning]
powershell.exe -NoProfile -ExecutionPolicy Bypass -File $S/window_shot.ps1 -Attach -Out <png>
```

Expand All @@ -55,8 +55,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File $S/window_shot.ps1 -Atta
截圖、不結束它。
- 舊專案觀察過 `PrintWindow` 回傳一張不再更新的畫面:兩張截圖逐位元相同時,先別斷定操作沒生效,
用 MSAA 或 log 對照。
- 輸出放 session 暫存目錄。身分頁顯示資料目錄的完整路徑(含使用者名稱),這種截圖不要貼進
PR 或回報;其他頁面確認沒有個人資訊才附。
- 輸出放 session 暫存目錄。確認畫面上沒有個人資訊(使用者名稱、帳號)才貼進 PR 或回報。
- 不要改用整螢幕截圖(`CopyFromScreen`):會截到蓋在上面的其他視窗。

## 操作的陷阱
Expand All @@ -67,6 +66,16 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File $S/window_shot.ps1 -Atta
DPI 縮放下座標與截圖對不上。
- 對主視窗送 `WM_CLOSE` 才是「使用者按 X」;`Stop-Process` 是強殺,測不到關閉流程。

- **中文輸入法**:這台機器預設是中文輸入法時,`SendKeys` 打的英數字會先進組字,第一個 Enter 只是送出
組字、第二個才觸發搜尋;空白鍵會拿去選字(「a」+空白鍵變「日」)。送出搜尋就連按兩次 Enter;要驗
「輸入框內空白鍵只輸入空格」看 log 裡沒有 `Playback state` 即可,或在 Android 用 `adb shell input text`。
- **整合測試會蓋掉 dev 產物**:`flutter test integration_test/... -d windows` 建到同一個
`build\windows\x64\dev\runner\Debug\fmp.exe`,跑完後那裡是測試版,還可能留下一個沒有視窗、沒有 log
的 `fmp.exe`(占住單一實例,之後啟動會直接結束)。跑完整合測試先結束它,再
`flutter build windows --flavor dev --debug`。
- **Narrator**:`Start-Process narrator.exe` 會帶出「快速入門」視窗搶前景(之後的 `-Click` 回 exit 3),
`taskkill /F /IM NarratorQuickStart.exe` 關掉它;Narrator 本身 `taskkill` 會被拒,用 Win+Ctrl+Enter 關。

## SMTC(M2 起才有東西可讀)

```bash
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/verify-on-device/scripts/window_shot.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 只截 App 視窗(不含桌面與其他視窗,截圖不會帶到個人資訊),存成 PNG。
#
# 兩種用法:
# 啟動並截圖: ... -Exe <fmp.exe 路徑> [-ArgLine '--fmp-dev-playback'] -Out <png> [-Wait 8] [-KeepRunning]
# 啟動並截圖: ... -Exe <fmp.exe 路徑> [-ArgLine '--fmp-dev-plugin=<插件.js>'] -Out <png> [-Wait 8] [-KeepRunning]
# 接上已開的: ... -Attach -Out <png> (找行程 -Proc、視窗標題 -Title 的,
# 預設 fmp 與 FMP Dev;舊版與 prod 也叫 fmp.exe)
#
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,16 @@ jobs:
- name: Unit and widget tests
run: flutter test

# golden(alchemist 的 CI 版)比對失敗時,flutter_test 把差異圖寫在
# 各 golden 目錄旁的 failures/;上傳才看得到哪裡不同。
- name: Upload golden failures
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: app-golden-failures
path: app/test/**/failures/
if-no-files-found: ignore

ci-result:
name: CI Result
if: always()
Expand Down
30 changes: 10 additions & 20 deletions .trellis/spec/app/playback/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ lib/playback/
queue_model.dart # QueueModel、QueueState(M1:記憶體、依序)
stream_resolver.dart # StreamResolver、ResolvedStream(期限)
recovery_policy.dart # decideRecovery 與它的輸入、輸出型別(純函數)
playback_providers.dart # audioBackendProvider、playbackControllerProvider、兩個狀態 stream
dev_playback_entry.dart # --fmp-dev-playback(只在 dev)
playback_providers.dart # audioBackendProvider、playbackControllerProvider、狀態/佇列/進度 stream
backends/
audio_backend.dart # AudioBackend 介面、BackendSource、狀態與事件
backend_rules.dart # classifyTrackEnd、LookAheadEdit(兩個後端共用)
Expand Down Expand Up @@ -79,24 +78,15 @@ lib/platform/audio/ # AudioBackendKind、PlayableFormat、PlaybackSuppor

## 實機驗證(ADR 0018 §如何確認)

測試插件的三首都是同一個 2 秒的 `tone.wav`(dev flavor 的 asset),不連網。
建置、安裝、啟動、讀 log 與 `dumpsys audio` 照 `verify-on-device` skill(`.claude/skills/verify-on-device/`)。
播放一律從 UI 開始:以 `--fmp-dev-plugin` 裝測試插件(重播,三首同一個 2 秒的 `tone.wav`、
不連網)或 B 站插件(真實),搜尋後點一首。播放相關要看的:

- Windows:`flutter build windows --flavor dev --debug`,再
`build\windows\x64\dev\runner\Debug\fmp.exe --fmp-dev-playback`。dev 是單一實例,先關掉其他
開著的 FMP Dev(包括別的 worktree 的)。log 在同目錄的 `userdata-dev\logs\fmp.jsonl`。
- Android 模擬器:`flutter build apk --flavor dev --debug`、`adb install -r
build\app\outputs\flutter-apk\app-dev-debug.apk`,再 `adb shell am start -n
com.personal.fmp.dev/com.personal.fmp.MainActivity --esal dart_entrypoint_args
--fmp-dev-playback`。debug build 的 log 在 `adb logcat -s flutter`。模擬器要有聲音輸出
(不是 `-no-audio` 啟動的),ExoPlayer 才會真的播。
- 看交接:`Look-ahead handover`(`previousPositionMs` 是上一首最後回報的位置)與接著的
- 交接:`Look-ahead handover`(`previousPositionMs` 是上一首最後回報的位置)與接著的
`Track audible`:`sinceHandoverMs` 是交接事件到這首第一次回報位置,`estimatedGapMs` 是從
上一首最後的位置推算的結束時間到這首第一次回報位置(含位置回報的間隔,只是估計)。
身分頁的 `Dev playback:` 一行是狀態與第幾首。
- Android 音訊焦點:播放中與交接前後重複 `adb shell dumpsys audio`,看 `Audio Focus stack`
的最上面一直是 `com.personal.fmp.dev`(`AUDIOFOCUS_GAIN`),沒有被 abandon 又重新 request;
播完之後也仍在(just_audio 不主動放)。
- 真實連線(可選、ADR 0027 §決定 2 的最少操作):`--fmp-dev-plugin=<B 站插件>
--fmp-dev-playback=bilibili:<BV 號>` 播一首,看 `Opening stream` 的 `headers` 有
`Referer`。多首就重複 `--fmp-dev-playback=`;Android 的 `--esal` 以逗號分隔陣列,寫成
`--esal dart_entrypoint_args --fmp-dev-plugin=<路徑>,--fmp-dev-playback=bilibili:<BV 號>`。
- Android 音訊焦點:播放中與交接前後,`Audio Focus stack` 的最上面一直是
`com.personal.fmp.dev`(`AUDIOFOCUS_GAIN`),沒有被 abandon 又重新 request;播完之後也仍在
(just_audio 不主動放)。模擬器要有聲音輸出(不是 `-no-audio` 啟動的),ExoPlayer 才會真的播。
- 真實連線(ADR 0027 §決定 2 的最少操作):B 站播一首,看 `Opening stream` 的 `headers` 有
`Referer`。
3 changes: 2 additions & 1 deletion .trellis/spec/app/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ export async function resolveStream({ sourceId, cid, formats }) {

- 測試:`PluginHarness().load(source)`(`test/plugins/plugin_harness.dart`),HTTP 走假 adapter。
- dev 實機:Windows `flutter run --dart-entrypoint-args=--fmp-dev-plugin=<路徑>`,或設環境變數
`FMP_DEV_PLUGIN`;Android 見 `app/AGENTS.md` § 插件的 `adb` 指令。身分頁列出載入的插件。
`FMP_DEV_PLUGIN`;Android 見 `verify-on-device` skill 的 `references/android.md`。有 `search`
能力的插件出現在搜尋頁的音源 chip。

## 加一個宿主 API

Expand Down
Loading
Loading