feat(local-tool): 首次啟動自動安裝 Kneron WinUSB driver

行為改變:Wails app 首次啟動會在 venv 就緒後、spawn server 前自動呼叫
libwdi 安裝 Kneron WinUSB driver。使用者不再需要手動點「安裝 USB Driver」
按鈕(按鈕保留供失敗後重試用)。

實作:
- startServer() step 1.5 新增 ensureDriverInstalled() 呼叫
- 用 <dataDir>/.driver-installed 記號檔避免每次啟動都彈 UAC
- 失敗不擋 server 啟動,只寫 log,使用者可稍後手動重試
- 新增 app-level log helper appLog() 寫到 <dataDir>/logs/wails.log
  (Wails Windows 以 windowsgui subsystem build,os.Stderr 指向 null device,
   沒有這個檔使用者看不到 startup 期間的 debug 訊息)
- 手動 InstallKneronDriver binding 成功時也寫記號檔

使用者移除 .driver-installed 檔就能強制重裝(例如 Windows 更新把 driver 弄壞時)。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-04-12 08:03:37 +08:00
parent d1ff55005a
commit 3355a096b8

View File

@ -332,7 +332,68 @@ func (a *App) InstallKneronDriver() error {
pyBin = resolved pyBin = resolved
} }
return installKneronWinUSBDriver(pyBin) if err := installKneronWinUSBDriver(pyBin); err != nil {
return err
}
// 手動安裝成功後也寫記號檔(之後就不會再自動彈 UAC
a.markDriverInstalled()
return nil
}
// appLog 把一行訊息寫到 <dataDir>/logs/wails.log 以及 os.Stderr。
// Wails Windows app 以 windowsgui subsystem buildos.Stderr 指向 null device
// 沒有這個檔使用者就看不到 startup 期間的 debug 訊息。
func (a *App) appLog(format string, args ...interface{}) {
msg := fmt.Sprintf("["+time.Now().Format("15:04:05")+"] "+format, args...)
fmt.Fprintln(os.Stderr, msg) // dev 模式 / macOS / Linux 會看到
if a.dataDir == "" {
return
}
logsDir := filepath.Join(a.dataDir, "logs")
_ = os.MkdirAll(logsDir, 0o755)
f, err := os.OpenFile(filepath.Join(logsDir, "wails.log"),
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return
}
defer f.Close()
fmt.Fprintln(f, msg)
}
// ensureDriverInstalled 在首次 startup 時自動安裝 Kneron WinUSB driver。
//
// 用 `<dataDir>/.driver-installed` 記號檔避免每次啟動都彈 UAC
// - 檔案存在 → 跳過(之前已安裝過,假設仍有效)
// - 檔案不存在 → 呼叫 installKneronWinUSBDriver → 成功後寫記號檔
//
// 失敗時寫 stderr 但不回 error讓 startup 流程繼續走,使用者之後可手動重試)。
//
// 使用者移除 `.driver-installed` 檔就能強制重裝(例如 Windows 更新把 driver 弄壞時)。
func (a *App) ensureDriverInstalled(pyBin string) error {
if runtime.GOOS != "windows" {
return nil // macOS / Linux 不需要
}
marker := filepath.Join(a.dataDir, ".driver-installed")
if fileExists(marker) {
a.appLog("driver 記號檔存在,跳過自動安裝:%s", marker)
return nil
}
a.appLog("首次啟動:自動安裝 Kneron WinUSB driver會彈出 UAC 提權視窗,請點「是」)")
if err := installKneronWinUSBDriver(pyBin); err != nil {
a.appLog("driver 自動安裝失敗(非致命):%v", err)
return err
}
a.markDriverInstalled()
a.appLog("driver 自動安裝完成,記號檔已建立:%s", marker)
return nil
}
// markDriverInstalled 寫 `.driver-installed` 記號檔,內容為時間戳供 debug。
func (a *App) markDriverInstalled() {
marker := filepath.Join(a.dataDir, ".driver-installed")
content := fmt.Sprintf("installed at %s\n", time.Now().Format(time.RFC3339))
_ = os.WriteFile(marker, []byte(content), 0o644)
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@ -351,6 +412,15 @@ func (a *App) startServer() error {
a.pythonModeR = pyMode a.pythonModeR = pyMode
a.mu.Unlock() a.mu.Unlock()
// 1.5. 首次啟動自動安裝 Kneron WinUSB driverWindows onlymacOS/Linux no-op
// 失敗不擋 server 啟動 —— 使用者之後可手動點「安裝 USB Driver」按鈕重試。
// 用 .driver-installed 記號檔避免每次都跑。
if !a.mockMode && pyBin != "" {
if err := a.ensureDriverInstalled(pyBin); err != nil {
fmt.Fprintln(os.Stderr, "[visiona-local] driver auto-install failed (非致命,可於 UI 手動重試):", err)
}
}
// 2. 找 port // 2. 找 port
port, err := pickPort(defaultPreferredPort) port, err := pickPort(defaultPreferredPort)
if err != nil { if err != nil {