visionA/local-tool/.autoflow/05-implementation/reviews/review-m8-10a-builtin-data-dir-fix.md
jim800121chen d7cddf364b fix(local-tool): 拆 built-in / user data dir — 修預設模型永遠載入 0 個
M8-10a smoke test 抓到 M1 就潛藏的 P0 latent bug:server 預設 dataDir =
<binary>/data,bundle 內解析成 Contents/Resources/bin/data/(空目錄),
實際 models.json + 8 個 .nef 住在 Contents/Resources/data/(上一層)。
Wails 又傳 --data-dir 成 user home(writable),同樣沒 models.json。
結果任何正式啟動路徑下 /api/models 都回 total: 0,M1-M7 smoke 從沒
跑過這個 endpoint 才漏抓。

修法:把「read-only bundle 內資料」和「writable user 資料」語意拆開。

- 新增 findFirstExisting(candidates, sentinel) helper
- 新增 resolveBuiltInDataDir:①VISIONA_BUNDLE_LIB_DIR/data(AppImage)
  ②<base>/data ③<base>/../data ④<base>/../Resources/data
  ⑤<base>/../lib/visiona-local/data(Linux FHS)。命中條件是
  models.json 存在為 regular file,避開 Wails build artifact 留下的
  空 data/ 目錄
- main() 拆 builtInDataDir(modelRepo + flash.Service 用)與 dataDir
  (custom-models / sentinel / logs 用),職責分明
- flash.NewService 改吃 builtInDataDir — 它要解析 models.json 裡的
  相對路徑 "data/nef/kl520/xxx.nef",來源是 bundle 不是 user home
- resolveBridgeScript 同步修(同樣的技術債一起清),候選 env var 優先
  + FHS fallback,避免 Linux AppImage 上 kneron_bridge.py 也找不到
- fallback 全 filepath.Abs 化,log.Printf 印嘗試過的路徑清單便於除錯

驗證(build / vet / test + smoke)全綠:
- macOS bundle:/api/models → 15 models 
- dev mode(server/ 下 go run):15 models 
- Linux AppImage 模擬 + env var:命中候選 1 
- Linux AppImage 模擬 + 無 env var:命中候選 5(FHS)
- 全不命中:log 印完整 tried 清單 + server 不 crash 

Reviewer 兩輪通過。第一輪抓到 Linux AppImage 未覆蓋(Major-1)+ 2 Minor
+ 2 Suggestion;第二輪確認全部處理到位、新發現兩項非阻擋風格建議列為
技術債。報告見 .autoflow/05-implementation/reviews/review-m8-10a-*.md。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:23:25 +08:00

21 KiB
Raw Blame History

Code Review 報告 — M8-10a P0 Latent Bug 修復built-in data dir 解析)

審查摘要

  • 審查對象server/main.go(新增 resolveBuiltInDataDir helper + main() 拆分 builtInDataDir / dataDir 兩個變數)
  • 關聯檔server/internal/flash/service.go(接收參數由 dataDirbuiltInDataDir,無 body 變動)
  • 產出 AgentBackend
  • 審查結果⚠️ 需修 1 個 MajorLinux AppImage 布局未覆蓋)後通過
  • 問題統計Critical: 0 / Major: 1 / Minor: 2 / Suggestion: 2

獨立驗證結果

驗證項 指令 結果
Build cd server && go build -o /tmp/rv-server . PASS
Vet cd server && go vet ./... PASS
Test cd server && go test -count=1 ./... 全綠api / api/handlers / api/ws / device / model 5 個有測試的 package 全部通過)
Smoke (bundle) 直接跑 .../Contents/Resources/bin/visiona-local-server -port 3801 -data-dir $(mktemp -d) Built-in data dir: .../Contents/Resources/dataLoaded 15 built-in modelsGET /api/models 回 15 個 modelkl520-yolov5-detectionfilePath = data/nef/kl520/kl520_20005_yolov5-noupsample_w640h640.nefbundle 實體 .nef 檔存在7,506,224 bytes與 catalog modelSize: 7200000 為人工估值合理吻合)
Smoke (dev mode) 把 binary 放到 server/ 下跑 ./rv-server-tmp -port 3802 -data-dir $(mktemp -d) Built-in data dir: /Users/jimchen/visionA/local-tool/server/dataLoaded 15 built-in modelsGET /api/models 回 15 個 model

修復前的 bug 無法在修復後重現:不管 Wails 傳什麼 --data-dirbuiltInDataDir 都從 bundle 內解析,/api/models 保證回 > 0 個 model。


所有候選布局覆蓋狀態

布局 binary 位置 data 位置 命中候選 覆蓋狀態
macOS bundle Contents/Resources/bin/visiona-local-server Contents/Resources/data/ <base>/../data 已驗證
Dev modego run / 直接在 server/ 下) server/visiona-local-server(或 cwd server/data/ <base>/data 已驗證
Windows installerInno Setupinstaller/windows/visiona-local.iss L72/L86 {app}\bin\visiona-local-server.exe {app}\data\ <base>/../data 理論命中(未在 Windows 機器實跑,但 Inno 布局 = base = {app}\bin<base>/../data = {app}\data,路徑對齊)
Linux AppImageinstaller/linux/build-appimage.sh L29-33, L80 $APPDIR/usr/bin/visiona-local-server $APPDIR/usr/lib/visiona-local/data/ (三個候選算出來分別是 /usr/bin/data/usr/data/usr/Resources/data,沒一個對) 不覆蓋 — 見 Major-1

文件符合性檢查

這次是 P0 bug fix既有行為修復依 Autoflow S 級規則不需新增 PRD/TDD 條目。修復與現有 TDD §架構決策bundle 內唯讀 vs user home 可寫分離)一致——事實上修復才讓程式碼行為與 TDD 的決策真正對齊(修復前是文件對但程式碼錯)。


問題清單

Critical必須修復阻擋交付

無。

Major必須修復但不阻擋 macOS 初步交付)

# 檔案 行數 問題描述 建議修改方式
M-1 server/main.go 95-112 resolveBuiltInDataDir 的三個候選沒有覆蓋 Linux AppImage 布局。AppImage 把 server binary 放 usr/bin/、data 放 usr/lib/visiona-local/data/(見 installer/linux/build-appimage.sh L29-33, L80三個現有候選 <base>/data<base>/../data<base>/../Resources/data 都不會命中。結果Linux 版 AppImage 啟動後 built-in catalog 依然載入 0 個 model等於這個 P0 bug 在 Linux 上沒被修掉。另外 AppRun 已經 export VISIONA_BUNDLE_LIB_DIR=${HERE}/usr/lib/visiona-localL134server 目前完全沒讀這個 env var。備註:同樣的缺失其實也存在於先前就有的 resolveBridgeScriptL60-78不是本次 PR 引入的 regression但應同時修掉。 建議兩層保險同時做:①在 resolveBuiltInDataDir 開頭優先讀 VISIONA_BUNDLE_LIB_DIR env若非空且 <env>/data/models.json 存在則直接回 <env>/data);②新增第 4 個候選 <base>/../lib/visiona-local/data(對齊 FHS 布局,未來其他 Linux 打包方式也能用)。resolveBridgeScript 同步比照修掉,避免 Linux 版 Kneron bridge 也找不到。

Minor建議修復

# 檔案 行數 問題描述 建議修改方式
m-1 server/main.go 110-111 三個候選全部沒命中時回的 fallback 是 filepath.Join(base, "data"),不是 filepath.Abs(...) 後的絕對路徑,與成功路徑回的是絕對路徑不一致(成功分支有 filepath.Abs(c))。雖然 model.NewRepository 還是能用相對路徑讀檔,但 log 裡印出來的 Built-in data dir: ... 會是相對路徑,讓除錯變難。 fallback 也過一次 filepath.Absif abs, err := filepath.Abs(filepath.Join(base, "data")); err == nil { return abs }; return filepath.Join(base, "data")
m-2 server/main.go 95-112 函式沒 log 自己試了哪幾條路徑。落到 fallback 時 NewRepository 只會印 Warning: could not load models from <path>使用者看不到「我還試了另外兩個位置都沒中」。bug reporting 體驗不佳。 在 fallback 分支 return 前用 log.Printf(或注入 logger印出嘗試過的三個候選路徑或在函式簽名改成接收 logger。

Suggestion非必要改善建議

# 檔案 行數 建議內容
s-1 server/main.go 95-112 resolveBuiltInDataDirresolveBridgeScriptL60-78兩個函式結構幾乎一模一樣都是「一組候選 → 找存在的 → fallback 回第一個」)。可以抽一個通用 findFirstExisting(candidates []string, sentinel string) (string, bool),讓兩者各自只定義候選清單和 sentinel 檔案(models.json vs kneron_bridge.py),減少未來「改了一個忘了改另一個」的風險。
s-2 server/main.go 146-149 dataDircfg.DataDir == "" 時 fallback 到 builtInDataDir= bundle 內 read-only 目錄),但這只服務 dev mode。實際執行時 sentinel (.first-ws-connected)、custom-models、logs 都可能想寫進去,而 bundle 內目錄在 prod 是 read-only。雖然 dev mode 下通常有寫權限但把「writable user dir fallback 到 read-only bundle dir」這件事放進註解說清楚比較好避免未來有人以為這是正常的 prod 行為。

邊界情況檢查

情境 行為 評估
os.Executable() 失敗 baseDir"."resolveBuiltInDataDir./data../data../Resources/data,其中任一有 models.json 就命中 合理,接力自然
三個候選都沒命中 <base>/datamodel.NewRepository 印 warningr.models = []server 繼續跑 容錯策略合理(見 Minor m-2若能多印「我試過的路徑」更好
目錄對但 models.json 損毀JSON 解析錯) os.Stat 判定命中 → NewRepository json.Unmarshal 印 warning → 0 個 model 合理,不會誤判成「找別的目錄」
目錄對但 models.json 是 0 bytes os.Stat 判定命中non-dirNewRepository json.Unmarshal 失敗印 warning → 0 個 model 同上
models.json 是個目錄(奇怪但可能發生) info.IsDir() 為 true跳過該候選繼續試下一個 已處理
cfg.DataDir 指定為不存在路徑 只影響 writable 部分custom-models 讀取會 warning 但 empty、sentinel 寫入失敗但 WS Hub 會跳過、logs 可能寫不出。built-in catalog 不受影響(依然從 builtInDataDir 讀) 修復後行為比修復前好 —— 修復前一個無效 --data-dir 會同時把 built-in catalog 弄掛
cfg.DataDir 指定為路徑但 parent 不存在 同上

其他 dataDir 誤用檢查Grep server/ 全專案)

我 grep 了 dataDir / DataDir / models.json / nef/kl / data/nef 的所有出現位置,確認:

  • server/internal/api/ws/hub.gosentinelDataDir(用於 .first-ws-connected sentinel 語意正確,寫入 writable user dataDir
  • server/internal/config/config.goDataDir CLI flag → 沒改
  • server/internal/flash/service.gos.dataDir 解析 data/nef/... 相對路徑 → 本次修復已把 flash.NewService 改吃 builtInDataDir,語意對齊
  • main.go 其他位置sentinel / custom-models / WS Hub 都正確用 writable dataDir,沒有誤用

沒找到其他地方把「應屬 bundle 內 read-only 的檔案」錯用 writable dataDir 解析。 本次修復清單完備。


優點

  1. 做法對齊既有範式resolveBuiltInDataDir 的候選路徑邏輯1: dev flat / 2: installer / 3: macOS bundle和既有 resolveBridgeScript 一致,未來維護者不需學新模式,讀一個就懂兩個。
  2. 語意清晰拆分builtInDataDirread-only, bundle 內)和 dataDirwritable, user home兩個變數名 + 兩段 comment block 把意圖講得很清楚,修復前後讀 code 一看就懂「為什麼要兩個」。model.NewRepositoryflash.NewService 傳的是 builtInDataDircustom-models / WS sentinel 傳的是 dataDir,分工直觀。
  3. Dev mode 不 regressioncfg.DataDir == "" 時 fallback 回 builtInDataDir,讓 go run ./server 繼續可跑 —— 重要,因為它同時讓 reviewer 這次能在 server/ 目錄下獨立驗證 dev mode。
  4. 命中條件嚴格:用「models.json 存在且非目錄」當 sentinel而不是 data/ 目錄存在就行),避免選到 wails build artifact 留下的空 data/ 目錄(這正是修復前誤入的那個坑)。
  5. 容錯好:三候選都沒命中時 fallback 而非 panicNewRepository 也只印 warning 不 crash符合「不 regression、壞的優雅」。
  6. 增量最小:唯一改動檔就是 server/main.go+40 6沒動 flash/service.go 的 body風險面小。
  7. Build/vet/test 全綠,獨立 smokebundle + dev mode 兩種路徑)也全綠

總結意見≤400 字)

M8-10a 這個 P0 latent bug 的修復方向正確、做法乾淨,唯一改動檔 server/main.gobuiltInDataDirread-only bundle 內)與 dataDirwritable user home拆成兩個語意明確的變數並新增 resolveBuiltInDataDir helper 以 models.json 存在為命中條件自動跨 dev / installer / macOS bundle 三種布局。Flash service 同步改吃 builtInDataDir,避免相對路徑被誤解到 user home。Build / vet / test / smokebundle + dev mode四項獨立驗證全綠/api/models 穩定回 15 個 model實體 .nef 檔路徑可對到 bundle 內。

但 Linux AppImage 布局(usr/bin/visiona-local-server + usr/lib/visiona-local/data/三個候選都不命中Major-1,等於這個 P0 bug 在 Linux 上繼續潛藏——雖然同樣的缺失 resolveBridgeScript 先前就存在、並非本次 PR 引入。修復建議:①讀 VISIONA_BUNDLE_LIB_DIR envAppRun 已 export②加第 4 候選 <base>/../lib/visiona-local/data。兩個 Minorfallback 沒 abs 化、沒 log 試過的路徑)和兩個 Suggestion抽公用 helper、dev mode writable fallback 註解)屬改善性質,不阻擋交付。

結論:⚠️ 需修 1 個 MajorLinux AppImage 覆蓋)後可交付;如果本次交付明確只限 macOS + Windows 兩平台Major-1 可降級為「Linux 平台列為 known issue 追蹤到 M9」並直接放行。建議優先採前者resolveBridgeScript 一起修掉,避免兩個函式分開欠債。


第二輪 Review2026-04-15

審查對象

第一輪所有 Major / Minor / Suggestion 的修復 —— 唯一改動檔 server/main.go+119 / 24 行)。

第一輪問題處理狀態

# 問題 狀態 驗證
Major-1 Linux AppImage 布局未覆蓋(三候選全不命中) 解決 候選清單補兩層①env VISIONA_BUNDLE_LIB_DIR/dataAppRun 已 export② FHS <base>/../lib/visiona-local/data。情境 2模擬 AppImage 布局 + env var→ log Built-in data dir: .../usr/lib/visiona-local/dataLoaded 1GET /api/models total=1 ;情境 3同布局但不設 env靠 FHS 候選 5 命中)→ 同樣成功 resolveBridgeScript 同步修掉,連帶把第一輪備註的技術債清掉。
Minor-1 fallback 沒 filepath.Abs 解決 L145-149、L103-107兩個函式 fallback 都過 filepath.Abs,絕對路徑能 err 時才退成 Join 的相對路徑(絕不 crash。全不命中情境 log 印出 Built-in data dir: /var/.../data(絕對路徑)
Minor-2 沒 log 試過的候選路徑 解決 L142、L100log.Printf("warn: ... not found. Tried: %v", tried)findFirstExisting 每個候選都先 filepath.Abs 再丟進 triedlog 出來是絕對路徑清單debug 訊息清楚。全不命中情境實測 log 輸出完整 4/5 個候選絕對路徑
Suggestion s-1 findFirstExisting helper 解決 L53-73 實作 findFirstExisting(candidates, sentinel) (string, []string)resolveBuiltInDataDirL139resolveBridgeScriptL97都改用。未來改動兩個函式的共通邏輯只需改一處。
Suggestion s-2 dev mode writable fallback 加註解 解決 L180-1905 行 comment block 明確寫出「dev mode-only fallback、production 下 Wails 永遠會傳 --data-dir、若 packaged 模式沒傳則 writable 操作 log warning 不 crash」。意圖表達清楚。

獨立複驗結果

驗證項 指令 結果
Build cd server && go build -o /tmp/rv2-server . PASS
Vet cd server && go vet ./... PASS
Test cd server && go test -count=1 ./... 全綠api / handlers / ws / device / model 5 個 test package 全通過)
情境 2AppImage + env var 模擬 usr/bin/<server> + usr/lib/visiona-local/data/models.jsonVISIONA_BUNDLE_LIB_DIR=<appdir>/usr/lib/visiona-local Built-in data dir: .../usr/lib/visiona-local/dataLoaded 1GET /api/models total=1
情境 3AppImage FHS fallback 同上布局但設 env var 候選 5 <base>/../lib/visiona-local/data 命中,同樣 Loaded 1 → total=1
情境:全不命中 把 server binary 放到 /tmp/xxx/ 獨立目錄,無 env 兩個函式 log 出完整 Tried: 清單都是絕對路徑fallback 回絕對路徑server 繼續跑 0 models不 crash
AppImage 對齊檢查 installer/linux/build-appimage.sh L29-33 / L124-138 APPDIR/usr/bin/visiona-local-server + APPDIR/usr/lib/visiona-local/{data,scripts} + AppRun export VISIONA_BUNDLE_LIB_DIR=${HERE}/usr/lib/visiona-local —— 與 server 候選 ①env 路徑)和 ⑤FHS 路徑)雙雙對齊
cwd 變動風險檢查 grep 專案找 os.Chdir / Chdir( 零匹配 —— ./scripts 相對候選在執行期不會因 cwd 漂移而解析錯

候選優先順序誤命中檢查

逐平台驗證新候選 <base>/../lib/visiona-local/data 不會在非 Linux 場景誤命中:

平台 <base> 候選 5 解析 該路徑實際是否存在 風險
macOS bundle Contents/Resources/bin Contents/Resources/lib/visiona-local/data macOS bundle 不用 FHS
Windows installer {app}\bin {app}\lib\visiona-local\data Inno Setup 放 {app}\data
Dev mode server/. ../lib/visiona-local/data
Linux AppImage usr/bin usr/lib/visiona-local/data 正是目標

候選順序把 <base>/data<base>/../data<base>/../Resources/data 排在 FHS 之前,所有既有布局都會在進 FHS 前命中。沒有誤命中風險。

新觀察的邊界情況

# 觀察 評估 建議
E-1 findFirstExisting 簽名 (string, []string),呼叫端用 dir != "" 判斷 OK Go 慣例通常是 (value, bool)(value, error)現行簽名把「hit/miss」和「嘗試清單」合併一個回傳值讀起來略非 idiomatic但因為 tried 在 success path 沒用但無害、miss path 才拿去 log合併簽名避免多一個回傳值權衡合理 不阻擋通過。若要更 idiomatic 可改 (dir string, tried []string, ok bool),但目前 call site 寫法(if dir, tried := ...; dir != "")已經夠清楚
E-2 resolveBridgeScript / resolveBuiltInDataDir 的 warn 走 std log.Printf,其他資訊走 pkglogger.logger.Info —— log 格式不一致 resolve 函式在 main.go 被呼叫的時序上 logger 已存在L155 pkglogger.New()resolve 在 L177、L236其實可以注入 logger;但本次修改保留 std log 有個好處:若將來把 resolve 函式移到更早(在 logger 初始化前),不用再改簽名。不影響正確性。 不阻擋通過。建議追蹤到下次重構 logger 時一起統一(加 Minor 備忘)
E-3 VISIONA_BUNDLE_LIB_DIR 只在 Linux AppImage AppRun 設定 —— 其他平台若使用者手動設錯,是否會誤命中? findFirstExisting 對每個候選做 os.Stat(.../sentinel)env 指到不存在的路徑會 Stat 失敗自然跳下一個候選env 指到存在且含正確 sentinel 的路徑,代表使用者有意識地 override行為合理 無風險。Resolve 函式的 os.Stat sentinel check 已是自然保險
E-4 ./scripts 相對路徑候選resolveBridgeScript 候選 6在理論上會隨 cwd 漂移 Grep 專案零匹配 os.Chdirserver 啟動流程不改 cwd且相對候選放最後env / base 相關的絕對候選都不命中才會走到),實務上只在純 dev mode cwd 對齊時觸發 無風險

新發現問題

Critical無。

Major無。

Minor非阻擋

# 檔案 問題 建議
m2-1 server/main.go 100, 142 兩個 resolve 函式的 warn log 用 std log.Printf 而非 pkglogger.logger.Warn,與後續 logger.Info("Built-in data dir: ...") 格式不一致 下次重構 logger 時把 logger或至少 pkglogger注入 resolve 函式,統一格式。不影響本輪通過。

Suggestion

# 檔案 建議
s2-1 server/main.go 59-73 findFirstExisting 若改 (dir string, tried []string, ok bool) 可讓 call site 更 idiomaticif dir, tried, ok := ...; ok { ... } else { log; fallback })。非必須。

優點(第二輪新增)

  1. 修復完整度超出第一輪要求:第一輪只要求 Major-1Linux 資料目錄覆蓋Orchestrator 把所有 Minor + 兩個 Suggestion 一併做掉,且把第一輪備註的技術債 resolveBridgeScript 也同步修了,避免「兩個函式結構一樣但一修一未修」的分裂狀態。
  2. 候選順序保守FHS 候選放在既有候選之後,零誤命中風險(見上表);既有行為零 regression。
  3. findFirstExisting 抽象得當:回傳 tried 清單給 log 用的設計雖然非完全 idiomatic但對 debug 訊息清晰度貢獻很大(見全不命中情境 log 輸出)—— 這個權衡合理。
  4. 註解品質高:每個 resolve 函式的候選清單都有編號 + 情境說明;dataDir fallback 的註解把「dev-only / production Wails 永遠傳 / 壞了不 crash」三件事講清楚未來維護者不會誤解。
  5. 情境 2 / 3 雙路命中env var 和 FHS 兩層保險,即使未來 AppRun 的 env var 被移掉或名稱改了FHS 還能兜底;反之亦然 —— 防禦性設計好。
  6. 所有驗證全綠build / vet / test / 3 個布局實跑bundle 第一輪已驗 / AppImage-env / AppImage-FHS / 全不命中 fallback全通過。

總結意見≤300 字)

Orchestrator 本輪把第一輪的 Major-1 + Minor-1/2 + Suggestion s-1/s-2 全數處理,且連帶修掉備註中 resolveBridgeScript 的同樣欠債,新增 findFirstExisting 共用 helper 讓兩個函式結構一致。候選清單補 env varVISIONA_BUNDLE_LIB_DIRAppRun 已 export+ FHS 雙層保險,對 macOS bundle / Windows installer / Dev mode 零誤命中風險。三個布局實跑AppImage + env var / AppImage FHS fallback / 全不命中 fallback全綠log 明確列出嘗試過的絕對路徑fallback 也已 filepath.Abs 化。Build / vet / test 全綠。新觀察到的 2 個 Minor / Suggestionstd log vs pkglogger 格式一致性、helper 簽名 idiomatic 度)屬改善性,不阻擋交付。修復完整度超出第一輪最低要求,技術債清理徹底。

結論: 通過可交付三平台macOS bundle / Windows installer / Linux AppImage 追蹤到下次 logger 重構時統一 std log 與 pkglogger 的格式Minor m2-1