local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
262 lines
11 KiB
Markdown
262 lines
11 KiB
Markdown
# Code Reuse Plan — visionA-local
|
||
|
||
> 從 `/Users/jimchen/Innovedus/edge-ai-platform/edge-ai-platform/` 取用程式碼的完整對照表。
|
||
> 三種策略:**直接複製**、**改寫**、**新寫**。
|
||
|
||
---
|
||
|
||
## 1. 總體比例(預估)
|
||
|
||
- **直接複製**:~60% LOC
|
||
- **改寫**:~20%
|
||
- **新寫**:~20%
|
||
- **刪除**:獨立計算,見 [`removed-code.md`](./removed-code.md)
|
||
|
||
## 2. 目錄層級策略
|
||
|
||
| From(edge-ai-platform) | To(local_tool) | 策略 | 備註 |
|
||
|------------------------|----------------|------|------|
|
||
| `server/main.go` | `server/main.go` | **改寫** | 移除 cluster / tunnel / relay / hwid / gitea 邏輯 |
|
||
| `server/go.mod` | `server/go.mod` | **改寫** | 移除不再需要的 imports;module name 可改為 `visiona-local` |
|
||
| `server/go.sum` | `server/go.sum` | **改寫** | 跟著 go.mod 重新生成 |
|
||
| `server/internal/api/router.go` | `server/internal/api/router.go` | **改寫** | 見 [`api-endpoints.md`](./api-endpoints.md) §4 |
|
||
| `server/internal/api/middleware.go` | 同路徑 | **直接複製** | |
|
||
| `server/internal/api/handlers/` | 同路徑 | **部分複製 / 改寫** | 見下方 §3.1 |
|
||
| `server/internal/api/ws/` | 同路徑 | **部分複製 / 改寫** | 見下方 §3.2 |
|
||
| `server/internal/api/api_e2e_test.go` | 同路徑 | **改寫** | 刪除 cluster / flash / auth test case |
|
||
| `server/internal/camera/` | 同路徑 | **直接複製** | 除了 ffmpeg 路徑查找改為「優先 bundled」,見 §3.4 |
|
||
| `server/internal/config/config.go` | 同路徑 | **改寫** | 見 §3.3 |
|
||
| `server/internal/deps/` | 同路徑 | **改寫** | 移除 update / relay 相關檢查 |
|
||
| `server/internal/device/` | 同路徑 | **直接複製** | 核心裝置管理 |
|
||
| `server/internal/driver/` | 同路徑 | **直接複製** | |
|
||
| `server/internal/inference/` | 同路徑 | **直接複製** | |
|
||
| `server/internal/model/` | 同路徑 | **直接複製** | |
|
||
| `server/internal/flash/` | ❌ **刪除** | 使用者決策 Q9 |
|
||
| `server/internal/cluster/` | ❌ **刪除** | 使用者決策 |
|
||
| `server/internal/tunnel/` | ❌ **刪除** | 使用者決策 |
|
||
| `server/internal/update/` | ❌ **刪除** | 使用者決策 Q6 |
|
||
| `server/pkg/logger/` | 同路徑 | **直接複製** | |
|
||
| `server/pkg/hwid/` | ❌ **刪除** | 只給 relay 用的 |
|
||
| `server/cmd/relay-server/` | ❌ **刪除** | 不需要 relay |
|
||
| `server/tray/` | ❌ **刪除** | 使用者決策 Q-A=A3:砍掉 tray,省跨平台圖資產與 Wails tray 踩坑 |
|
||
| `server/scripts/kneron_bridge.py` | `server/scripts/kneron_bridge.py` | **直接複製** | |
|
||
| `server/scripts/requirements.txt` | 同路徑 | **直接複製** | |
|
||
| `server/scripts/update_kl720_firmware.py` | ❌ **刪除** | flash 已砍 |
|
||
| `server/scripts/firmware/` | ❌ **刪除** | 同上 |
|
||
| `server/data/models.json` | 同路徑 | **直接複製** | |
|
||
| `server/data/nef/kl520/` | 同路徑 | **直接複製** | 全部預置模型 |
|
||
| `server/data/nef/kl720/` | 同路徑 | **直接複製** | |
|
||
| `server/web/` | 同路徑(空目錄) | **直接複製** | go:embed 掛載點 |
|
||
| `frontend/` | `frontend/` | **改寫** | M1 就要清乾淨:刪除 cluster / relay / tunnel 相關頁面、元件、store、API client,讓 `pnpm build` 通過且 UI 乾淨(使用者決策 Q-C=C2) |
|
||
| `installer/` | `visiona-local/` | **改寫(改名 + 精簡)** | 見 §3.5 |
|
||
| `installer/wheels/` | `vendor/wheels/` 或 `visiona-local/payload/scripts/wheels/` | **直接複製** | KneronPLUS wheel |
|
||
| `installer/drivers/` | `vendor/drivers/` 或 `visiona-local/payload/drivers/` | **直接複製** | Windows WinUSB |
|
||
| `installer/payload/` | `visiona-local/payload/` | **結構沿用,內容重建** | 由 Makefile payload target 重新 stage |
|
||
| `Makefile` | `Makefile` | **改寫** | 見 [`build-pipeline.md`](./build-pipeline.md) |
|
||
| `docker/` | ❌ **刪除** | |
|
||
| `scripts/deploy-*.sh` | ❌ **刪除** | |
|
||
| `scripts/kneron_detect.py` | `server/scripts/kneron_detect.py` | **直接複製** | installer 裝置偵測用 |
|
||
| `tools/` | 視檔案決定 | **多數刪除** | 只保留開發相關的 script |
|
||
| `docs/` | ❌ **刪除**(之後重寫) | | |
|
||
|
||
## 3. 需要改寫的檔案細節
|
||
|
||
### 3.1 `server/internal/api/handlers/`
|
||
|
||
| 檔案 | 策略 | 動作 |
|
||
|------|-----|-----|
|
||
| `system_handler.go` | 改寫 | 移除 `CheckUpdate`、`giteaURL` 參數;刪除 `update-check` handler |
|
||
| `model_handler.go` | 直接複製 | |
|
||
| `model_upload_handler.go` | 直接複製 | |
|
||
| `device_handler.go` | 改寫 | 移除 `FlashDevice` handler 與 `flashSvc` 注入 |
|
||
| `camera_handler.go` | 直接複製 | 但 `StartFromURL` 內部的 yt-dlp 路徑查找改為 bundled(見 §3.4) |
|
||
| `cluster_handler.go` | ❌ 刪除 | |
|
||
|
||
### 3.2 `server/internal/api/ws/`
|
||
|
||
| 檔案 | 策略 |
|
||
|------|-----|
|
||
| `hub.go` | 直接複製 |
|
||
| `device_events.go` | 直接複製 |
|
||
| `server_logs.go` | 直接複製 |
|
||
| `inference.go` | 直接複製 |
|
||
| `flash_progress.go` | ❌ 刪除 |
|
||
| `cluster_*.go` | ❌ 刪除 |
|
||
|
||
### 3.3 `server/internal/config/config.go` 改寫版
|
||
|
||
```go
|
||
package config
|
||
|
||
import (
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
)
|
||
|
||
type Config struct {
|
||
Port int
|
||
Host string
|
||
MockMode bool
|
||
MockCamera bool
|
||
MockDeviceCount int
|
||
LogLevel string
|
||
DevMode bool
|
||
PythonBin string // 新增:由 Wails app 傳入
|
||
ScriptsDir string // 新增
|
||
DataDir string // 新增
|
||
// ❌ 以下全部移除:
|
||
// RelayURL, RelayToken, GUIMode, GiteaURL
|
||
}
|
||
|
||
func Load() *Config {
|
||
cfg := &Config{}
|
||
flag.IntVar(&cfg.Port, "port", 3721, "Server port")
|
||
flag.StringVar(&cfg.Host, "host", "127.0.0.1", "Server host (always localhost)")
|
||
flag.BoolVar(&cfg.MockMode, "mock", false, "Enable mock device driver")
|
||
flag.BoolVar(&cfg.MockCamera, "mock-camera", false, "Enable mock camera")
|
||
flag.IntVar(&cfg.MockDeviceCount, "mock-devices", 1, "Number of mock devices")
|
||
flag.StringVar(&cfg.LogLevel, "log-level", "info", "Log level")
|
||
flag.BoolVar(&cfg.DevMode, "dev", false, "Dev mode")
|
||
flag.StringVar(&cfg.PythonBin, "python", "", "Path to python3 interpreter")
|
||
flag.StringVar(&cfg.ScriptsDir, "scripts-dir", "", "Path to scripts directory")
|
||
flag.StringVar(&cfg.DataDir, "data-dir", "", "Path to data directory")
|
||
flag.Parse()
|
||
|
||
// 合理 default(dev 模式)
|
||
if cfg.ScriptsDir == "" {
|
||
cfg.ScriptsDir = filepath.Join(".", "scripts")
|
||
}
|
||
if cfg.DataDir == "" {
|
||
cfg.DataDir = filepath.Join(".", "data")
|
||
}
|
||
return cfg
|
||
}
|
||
|
||
func (c *Config) Addr() string {
|
||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||
}
|
||
```
|
||
|
||
### 3.4 ffmpeg / yt-dlp 路徑查找
|
||
|
||
新增 `server/internal/camera/binaries.go`:
|
||
|
||
```go
|
||
package camera
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
)
|
||
|
||
// FfmpegBinary returns the ffmpeg binary to use, preferring bundled over PATH.
|
||
func FfmpegBinary() string {
|
||
if p := os.Getenv("VISIONA_FFMPEG"); p != "" {
|
||
return p
|
||
}
|
||
if p := bundled("ffmpeg"); p != "" {
|
||
return p
|
||
}
|
||
return "ffmpeg" // PATH fallback
|
||
}
|
||
|
||
func YtDlpBinary() string {
|
||
if p := os.Getenv("VISIONA_YTDLP"); p != "" {
|
||
return p
|
||
}
|
||
if p := bundled("yt-dlp"); p != "" {
|
||
return p
|
||
}
|
||
return "yt-dlp"
|
||
}
|
||
|
||
func bundled(name string) string {
|
||
exe, err := os.Executable()
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
dir := filepath.Dir(exe)
|
||
candidate := filepath.Join(dir, name)
|
||
if runtime.GOOS == "windows" {
|
||
candidate += ".exe"
|
||
}
|
||
if _, err := os.Stat(candidate); err == nil {
|
||
return candidate
|
||
}
|
||
return ""
|
||
}
|
||
```
|
||
|
||
### 3.5 `installer/app.go` → `visiona-local/app.go` 改寫
|
||
|
||
原 894 行,改寫要點:
|
||
|
||
**移除的欄位 / 函式**:
|
||
- `relayURL`, `relayToken`, `dashboardURL`
|
||
- `GetDashboardURL`, `GenerateToken`, `OpenBrowser` 中任何跟 relay 有關的邏輯
|
||
- `stepInstallUSBDriver` 需要保留,但只給 Windows 用(flash driver 已砍但 WinUSB 還需要)
|
||
- `stepInstallFfmpeg` **改寫**:不去系統 PATH 找,而是從 payload/bin 解壓 bundled ffmpeg
|
||
|
||
**新增的欄位 / 函式**:
|
||
- `pythonMode`(`bundled` / `system` / `auto`)
|
||
- `stepSetupPythonStandalone`(解壓內嵌 python-build-standalone)
|
||
- `stepSetupPythonSystem`(fallback 到系統 python,沿用舊的 `setupPythonVenv` 邏輯)
|
||
- `stepInstallWheelsOffline`(用 `pip install --no-index --find-links`)
|
||
- `i18n` translator 注入
|
||
- `launchServer` / `stopServer` / `watchServer`(見 [`tray-and-lifecycle.md`](./tray-and-lifecycle.md) §4)
|
||
- `single-instance lock`
|
||
|
||
**保留(幾乎不動)**:
|
||
- `GetSystemInfo`, `BrowseDirectory`, `ValidatePath`
|
||
- `stepCreateDir`, `stepExtractBinary`, `stepExtractData`, `stepExtractScripts`
|
||
- `DetectHardware`, `parseDetectOutput`
|
||
- `extractFile`, `extractDir`
|
||
- Platform-specific 檔案(`platform_darwin.go` / `platform_linux.go` / `platform_windows.go`)
|
||
|
||
## 4. 新寫的程式碼
|
||
|
||
完全新寫的檔案:
|
||
|
||
### 4.1 Wails app 側
|
||
|
||
```
|
||
visiona-local/
|
||
├── python_runtime.go ← python-build-standalone 解壓 + 版本偵測
|
||
├── server_launcher.go ← spawn / stop / watch Go server
|
||
├── lifecycle.go ← single-instance lock、port picking、cleanup
|
||
├── ipc.go ← /ipc/raise 小型 HTTP listener
|
||
├── i18n.go ← Translator + locales embed
|
||
└── locales/
|
||
├── en.json
|
||
└── zh-TW.json
|
||
```
|
||
|
||
### 4.2 Build / CI
|
||
|
||
```
|
||
scripts/
|
||
├── vendor-sync.sh ← 下載 python-build-standalone、ffmpeg、yt-dlp 到 vendor/
|
||
├── build-appimage.sh ← Linux AppImage 打包腳本
|
||
├── check-i18n.sh ← 檢查 i18n key 一致性
|
||
└── smoke-test.sh ← 安裝後 smoke test
|
||
visiona-local-installer.iss ← Inno Setup 腳本
|
||
dmg-config.py ← macOS dmgbuild 設定
|
||
.github/workflows/release.yml ← CI(選配)
|
||
```
|
||
|
||
## 5. 搬家步驟(建議執行順序)
|
||
|
||
1. **建立骨架**:`mkdir -p local_tool/{server,frontend,visiona-local,vendor,scripts,dist}`
|
||
2. **複製 server core**:`cp -r edge-ai-platform/server/{internal/{api,camera,config,deps,device,driver,inference,model},pkg/logger,scripts,data,web} local_tool/server/`
|
||
3. **跳過要刪的 package**:不要複製 `cluster`、`tunnel`、`flash`、`update`、`pkg/hwid`、`cmd/relay-server`、`tray`(使用者決策 Q-A:砍 tray)
|
||
4. **改寫 `main.go` + `config.go` + `router.go`**(見上方 §3)
|
||
5. **改寫 go.mod**:`go mod init visiona-local/server` → `go mod tidy`(會自動清掉 unused imports)
|
||
6. **驗證 `go build ./...` 通過**
|
||
7. **複製 frontend**:`cp -r edge-ai-platform/frontend local_tool/frontend`
|
||
8. **清理前端 cluster / relay / tunnel UI**(使用者決策 Q-C=C2:M1 就要清乾淨,不留到 M2):刪除 `src/app/clusters/`、`src/app/workspace/cluster/`、`src/components/cluster/`、`src/components/relay-token-sync.tsx`、`src/lib/api/clusters.ts`、`src/lib/api/tunnel.ts`(若有)、`src/lib/api/update.ts`(若有),修改 `sidebar.tsx` 移除 Clusters 導航項、`page.tsx` 移除 cluster stat、`settings/page.tsx` 移除 relay / cluster 區塊。最後驗證 `pnpm build` 通過且 UI 乾淨。
|
||
9. **複製 installer**:`cp -r edge-ai-platform/installer local_tool/visiona-local`,改 `main.go` 的 app 名稱與 bundle ID
|
||
10. **先跑 M1-12**:全新機器上 installer 能裝起來並跑通 Mock 模式
|