fix(local-tool): sidebar icon + Wails Windows icon.ico + 掃裝置路徑修復

sidebar E icon:
- frontend/src/components/layout/sidebar.tsx 左上角的 "E" 方塊換成 <img> 指向
  /visiona-logo.png,從 frontend/public/visiona-logo.png serve

Wails 桌面/工作列 icon:
- 把 branding/icon.ico 複製到 visiona-local/build/windows/icon.ico
- Wails v2 Windows build 偵測到這個檔案就會直接用它當 exe embedded icon,
  不再從 appicon.png 自動 resize(解析度更好)

掃裝置根因修復:
1. server main.go:新增 resolveBridgeScript() 智慧找 kneron_bridge.py
   - 優先 <exe>/scripts/kneron_bridge.py
   - fallback <exe>/../scripts/... 對應 Windows installer 的 {app}\bin\ + {app}\scripts\ 佈局
   - fallback <exe>/../Resources/scripts/... 對應 macOS app bundle

2. server kneron/detector.go:ResolvePython 重寫
   - 最高優先:VISIONA_PYTHON 環境變數(由 Wails 殼層注入)
   - 加入 visiona-local 新路徑:%APPDATA%\visiona-local\runtime\venv、
     ~/Library/Application Support/visiona-local/runtime/venv、
     ~/.local/share/visiona-local/runtime/venv
   - 保留 edge-ai-platform 舊路徑作為 legacy fallback

3. visiona-local/app.go:spawn server 時 export VISIONA_PYTHON=<pyBin>
   讓 detector 直接用 Wails 殼層已經 resolve 好的 python interpreter,
   不再自己獨立去找造成不一致。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-04-12 04:59:23 +08:00
parent 50a3f73acd
commit f8533a6b04
6 changed files with 75 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -21,9 +21,12 @@ export function Sidebar() {
<aside className="flex h-full w-60 flex-col border-r bg-card"> <aside className="flex h-full w-60 flex-col border-r bg-card">
<div className="flex h-14 items-center border-b px-4"> <div className="flex h-14 items-center border-b px-4">
<Link href="/" className="flex items-center gap-2 font-semibold"> <Link href="/" className="flex items-center gap-2 font-semibold">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold"> {/* eslint-disable-next-line @next/next/no-img-element */}
E <img
</div> src="/visiona-logo.png"
alt="visionA Local"
className="h-8 w-8 rounded-lg"
/>
<span className="text-lg">{t('nav.appName')}</span> <span className="text-lg">{t('nav.appName')}</span>
</Link> </Link>
</div> </div>

View File

@ -13,28 +13,52 @@ import (
) )
// ResolvePython finds the best Python interpreter for the given script path. // ResolvePython finds the best Python interpreter for the given script path.
// Search order: script-local venv → parent venv → platform config dir venv → system python3/python. //
// Search order:
// 1. VISIONA_PYTHON env var (highest priority — set by Wails shell when it
// has already provisioned a bundled venv)
// 2. Script-local venv / parent venv
// 3. %APPDATA%\visiona-local\runtime\venv (Windows installer seeded venv)
// 4. ~/.local/share/visiona-local/runtime/venv (Linux)
// 5. ~/Library/Application Support/visiona-local/runtime/venv (macOS)
// 6. Legacy: ~/.edge-ai-platform/venv / %LOCALAPPDATA%\EdgeAIPlatform\venv
// 7. System python3 / python
func ResolvePython(scriptPath string) string { func ResolvePython(scriptPath string) string {
// 1. Environment variable override (set by Wails shell)
if p := os.Getenv("VISIONA_PYTHON"); p != "" {
if _, err := os.Stat(p); err == nil {
return p
}
}
scriptDir := filepath.Dir(scriptPath) scriptDir := filepath.Dir(scriptPath)
parentDir := filepath.Dir(scriptDir) parentDir := filepath.Dir(scriptDir)
// Build candidate list with both Unix and Windows venv layouts // 2. Script-local / parent venv
var candidates []string var candidates []string
for _, base := range []string{scriptDir, parentDir} { for _, base := range []string{scriptDir, parentDir} {
candidates = append(candidates, candidates = append(candidates,
filepath.Join(base, "venv", "bin", "python3"), // Unix filepath.Join(base, "venv", "bin", "python3"), // Unix
filepath.Join(base, "venv", "Scripts", "python.exe"), // Windows filepath.Join(base, "venv", "Scripts", "python.exe"), // Windows
) )
} }
// 3-5. Platform-specific data dir (visiona-local)
if home, err := os.UserHomeDir(); err == nil { if home, err := os.UserHomeDir(); err == nil {
candidates = append(candidates, candidates = append(candidates,
// Windows: %APPDATA%\visiona-local\runtime\venv
filepath.Join(os.Getenv("APPDATA"), "visiona-local", "runtime", "venv", "Scripts", "python.exe"),
// macOS
filepath.Join(home, "Library", "Application Support", "visiona-local", "runtime", "venv", "bin", "python3"),
// Linux
filepath.Join(home, ".local", "share", "visiona-local", "runtime", "venv", "bin", "python3"),
// 6. Legacy edge-ai-platform paths (backwards compat for upgrades)
filepath.Join(home, ".edge-ai-platform", "venv", "bin", "python3"), filepath.Join(home, ".edge-ai-platform", "venv", "bin", "python3"),
filepath.Join(home, ".edge-ai-platform", "venv", "Scripts", "python.exe"), filepath.Join(home, ".edge-ai-platform", "venv", "Scripts", "python.exe"),
) )
} }
// On Windows, also check %LOCALAPPDATA%\EdgeAIPlatform\venv // 6b. Legacy %LOCALAPPDATA%\EdgeAIPlatform\venv
if appData := os.Getenv("LOCALAPPDATA"); appData != "" { if appData := os.Getenv("LOCALAPPDATA"); appData != "" {
candidates = append(candidates, candidates = append(candidates,
filepath.Join(appData, "EdgeAIPlatform", "venv", "Scripts", "python.exe"), filepath.Join(appData, "EdgeAIPlatform", "venv", "Scripts", "python.exe"),
@ -42,12 +66,15 @@ func ResolvePython(scriptPath string) string {
} }
for _, p := range candidates { for _, p := range candidates {
if p == "" {
continue
}
if _, err := os.Stat(p); err == nil { if _, err := os.Stat(p); err == nil {
return p return p
} }
} }
// Fallback to system python // 7. Fallback to system python
for _, name := range []string{"python3", "python"} { for _, name := range []string{"python3", "python"} {
if p, err := exec.LookPath(name); err == nil { if p, err := exec.LookPath(name); err == nil {
return p return p

View File

@ -49,6 +49,33 @@ func baseDir(devMode bool) string {
return filepath.Dir(exe) return filepath.Dir(exe)
} }
// resolveBridgeScript finds kneron_bridge.py across different packaging layouts.
//
// Possible locations (tried in order):
// 1. <base>/scripts/kneron_bridge.py — dev mode or flat layout
// 2. <base>/../scripts/kneron_bridge.py — Windows/Linux installer: binary in {app}/bin, scripts in {app}/scripts
// 3. <base>/../Resources/scripts/kneron_bridge.py — macOS app bundle: binary in Contents/MacOS, scripts in Contents/Resources
// 4. ./scripts/kneron_bridge.py — cwd fallback
func resolveBridgeScript(base string) string {
candidates := []string{
filepath.Join(base, "scripts", "kneron_bridge.py"),
filepath.Join(base, "..", "scripts", "kneron_bridge.py"),
filepath.Join(base, "..", "Resources", "scripts", "kneron_bridge.py"),
filepath.Join(".", "scripts", "kneron_bridge.py"),
}
for _, c := range candidates {
abs, err := filepath.Abs(c)
if err != nil {
continue
}
if info, err := os.Stat(abs); err == nil && !info.IsDir() {
return abs
}
}
// Nothing found — return the default so downstream logs a clear error
return filepath.Join(base, "scripts", "kneron_bridge.py")
}
func main() { func main() {
cfg := config.Load() cfg := config.Load()
@ -101,7 +128,9 @@ func main() {
// Initialize device manager // Initialize device manager
registry := device.NewRegistry() registry := device.NewRegistry()
deviceMgr := device.NewManager(registry, cfg.MockMode, cfg.MockDeviceCount, filepath.Join(base, "scripts", "kneron_bridge.py")) bridgeScript := resolveBridgeScript(base)
logger.Info("Kneron bridge script: %s", bridgeScript)
deviceMgr := device.NewManager(registry, cfg.MockMode, cfg.MockDeviceCount, bridgeScript)
deviceMgr.SetLogBroadcaster(logBroadcaster) deviceMgr.SetLogBroadcaster(logBroadcaster)
deviceMgr.Start() deviceMgr.Start()

View File

@ -377,6 +377,12 @@ func (a *App) startServer() error {
env = append(env, "VISIONA_BUNDLE_BIN_DIR="+binDir) env = append(env, "VISIONA_BUNDLE_BIN_DIR="+binDir)
fmt.Fprintln(os.Stderr, "[visiona-local] bundle bin dir:", binDir) fmt.Fprintln(os.Stderr, "[visiona-local] bundle bin dir:", binDir)
} }
// 注入 python interpreter 路徑給 serverkneron detector 會讀 VISIONA_PYTHON
// 避免 detector 自己去 resolve 又走不同的路徑邏輯造成不一致。
if !a.mockMode && pyBin != "" {
env = append(env, "VISIONA_PYTHON="+pyBin)
fmt.Fprintln(os.Stderr, "[visiona-local] python interpreter:", pyBin)
}
cmd.Env = env cmd.Env = env
if stdoutLog != nil { if stdoutLog != nil {
cmd.Stdout = stdoutLog cmd.Stdout = stdoutLog

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB