refactor: reorganize repo — move edge-ai-platform to subdirectory
- Move all Edge AI Platform code into edge-ai-platform/ subdirectory - Remove legacy local_service_win/ and relay-server-linux binary - Keep docs/ and README.md at repo root - Update docs to latest PRD v3.1 and TDD v2.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c326e228a1
commit
dd8d9b0ce2
@ -8,8 +8,8 @@
|
||||
|------|------|
|
||||
| 文件名稱 | 邊緣 AI 開發平台 PRD |
|
||||
| 產品名稱 | (暫未定名,以下稱「本平台」) |
|
||||
| 版本 | v2.7 |
|
||||
| 日期 | 2026-03-02 |
|
||||
| 版本 | v3.1 |
|
||||
| 日期 | 2026-03-05 |
|
||||
| 狀態 | 更新中 |
|
||||
|
||||
---
|
||||
@ -1087,7 +1087,7 @@ Kneron Dongle Arduino 開發板 非 Kneron 晶片
|
||||
|
||||
### B4.5 已實作的額外功能
|
||||
|
||||
> 以下功能為 MVP 開發過程中額外實作,超出原始 PRD 範圍。共 11 項功能。
|
||||
> 以下功能為 MVP 開發過程中額外實作,超出原始 PRD 範圍。共 18 項功能。
|
||||
|
||||
#### F5 — 多來源推論輸入
|
||||
|
||||
@ -1203,15 +1203,16 @@ Kneron Dongle Arduino 開發板 非 Kneron 晶片
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| **概述** | 提供一行指令安裝體驗,涵蓋 binary 下載、環境設定、硬體偵測,支援 macOS 與 Windows |
|
||||
| **概述** | 提供一行指令安裝體驗,涵蓋 binary 下載、環境設定、硬體偵測,支援 macOS、Windows 與 Linux |
|
||||
| **macOS/Linux 安裝** | `curl -fsSL https://gitea.innovedus.com/.../install.sh \| bash`,自動偵測 OS + 架構(darwin/linux + amd64/arm64) |
|
||||
| **Windows 安裝** | `irm https://gitea.innovedus.com/.../install.ps1 \| iex`,自動下載 zip、解壓、加入 PATH |
|
||||
| **安裝步驟** | 4 步驟自動化:(1) 下載 binary + 資料檔 (2) 安裝 USB 驅動(libusb)(3) 建立 Python venv + pyusb (4) 檢查環境 + 偵測硬體 |
|
||||
| **安裝目錄** | macOS: `~/.edge-ai-platform/`、Windows: `%LOCALAPPDATA%\EdgeAIPlatform` |
|
||||
| **解除安裝** | macOS: `rm -rf ~/.edge-ai-platform && sudo rm -f /usr/local/bin/edge-ai-server`、Windows: 刪除目錄 + 移除 PATH |
|
||||
| **安裝目錄** | macOS: `~/.edge-ai-platform/`、Linux: `~/.edge-ai-platform/`、Windows: `%LOCALAPPDATA%\EdgeAIPlatform` |
|
||||
| **解除安裝** | macOS/Linux: `rm -rf ~/.edge-ai-platform && sudo rm -f /usr/local/bin/edge-ai-server`、Windows: 刪除目錄 + 移除 PATH |
|
||||
| **啟動依賴檢查** | Server 啟動時自動檢查 ffmpeg、yt-dlp、python3,缺少時顯示對應平台的安裝指引 |
|
||||
| **GoReleaser 打包** | 跨平台 archive 自動產出(darwin amd64/arm64 tar.gz、windows amd64 zip),含 binary + data + scripts |
|
||||
| **GoReleaser 打包** | 跨平台 archive 自動產出(darwin amd64/arm64、linux amd64/arm64 tar.gz、windows amd64 zip),含 binary + data + scripts + firmware 目錄 + kneron_detect.py |
|
||||
| **Kneron 硬體偵測** | 安裝完成時自動偵測 USB Kneron 裝置(KL520/KL720/KL730),使用 pyusb;KL720 KDP legacy 裝置提示韌體更新 |
|
||||
| **Linux 特殊處理** | Ubuntu/Debian: `apt install libusb-1.0-0-dev python3-venv`;設定 udev rules 以允許非 root 使用者存取 USB 裝置 |
|
||||
|
||||
#### F17 — 圖形化安裝與解除安裝程式
|
||||
|
||||
@ -1220,7 +1221,10 @@ Kneron Dongle Arduino 開發板 非 Kneron 晶片
|
||||
| **概述** | 提供非技術人員可使用的桌面 GUI 安裝精靈,雙擊即可完成所有安裝步驟(server binary、Python 環境、系統依賴、硬體驅動),無需開終端機或輸入任何指令 |
|
||||
| **目標平台** | macOS (.dmg → .app) + Windows (.exe installer) |
|
||||
| **技術方案** | Go + Wails v2(WebView-based GUI),前端以 HTML/CSS/JS 實作安裝畫面,後端 Go 執行實際安裝邏輯。共用 Go 工具鏈,無需額外 runtime |
|
||||
| **安裝精靈流程** | 6 步驟:(1) 歡迎頁 + 授權協議 → (2) 安裝路徑選擇 → (3) 元件選擇(必要/可選)→ (4) 自動安裝 + 即時進度 → (5) 硬體偵測結果 → (6) 完成 + 啟動選項 |
|
||||
| **安裝精靈流程** | 7 步驟:(1) 歡迎頁 + 授權協議 → (2) Relay 連線設定 → (3) 安裝路徑選擇 → (4) 元件選擇(必要/可選)→ (5) 自動安裝 + 即時進度 → (6) 硬體偵測結果 → (7) 完成 + 啟動選項 |
|
||||
| **Relay 設定步驟** | 新增 Relay Configuration 步驟:輸入 Relay URL、Relay Token、本地 Port(預設 3721);設定儲存至共用設定檔,供 Launcher 與 server 讀取 |
|
||||
| **i18n 支援** | 安裝程式支援多語系切換(English + 繁體中文),安裝介面右上角提供語言切換器,所有步驟文字與錯誤訊息皆翻譯 |
|
||||
| **設定檔** | `~/.edge-ai-platform/config.json`,安裝程式與 Launcher(系統匣應用)共用同一設定檔,包含 relay URL/token、port、語言偏好、auto-start 設定等 |
|
||||
| **必要元件(自動安裝)** | edge-ai-server binary(含嵌入式前端)、Python 3 venv + numpy + opencv-python-headless + pyusb、Kneron 韌體檔案(KL520 + KL720)、NEF 預訓練模型、libusb(USB 裝置通訊) |
|
||||
| **可選元件** | ffmpeg(攝影機/影片串流)、yt-dlp(YouTube 影片下載) |
|
||||
| **自動依賴解析** | macOS: 自動安裝 Homebrew(若未安裝)→ `brew install libusb python3 ffmpeg`;Windows: 自動下載 Python embedded + libusb DLL,免管理員權限 |
|
||||
@ -1228,10 +1232,11 @@ Kneron Dongle Arduino 開發板 非 Kneron 晶片
|
||||
| **硬體偵測** | 安裝完成後自動掃描 USB Kneron 裝置,顯示偵測到的晶片型號(KL520/KL720)、韌體版本、連線狀態;KL720 KDP legacy 裝置提示一鍵韌體更新 |
|
||||
| **解除安裝** | 內建解除安裝功能:刪除 server binary + Python venv + 資料檔案 + symlink/PATH,macOS 提供拖曳到垃圾桶 + 深度清理選項,Windows 整合「新增或移除程式」 |
|
||||
| **安裝目錄** | macOS: `~/.edge-ai-platform/`(不需 sudo)、Windows: `%LOCALAPPDATA%\EdgeAIPlatform\`(不需管理員) |
|
||||
| **安裝完成動作** | 可選擇立即啟動 server + 自動開啟瀏覽器(`http://localhost:3721`)、建立桌面捷徑、設定開機自動啟動 |
|
||||
| **安裝完成動作** | 自動啟動選項現在註冊 Launcher(系統匣應用程式)而非裸 server 進程;可選擇立即啟動 Launcher + 自動開啟瀏覽器(`http://localhost:3721`)、建立桌面捷徑、設定開機自動啟動 Launcher |
|
||||
| **更新支援** | 偵測現有安裝版本,僅更新 binary + 新模型,保留使用者資料(custom-models、設定檔)與 Python venv |
|
||||
| **打包產出** | macOS: `EdgeAI-Installer.dmg`(含 .app + 背景圖 + Applications 捷徑)、Windows: `EdgeAI-Setup.exe`(NSIS + Wails 包裝) |
|
||||
| **安裝體積** | 完整安裝約 300-400 MB(binary ~10MB + 模型 ~73MB + Python venv ~250MB + firmware ~2MB) |
|
||||
| **UI 設計** | 現代化視覺設計,包含步驟進度指示器、平滑過渡動畫、統一的色彩主題與字體排版 |
|
||||
|
||||
#### F18 — Kneron 硬體通訊整合(KL520 + KL720)
|
||||
|
||||
@ -1270,6 +1275,75 @@ Kneron Dongle Arduino 開發板 非 Kneron 晶片
|
||||
| **WebSocket** | 叢集推論結果使用 `inference:cluster:{clusterId}` room 廣播 |
|
||||
| **限制** | MVP 階段:每叢集最多 8 裝置;叢集設定不持久化;不支援跨機器叢集 |
|
||||
|
||||
#### F20 — 雲端中繼隧道(Cloud Relay Tunnel)
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| **概述** | 允許部署在雲端(AWS EC2 等)的前端 UI 透過中繼伺服器存取使用者本地端的 edge-ai-server,無需本地開放防火牆或使用 VPN。採用業界標準的反向隧道架構(參考 Edge Impulse daemon / Balena tunnel 模式) |
|
||||
| **架構** | 瀏覽器 → nginx(:80) → relay-server(:3800) ←yamux/WebSocket→ 本地 edge-ai-server(:3721) ↔ Kneron 硬體。relay-server 支援多台 local server 同時連線,以 token 為 key 管理多個 yamux session |
|
||||
| **Relay Server** | 獨立 Go binary(`relay-server`),部署於雲端 EC2。接受本地 server 的 WebSocket 隧道連線,透過 yamux 多工串流轉發前端的 HTTP 和 WebSocket 請求 |
|
||||
| **Tunnel Client** | 整合在 edge-ai-server 內,透過 `--relay-url` 和 `--relay-token` 啟動。主動發起 WebSocket 連線到 relay server,建立 yamux session,接收並處理轉發的請求 |
|
||||
| **yamux 多工** | 使用 hashicorp/yamux(Consul/Nomad 同款)在單一 WebSocket 上建立多條獨立串流,每個 HTTP 請求或 WebSocket 連線是獨立的 yamux stream |
|
||||
| **透明轉發** | 前端自動從 localhost:3721/auth/token 取得本機 relay token,HTTP 請求透過 `X-Relay-Token` header、WebSocket 透過 `?token=` query parameter 路由到對應的本地 server |
|
||||
| **串流支援** | 完整支援 MJPEG 即時影像串流(relay 使用 `http.Flusher` 逐塊轉發)、WebSocket 推送(upgrade 後雙向 byte copy)、大檔案上傳 |
|
||||
| **認證機制** | 硬體 ID 自動認證:local server 啟動時自動從 MAC 位址 SHA-256 雜湊產生 16 字元 token,連接 relay 時透過 URL query parameter 傳遞。`--relay-token` flag 仍可手動覆蓋 |
|
||||
| **自動重連** | Tunnel client 斷線後自動重連,使用指數退避策略(1s → 2s → 4s → ... → 30s 上限)|
|
||||
| **健康檢查** | `GET /relay/status?token=xxx` 回傳 `{"tunnelConnected": true/false}`,無 token 時回傳所有隧道總數 |
|
||||
| **部署** | `deploy-ec2.sh --relay` 自動上傳 relay-server binary、建立 systemd service、設定 nginx proxy 規則 |
|
||||
| **不影響本地** | 未設定 `--relay-url` 時完全不啟動 tunnel client,本地模式零影響 |
|
||||
|
||||
#### F21 — 系統匣啟動器(System Tray Launcher)
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| **概述** | 系統匣常駐應用程式,整合於 edge-ai-server binary 中,透過 `--tray` flag 啟動(參考 Ollama 模式)。提供圖形化的 server 生命週期管理,無需開啟終端機 |
|
||||
| **GUI 框架** | fyne.io/systray(輕量級,無 GTK 依賴,由 Fyne 團隊維護),僅使用系統原生匣圖示 API,不引入完整 GUI 框架 |
|
||||
| **功能** | Start/Stop server 切換、伺服器狀態顯示(Running / Stopped + port 號)、匣圖示狀態反映(綠色 = 運行中、灰色 = 已停止)、Open Browser(開啟 `http://localhost:{port}` 或 relay URL + token)、View Logs(開啟 log 檔案或系統 Console)、Relay 狀態顯示(Connected / Disconnected)、Quit(停止 server 並退出) |
|
||||
| **架構** | `--tray` flag 啟動系統匣模式,Launcher 作為主進程管理 server 作為子進程(child process);從 `~/.edge-ai-platform/config.json` 讀取 port、relay URL/token 等設定,傳遞為 server 啟動參數 |
|
||||
| **Build Tags** | 使用 Go build tags 區分建置模式:預設建置包含 tray 功能(CGO_ENABLED=1),`notray` tag 產出無 CGO 依賴的純 CLI binary;安裝程式建置啟用 CGO 以支援系統匣 |
|
||||
| **macOS 平台** | 開機自動啟動透過 launchd(`~/Library/LaunchAgents/com.edge-ai-platform.launcher.plist`);使用 template icons(支援 Light/Dark menu bar 自動切換) |
|
||||
| **Windows 平台** | 開機自動啟動透過 Scheduled Task(`schtasks /create`,登入時觸發);使用 .ico 格式匣圖示 |
|
||||
| **Linux 平台** | 開機自動啟動透過 XDG autostart(`~/.config/autostart/edge-ai-platform.desktop`);使用 PNG 格式匣圖示 |
|
||||
| **設定整合** | 讀取 `~/.edge-ai-platform/config.json`(與 F17 安裝程式共用),設定項包含:`port`、`relayUrl`、`relayToken`、`autoStart`、`language` |
|
||||
| **子進程管理** | 啟動 server 時以 `exec.Command` 建立子進程,監控其存活狀態;Quit 時先優雅關閉子進程(SIGTERM),超時後強制終止(SIGKILL) |
|
||||
|
||||
#### F22 — 多租戶裝置隔離(Multi-Tenant Device Isolation)
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| **概述** | 多位使用者同時透過同一 relay server 存取各自的本機 edge-ai-server 時,每位使用者只能看到自己電腦上的裝置。零設定、自動化認證,無需手動輸入 token |
|
||||
| **硬體 ID Token** | `pkg/hwid/hwid.go`:讀取第一個非 loopback 網路介面的 MAC 位址 → SHA-256 → 取前 16 字元 hex 作為 token。確保同一台電腦每次產生相同 token |
|
||||
| **Relay Server 多工** | Relay server 從單一 session 改為 `map[string]*yamux.Session`(以 token 為 key),支援多台 local server 同時連線 |
|
||||
| **Token 路由** | 前端 HTTP 請求透過 `X-Relay-Token` header、WebSocket 透過 `?token=` query parameter 傳遞 token,relay 根據 token 路由到對應的 yamux session |
|
||||
| **自動傳遞** | Local server 啟動並連線 relay 後,自動開啟瀏覽器前往 `{relay_url}/?token={hwid_token}`。前端從 URL query param 讀取 token → 存入 localStorage → 以 `history.replaceState` 移除 URL 參數。此機制取代先前的 localhost fetch 方式,因 Chrome Private Network Access (PNA) 策略禁止公網頁面 fetch localhost |
|
||||
| **手動備份** | Settings 頁面提供 Relay Token 手動輸入欄位,供自動傳遞失敗時使用(如瀏覽器未自動開啟) |
|
||||
| **安全性** | Token 在 log 中遮蔽顯示(僅前 8 字元);`X-Relay-Token` 在轉發至 local server 前被移除 |
|
||||
| **向下相容** | 未設定 relay 時所有 token 機制不啟動,本地模式完全不受影響 |
|
||||
|
||||
#### F23 — 新手引導體驗(Onboarding & Empty States)
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| **概述** | 針對首次使用者提供引導式體驗,降低上手門檻。包含首次使用導覽、空狀態引導、以及更友善的錯誤訊息 |
|
||||
| **首次導覽** | 首次進入平台時顯示 3 步驟快速導覽:(1) 連接 Kneron 裝置 (2) 選擇 AI 模型 (3) 開始推論。使用 localStorage 記錄是否已完成導覽 |
|
||||
| **裝置空狀態** | Devices 頁面無裝置時顯示插圖 + 引導文字:「請插入 Kneron USB Dongle 並點擊掃描」,附帶硬體購買連結與常見問題 |
|
||||
| **模型空狀態** | Models 頁面無模型時引導使用者瀏覽預訓練模型庫或上傳自訂模型 |
|
||||
| **推論空狀態** | Workspace 頁面顯示清楚的步驟提示:先連接裝置 → 載入模型 → 選擇輸入來源 |
|
||||
| **錯誤訊息改善** | 所有錯誤訊息附帶可能原因與排除建議。例:「無法偵測到裝置」→ 建議檢查 USB 連接、安裝驅動、嘗試其他 USB 孔位 |
|
||||
| **幫助連結** | 關鍵操作旁提供 tooltip 或 info icon,連結至文件或常見問題頁 |
|
||||
|
||||
#### F24 — 自動版本檢查與更新提示(Auto Update Check)
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| **概述** | Server 啟動時自動檢查是否有新版本可用,前端 Settings 頁面顯示更新提示與下載連結 |
|
||||
| **版本檢查 API** | Server 啟動時呼叫 Gitea API(`/api/v1/repos/{owner}/{repo}/releases/latest`)取得最新版本號,與當前 `Version` 比較 |
|
||||
| **檢查頻率** | Server 啟動時檢查一次;前端 Settings 頁面可手動觸發檢查 |
|
||||
| **前端顯示** | Settings 頁面顯示:當前版本、最新版本、更新日期。若有新版本則顯示醒目的更新提示 badge + 變更日誌摘要 + 下載連結 |
|
||||
| **API 端點** | `GET /api/system/update-check` — 回傳 `{ currentVersion, latestVersion, updateAvailable, releaseUrl, releaseNotes }` |
|
||||
| **降級處理** | 檢查失敗時靜默忽略(網路不通、Gitea 不可達),不影響正常使用 |
|
||||
| **隱私** | 版本檢查不傳送任何使用者資料或硬體資訊 |
|
||||
|
||||
---
|
||||
|
||||
## B5. 功能路線圖(Post-MVP)
|
||||
@ -1456,4 +1530,16 @@ Phase 3 — 進階功能(長期差異化)
|
||||
|
||||
---
|
||||
|
||||
*文件版本:v2.7 | 日期:2026-03-02 | 狀態:更新中*
|
||||
*文件版本:v3.1 | 日期:2026-03-05 | 狀態:更新中*
|
||||
|
||||
---
|
||||
|
||||
## 修訂紀錄
|
||||
|
||||
| 版本 | 日期 | 變更內容 |
|
||||
|------|------|---------|
|
||||
| v3.1 | 2026-03-05 | F22: Token 傳遞改為 URL query param(因 Chrome PNA 限制移除 localhost fetch)。F16: 加入 Linux 平台支援 + GoReleaser 打包 firmware/detect script。F21: System Tray 增強(Open Browser、View Logs、Relay 狀態)。新增 F23 新手引導體驗(Onboarding + 空狀態 UX)。新增 F24 自動版本檢查與更新提示。功能總數更新至 20 項 |
|
||||
| v3.0 | 2026-03-04 | F20: 更新為多租戶架構(多 session map + token routing)。新增 F22 多租戶裝置隔離(硬體 ID 自動 token + 前端自動偵測 + relay token routing)。功能總數更新至 18 項 |
|
||||
| v2.9 | 2026-03-04 | F17: 新增 Relay 設定步驟、i18n 支援(EN + zh-TW)、config.json 共用設定檔、auto-start 改為註冊 Launcher、UI 現代化設計。新增 F21 系統匣啟動器(System Tray Launcher)。功能總數更新至 17 項 |
|
||||
| v2.8 | 2026-03-04 | 前次更新 |
|
||||
| v2.7 | 2026-03-02 | 前次更新 |
|
||||
|
||||
663
docs/TDD.md
663
docs/TDD.md
@ -7,9 +7,9 @@
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 文件名稱 | 邊緣 AI 開發平台 TDD |
|
||||
| 對應 PRD | PRD-Integrated.md v2.7 |
|
||||
| 版本 | v1.6 |
|
||||
| 日期 | 2026-03-02 |
|
||||
| 對應 PRD | PRD-Integrated.md v3.1 |
|
||||
| 版本 | v2.0 |
|
||||
| 日期 | 2026-03-05 |
|
||||
| 狀態 | 更新中 |
|
||||
|
||||
---
|
||||
@ -88,6 +88,27 @@
|
||||
│ ● 模型儲存庫 │
|
||||
│ ● 使用者帳號 │
|
||||
└──────────────────┘
|
||||
|
||||
雲端中繼模式(Cloud Relay Mode):
|
||||
┌────────────────────────────────────────────┐
|
||||
│ 雲端 EC2 │
|
||||
│ │
|
||||
│ ┌──────────┐ proxy ┌────────────────┐ │
|
||||
│ │ nginx │────────►│ relay-server │ │
|
||||
│ │ (:80) │ │ (:3800) │ │
|
||||
│ └──────────┘ └───────┬────────┘ │
|
||||
└───────────────────────────────┼────────────┘
|
||||
│ yamux/WebSocket
|
||||
│ (outbound from local)
|
||||
┌───────────────────────────────┼────────────┐
|
||||
│ 使用者電腦 │ │
|
||||
│ ┌────────────────────────────┴──────────┐ │
|
||||
│ │ edge-ai-server (:3721) │ │
|
||||
│ │ ├── tunnel client (→ relay) │ │
|
||||
│ │ ├── REST API + WebSocket │ │
|
||||
│ │ └── Device Manager → Kneron 硬體 │ │
|
||||
│ └───────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 技術選型總覽
|
||||
@ -1910,14 +1931,21 @@ type StepProgress struct {
|
||||
}
|
||||
```
|
||||
|
||||
**安裝精靈 UI 流程(6 步驟):**
|
||||
**i18n 多語系支援:**
|
||||
|
||||
安裝程式支援英文(EN)與繁體中文(zh-TW)雙語介面:
|
||||
- 語系偵測:啟動時讀取系統語言設定(macOS: `defaults read -g AppleLanguages`、Windows: `GetUserDefaultUILanguage()`)
|
||||
- 翻譯檔案:Go `embed` 嵌入 `installer/locales/en.json` 和 `installer/locales/zh-TW.json`
|
||||
- 使用者可在歡迎頁右上角切換語言,選擇結果寫入 config.json 的 `launcher.language`
|
||||
|
||||
**安裝精靈 UI 流程(7 步驟):**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 1: 歡迎頁 │
|
||||
│ Step 1: 歡迎頁 [EN|中] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ 🔧 Edge AI Platform Installer │ │
|
||||
│ │ Edge AI Platform Installer │ │
|
||||
│ │ │ │
|
||||
│ │ 版本: v0.2.0 │ │
|
||||
│ │ 安裝大小: ~350 MB │ │
|
||||
@ -1942,7 +1970,25 @@ type StepProgress struct {
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 3: 元件選擇 │
|
||||
│ Step 3: Relay 中繼設定(選填) │
|
||||
│ │
|
||||
│ 雲端中繼可讓您從外部網路存取本機裝置。 │
|
||||
│ 若不使用,請直接按「下一步」跳過。 │
|
||||
│ │
|
||||
│ Relay URL: │
|
||||
│ ┌──────────────────────────────────────────┐ │
|
||||
│ │ wss://relay.example.com/tunnel/connect │ │
|
||||
│ └──────────────────────────────────────────┘ │
|
||||
│ Relay Token: │
|
||||
│ ┌──────────────────────────────────────────┐ │
|
||||
│ │ ●●●●●●●●●●●● │ │
|
||||
│ └──────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [← 上一步] [下一步 →] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 4: 元件選擇 │
|
||||
│ │
|
||||
│ 必要元件: │
|
||||
│ ☑ Edge AI Server (10 MB) ── 核心伺服器 │
|
||||
@ -1959,7 +2005,7 @@ type StepProgress struct {
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 4: 安裝進度 │
|
||||
│ Step 5: 安裝進度 │
|
||||
│ │
|
||||
│ ✅ 下載 Edge AI Server 完成 │
|
||||
│ ✅ 解壓縮檔案 完成 │
|
||||
@ -1969,13 +2015,14 @@ type StepProgress struct {
|
||||
│ │ 安裝 numpy... │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ ○ 安裝模型檔案 等待中 │
|
||||
│ ○ 寫入設定檔 等待中 │
|
||||
│ ○ 偵測硬體 等待中 │
|
||||
│ │
|
||||
│ [取消] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 5: 硬體偵測 │
|
||||
│ Step 6: 硬體偵測 │
|
||||
│ │
|
||||
│ 偵測到的 Kneron 裝置: │
|
||||
│ │
|
||||
@ -1989,13 +2036,13 @@ type StepProgress struct {
|
||||
│ │ PID: 0x0100 │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ⚠️ 未偵測到裝置?請確認 USB 連接 │
|
||||
│ 未偵測到裝置?請確認 USB 連接。 │
|
||||
│ │
|
||||
│ [← 上一步] [下一步 →] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Step 6: 完成 │
|
||||
│ Step 7: 完成 │
|
||||
│ │
|
||||
│ ✅ 安裝成功! │
|
||||
│ │
|
||||
@ -2006,7 +2053,7 @@ type StepProgress struct {
|
||||
│ ☑ 立即啟動 Edge AI Server │
|
||||
│ ☑ 開啟瀏覽器 (http://localhost:3721) │
|
||||
│ ☐ 建立桌面捷徑 │
|
||||
│ ☐ 開機自動啟動 │
|
||||
│ ☑ 開機自動啟動 │
|
||||
│ │
|
||||
│ [完成] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
@ -2016,16 +2063,17 @@ type StepProgress struct {
|
||||
|
||||
| 步驟 | macOS 實作 | Windows 實作 |
|
||||
|------|-----------|-------------|
|
||||
| 下載 binary | `net/http` GET → Gitea release tar.gz | `net/http` GET → Gitea release zip |
|
||||
| 下載 binary | `net/http` GET → Gitea release tar.gz(tray-enabled build) | `net/http` GET → Gitea release zip(tray-enabled build) |
|
||||
| 解壓 | `archive/tar` + `compress/gzip` | `archive/zip` |
|
||||
| symlink | `os.Symlink()` → `/usr/local/bin/edge-ai-server` (需確認權限) | 加入 User PATH (`os.Setenv` + registry) |
|
||||
| libusb | `exec.Command("brew", "install", "libusb")` 或內嵌 dylib | 內嵌 `libusb-1.0.dll` 到安裝目錄 |
|
||||
| Python venv | `exec.Command("python3", "-m", "venv", ...)` | `exec.Command("python", "-m", "venv", ...)` 或內嵌 Python embedded |
|
||||
| pip install | `venv/bin/pip install numpy opencv-python-headless pyusb` | `venv\Scripts\pip install ...` |
|
||||
| ffmpeg | `brew install ffmpeg` (可選) | `winget install Gyan.FFmpeg` 或內嵌 (可選) |
|
||||
| 寫入設定檔 | 寫入 `~/.edge-ai-platform/config.json`(含 port、relay URL/token、語言偏好) | 寫入 `%LOCALAPPDATA%\EdgeAIPlatform\config.json` |
|
||||
| 硬體偵測 | `exec.Command(python, "kneron_detect.py")` | 同左 |
|
||||
| 桌面捷徑 | `~/Desktop/EdgeAI.command` | `shell:desktop\EdgeAI.lnk` (COM API) |
|
||||
| 開機啟動 | `~/Library/LaunchAgents/com.innovedus.edge-ai.plist` | `HKCU\...\Run` registry |
|
||||
| 開機啟動 | `~/Library/LaunchAgents/com.innovedus.edge-ai.plist`(執行 `edge-ai-server --tray`) | `HKCU\...\Run` registry(執行 `edge-ai-server.exe --tray`) |
|
||||
|
||||
**macOS 無 Homebrew 情境處理:**
|
||||
|
||||
@ -2291,6 +2339,520 @@ POST /api/clusters/:id/flash {modelId: "yolov5s"}
|
||||
|
||||
實際吞吐量受 USB 頻寬和 host CPU 限制,建議使用多個 USB controller 或 powered hub。
|
||||
|
||||
#### 8.5.16 雲端中繼隧道(F20 — Cloud Relay Tunnel)
|
||||
|
||||
| 前端元件 | 後端模組 | 說明 |
|
||||
|---------|---------|------|
|
||||
| (無需修改) | `internal/relay/server.go` | Relay server 核心邏輯 |
|
||||
| — | `cmd/relay-server/main.go` | Relay server 獨立 binary 進入點 |
|
||||
| — | `internal/tunnel/client.go` | Tunnel client(整合在 edge-ai-server) |
|
||||
| — | `pkg/wsconn/wsconn.go` | WebSocket-to-net.Conn 轉接器 |
|
||||
| — | `pkg/hwid/hwid.go` | 硬體 ID 產生 |
|
||||
| — | `internal/api/middleware.go` | CORS 支援 X-Relay-Token |
|
||||
| — | `internal/config/config.go` | 新增 `--relay-url`、`--relay-token` flags |
|
||||
| — | `main.go` | 條件啟動 tunnel client |
|
||||
| — | `scripts/deploy-ec2.sh` | EC2 部署腳本(含 relay 部署) |
|
||||
| `lib/constants.ts` | — | relay token 管理 |
|
||||
| `lib/api.ts` | — | X-Relay-Token header |
|
||||
| `lib/ws.ts` | — | token query param |
|
||||
| `components/relay-token-sync.tsx` | — | 自動偵測 token |
|
||||
| `app/settings/page.tsx` | — | relay token UI |
|
||||
|
||||
**整體架構:**
|
||||
|
||||
```
|
||||
瀏覽器 → nginx(:80) ─┬─ /api/* ──► relay-server(:3800) ◄──yamux/WS──► edge-ai-server(:3721)
|
||||
├─ /ws/* ──► ↑ ↕
|
||||
├─ /tunnel/ ─► │ Kneron 硬體
|
||||
└─ /* ──► 靜態前端 (SPA)
|
||||
```
|
||||
|
||||
**wsconn — WebSocket-to-net.Conn 轉接器(`pkg/wsconn/wsconn.go`):**
|
||||
|
||||
```go
|
||||
// 將 gorilla/websocket.Conn 包裝成 net.Conn 介面
|
||||
// 供 hashicorp/yamux 多工器使用
|
||||
type Conn struct {
|
||||
ws *websocket.Conn
|
||||
reader io.Reader // 當前 WebSocket 訊息的 reader
|
||||
rmu sync.Mutex // 讀取鎖
|
||||
wmu sync.Mutex // 寫入鎖
|
||||
}
|
||||
|
||||
// Read: 從 WebSocket Binary frame 讀取,自動處理訊息邊界
|
||||
// Write: 每次 Write 呼叫包裝為一個 Binary frame
|
||||
// 實現 net.Conn 所有方法: Close, LocalAddr, RemoteAddr, SetDeadline...
|
||||
```
|
||||
|
||||
**Relay Server(`internal/relay/server.go`):**
|
||||
|
||||
```go
|
||||
type Server struct {
|
||||
sessions map[string]*yamux.Session // token → yamux session
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// 路由:
|
||||
// GET /tunnel/connect?token=xxx → handleTunnel() — 接受 WS tunnel 連線
|
||||
// GET /relay/status → handleStatus() — 回傳 tunnel 狀態
|
||||
// /* → handleProxy() — 轉發請求到 tunnel
|
||||
|
||||
// getToken 從 X-Relay-Token header 或 ?token query param 取得 token
|
||||
func getToken(r *http.Request) string {
|
||||
if tok := r.Header.Get("X-Relay-Token"); tok != "" { return tok }
|
||||
return r.URL.Query().Get("token")
|
||||
}
|
||||
```
|
||||
|
||||
**handleTunnel 流程:**
|
||||
1. 從 URL query parameter 取得 token(必填,無 token 回傳 401)
|
||||
2. `upgrader.Upgrade()` 升級為 WebSocket
|
||||
3. `wsconn.New(conn)` 包裝成 net.Conn
|
||||
4. `yamux.Server(netConn, config)` 建立 server-side session
|
||||
5. 以 token 為 key 存入 `sessions[token]`,替換該 token 的舊 session(若有),舊 session 關閉
|
||||
6. 阻塞等待 `session.CloseChan()` 直到斷線,斷線後從 `sessions` 移除
|
||||
|
||||
**handleProxy 流程(一般 HTTP 請求):**
|
||||
1. 從 `X-Relay-Token` header 或 `?token=` query param 取得 token(`getToken(r)`)
|
||||
2. 以 token 查找 `sessions[token]` 取得對應 yamux session(RLock),找不到回傳 502
|
||||
3. `r.Header.Del("X-Relay-Token")` 移除 token header,避免傳到後端
|
||||
4. `session.Open()` 開啟新串流
|
||||
5. `r.Write(stream)` 將原始 HTTP request 寫入串流
|
||||
6. `http.ReadResponse(stream)` 讀取 response
|
||||
7. 複製 response headers + status code
|
||||
8. 串流 response body(使用 `http.Flusher` 支援 MJPEG 等串流回應)
|
||||
|
||||
**proxyWebSocket 流程(WebSocket 升級請求):**
|
||||
1. 寫入 HTTP upgrade request 到 yamux stream
|
||||
2. 讀取 response(預期 101 Switching Protocols)
|
||||
3. `hijacker.Hijack()` 奪取 client TCP 連線
|
||||
4. 寫入 101 response 給瀏覽器
|
||||
5. 雙向 `io.Copy`(client ↔ tunnel stream)
|
||||
|
||||
**Tunnel Client(`internal/tunnel/client.go`):**
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
relayURL string // ws(s)://host:port/tunnel/connect
|
||||
token string
|
||||
localAddr string // "127.0.0.1:3721"
|
||||
stopCh chan struct{}
|
||||
stoppedCh chan struct{}
|
||||
}
|
||||
|
||||
// Start() — 背景 goroutine 執行連線迴圈
|
||||
// Stop() — 關閉 tunnel 並等待清理完成
|
||||
```
|
||||
|
||||
**connect() 流程:**
|
||||
1. WebSocket Dial 到 relay server
|
||||
2. `yamux.Client(netConn, config)` 建立 client-side session
|
||||
3. 迴圈 `session.Accept()` 接受 relay 開啟的串流
|
||||
4. 每個串流在 goroutine 中處理
|
||||
|
||||
**handleStream() 流程:**
|
||||
1. `http.ReadRequest(stream)` 從 yamux stream 讀取 HTTP request
|
||||
2. 判斷是否為 WebSocket upgrade
|
||||
3. 一般請求:`http.DefaultTransport.RoundTrip(req)` → `resp.Write(stream)`
|
||||
4. WebSocket:raw TCP 連到 local server → 雙向 `io.Copy`
|
||||
|
||||
**自動重連:**
|
||||
```go
|
||||
// 指數退避: 1s, 2s, 4s, 8s, 16s, 30s (cap)
|
||||
func backoff(attempt int) time.Duration {
|
||||
d := min(2^(attempt-1), 30) * time.Second
|
||||
return d
|
||||
}
|
||||
```
|
||||
|
||||
**部署方式:**
|
||||
|
||||
```bash
|
||||
# 建置 relay-server binary
|
||||
make build-relay
|
||||
|
||||
# 部署到 EC2(含 relay + nginx proxy)— relay-server 不再需要 --relay-token(tokens 由各 client 自帶)
|
||||
bash scripts/deploy-ec2.sh user@host --key key.pem --build --relay
|
||||
|
||||
# 本地 server 連接到 relay(token 由 hwid.Generate() 自動產生)
|
||||
./edge-ai-server --relay-url ws://ec2-host:3800/tunnel/connect --relay-token <auto-generated>
|
||||
```
|
||||
|
||||
**nginx proxy 設定(自動由 deploy-ec2.sh 產生):**
|
||||
```nginx
|
||||
# API 請求 → relay(關閉 buffering 以支援串流)
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3800;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
# WebSocket → relay(upgrade 支援)
|
||||
location /ws/ {
|
||||
proxy_pass http://127.0.0.1:3800;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
# Tunnel 端點(超長 timeout)
|
||||
location /tunnel/ {
|
||||
proxy_pass http://127.0.0.1:3800;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 86400s;
|
||||
}
|
||||
```
|
||||
|
||||
**依賴:**
|
||||
| 套件 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| `github.com/hashicorp/yamux` | v0.1.2 | WebSocket 上的多工串流(Consul/Nomad 同款)|
|
||||
| `github.com/gorilla/websocket` | 既有 | WebSocket 連線(relay + tunnel 兩端共用)|
|
||||
|
||||
#### 8.5.17 系統匣啟動器(F21 — System Tray Launcher)
|
||||
|
||||
| 前端元件 | 後端模組 | 說明 |
|
||||
|---------|---------|------|
|
||||
| (無) | `server/tray/config.go` | Config struct、`LoadConfig()`、`Save()`、`ConfigDir()`、`ConfigPath()` |
|
||||
| — | `server/tray/stub.go` | `//go:build notray` — 未啟用 CGO 時印出錯誤訊息 |
|
||||
| — | `server/tray/tray.go` | `//go:build !notray` — `fyne.io/systray` 系統匣實作 |
|
||||
| — | `server/tray/icons.go` | `//go:embed` 載入 tray icon 資源 |
|
||||
| — | `server/tray/assets/` | `icon_running.png`(22x22 綠色)、`icon_stopped.png`(22x22 灰色) |
|
||||
| — | `server/main.go` | `--tray` flag 進入點:`config.Load()` → `tray.Run(tray.LoadConfig())` |
|
||||
|
||||
**目錄結構:**
|
||||
|
||||
```
|
||||
server/tray/
|
||||
├── config.go # Config struct, LoadConfig(), Save(), ConfigDir(), ConfigPath()
|
||||
├── stub.go # //go:build notray — prints error if --tray used without CGO
|
||||
├── tray.go # //go:build !notray — fyne.io/systray implementation
|
||||
├── icons.go # //go:embed for tray icon assets
|
||||
└── assets/
|
||||
├── icon_running.png # 22x22 green icon
|
||||
└── icon_stopped.png # 22x22 gray icon
|
||||
```
|
||||
|
||||
**Config 檔案格式**(`~/.edge-ai-platform/config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"server": { "port": 3721, "host": "127.0.0.1" },
|
||||
"relay": { "url": "", "token": "" },
|
||||
"launcher": { "autoStart": true, "language": "zh-TW" }
|
||||
}
|
||||
```
|
||||
|
||||
**核心元件:**
|
||||
|
||||
```go
|
||||
// server/tray/config.go
|
||||
type Config struct {
|
||||
Version int `json:"version"`
|
||||
Server ServerConfig `json:"server"`
|
||||
Relay RelayConfig `json:"relay"`
|
||||
Launcher LauncherConfig `json:"launcher"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type RelayConfig struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type LauncherConfig struct {
|
||||
AutoStart bool `json:"autoStart"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
func ConfigDir() string // 回傳 ~/.edge-ai-platform/
|
||||
func ConfigPath() string // 回傳 ~/.edge-ai-platform/config.json
|
||||
func LoadConfig() *Config // 讀取設定檔,不存在時回傳預設值
|
||||
func (c *Config) Save() error // 寫入設定檔
|
||||
```
|
||||
|
||||
```go
|
||||
// server/tray/tray.go (//go:build !notray)
|
||||
type TrayApp struct {
|
||||
cfg *Config
|
||||
cmd *exec.Cmd // server 子程序
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Run() — systray.Run(onReady, onExit) 主迴圈
|
||||
// onReady() — 設定 icon、tooltip、選單項目
|
||||
// startServer() — 以 os/exec 啟動子程序: os.Args[0] --port X --relay-url Y --relay-token Z
|
||||
// stopServer() — SIGTERM (Unix) / Process.Kill() (Windows) 優雅關閉
|
||||
// watchServer() — goroutine 監控 cmd.Wait(),子程序結束時更新 tray 狀態
|
||||
```
|
||||
|
||||
**`TrayApp` 管理 server 子程序:**
|
||||
|
||||
- Server 以子程序方式啟動:`os.Args[0] --port X --relay-url Y --relay-token Z`
|
||||
- 背景 goroutine 呼叫 `cmd.Wait()` 監控子程序狀態,若異常結束則更新 tray icon 及選單狀態
|
||||
- 優雅關閉:Unix 發送 `SIGTERM`;Windows 呼叫 `Process.Kill()`
|
||||
|
||||
**選單結構:**
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ Edge AI Platform │ ← Status(disabled,僅顯示)
|
||||
├──────────────────────────┤
|
||||
│ ▶ Start Server │ ← Start/Stop 切換
|
||||
├──────────────────────────┤
|
||||
│ Quit │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- 選單項目:Status (disabled) → Start/Stop toggle → Separator → Quit
|
||||
- Icon 平台適配:macOS 使用 `SetTemplateIcon()` 自動適配深色/淺色模式;Windows/Linux 使用 `SetIcon()`
|
||||
- 狀態圖示:運行中 = `icon_running.png`(綠色)、已停止 = `icon_stopped.png`(灰色)
|
||||
|
||||
**Build System:**
|
||||
|
||||
| 建置模式 | 環境變數 | 用途 |
|
||||
|---------|---------|------|
|
||||
| `CGO_ENABLED=0 -tags notray` | CLI 發行版 | GoReleaser 跨平台 CLI 分發(stub 印出錯誤) |
|
||||
| `CGO_ENABLED=1` | Tray 完整版 | 安裝程式 payload(含系統匣功能) |
|
||||
|
||||
```makefile
|
||||
# Makefile 新增目標
|
||||
build-server-tray:
|
||||
cd server && CGO_ENABLED=1 go build -o ../dist/edge-ai-server-tray
|
||||
|
||||
# installer-payload 目標改為依賴 build-server-tray
|
||||
installer-payload: build-frontend build-server-tray
|
||||
```
|
||||
|
||||
**進入點(`server/main.go`):**
|
||||
|
||||
```go
|
||||
func main() {
|
||||
cfg := config.Load() // 解析 --tray flag
|
||||
|
||||
if cfg.TrayMode {
|
||||
// Tray 模式:啟動系統匣,server 由 TrayApp 管理
|
||||
tray.Run(tray.LoadConfig())
|
||||
return
|
||||
}
|
||||
|
||||
// 一般模式:直接啟動 server
|
||||
startServer(cfg)
|
||||
}
|
||||
```
|
||||
|
||||
**Stub(`server/tray/stub.go`):**
|
||||
|
||||
```go
|
||||
//go:build notray
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Run(cfg *Config) {
|
||||
fmt.Fprintln(os.Stderr, "error: --tray requires CGO_ENABLED=1 build (this binary was built with -tags notray)")
|
||||
os.Exit(1)
|
||||
}
|
||||
```
|
||||
|
||||
**依賴:**
|
||||
|
||||
| 套件 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| `fyne.io/systray` | latest | 跨平台系統匣(macOS/Windows/Linux) |
|
||||
|
||||
#### 8.5.18 多租戶裝置隔離(F22 — Multi-Tenant Device Isolation)
|
||||
|
||||
| 前端元件 | 後端模組 | 說明 |
|
||||
|---------|---------|------|
|
||||
| `components/relay-token-sync.tsx` | `pkg/hwid/hwid.go` | 硬體 ID → token 產生 |
|
||||
| `lib/constants.ts` | `internal/relay/server.go` | Relay 多 session map |
|
||||
| `lib/api.ts` | `internal/api/router.go` | /auth/token endpoint |
|
||||
| `lib/ws.ts` | `internal/api/middleware.go` | CORS X-Relay-Token |
|
||||
| `app/settings/page.tsx` | `main.go` | 自動產生 + 注入 token |
|
||||
| `stores/camera-store.ts` | — | Stream URL token 注入 |
|
||||
| `components/camera/*` | — | appendRelayToken for <img> |
|
||||
|
||||
**硬體 ID 產生(`pkg/hwid/hwid.go`):**
|
||||
|
||||
```go
|
||||
func Generate() string {
|
||||
ifaces, _ := net.Interfaces()
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagLoopback != 0 { continue }
|
||||
mac := iface.HardwareAddr.String()
|
||||
if mac == "" { continue }
|
||||
return SHA256(mac)[:16] // 16-char hex token
|
||||
}
|
||||
return SHA256("unknown")[:16]
|
||||
}
|
||||
```
|
||||
|
||||
**Token 流程:**
|
||||
```
|
||||
啟動時:
|
||||
local server → hwid.Generate() → relayToken
|
||||
local server → tunnel.NewClient(relayURL, relayToken, addr) → 連到 relay
|
||||
local server → relayWebURL(wsURL, token) → http://relay-host/?token=xxx
|
||||
local server → openBrowser(relayHTTP) → 自動開啟瀏覽器
|
||||
|
||||
瀏覽器載入時(URL 帶 ?token=xxx):
|
||||
RelayTokenSync → syncRelayTokenFromURL()
|
||||
→ URLSearchParams.get("token") → "abc123..."
|
||||
→ localStorage.setItem("edge-ai-relay-token", token)
|
||||
→ history.replaceState() 移除 URL 中的 token 參數
|
||||
※ 取代先前的 fetch("http://localhost:3721/auth/token") 方式
|
||||
※ 原因:Chrome Private Network Access (PNA) 禁止公網頁面 fetch localhost
|
||||
|
||||
API 請求:
|
||||
api.ts → buildHeaders() → { "X-Relay-Token": token }
|
||||
relay → getToken(r) → sessions[token] → yamux stream → local server
|
||||
|
||||
WebSocket:
|
||||
ws.ts → buildUrl() → ws://host/ws/path?token=abc123
|
||||
relay → getToken(r) → sessions[token] → yamux stream → local server
|
||||
|
||||
MJPEG/圖片(<img src>):
|
||||
appendRelayToken(url) → url + "?token=abc123"
|
||||
relay → getToken(r) → sessions[token] → yamux stream → local server
|
||||
```
|
||||
|
||||
**Relay Server 多租戶架構:**
|
||||
```go
|
||||
type Server struct {
|
||||
sessions map[string]*yamux.Session // token → session
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{sessions: make(map[string]*yamux.Session)}
|
||||
}
|
||||
|
||||
// getToken 從 X-Relay-Token header 或 ?token query param 取得 token
|
||||
func getToken(r *http.Request) string {
|
||||
if tok := r.Header.Get("X-Relay-Token"); tok != "" { return tok }
|
||||
return r.URL.Query().Get("token")
|
||||
}
|
||||
```
|
||||
|
||||
**前端 Token 管理(`lib/constants.ts`):**
|
||||
```typescript
|
||||
// 從 localStorage 取得快取的 token
|
||||
export function getRelayToken(): string
|
||||
// 儲存 token 到 localStorage
|
||||
export function setRelayToken(token: string): void
|
||||
// 從 URL query param 讀取 token → 存入 localStorage → 清理 URL
|
||||
export function syncRelayTokenFromURL(): string
|
||||
// 向下相容 wrapper(內部呼叫 syncRelayTokenFromURL)
|
||||
export async function fetchAndCacheRelayToken(): Promise<string>
|
||||
// 為 <img src> 等無法帶 header 的元素附加 ?token= 參數
|
||||
export function appendRelayToken(url: string): string
|
||||
```
|
||||
|
||||
**Server 端自動開啟瀏覽器(`main.go`):**
|
||||
```go
|
||||
// relayWebURL 將 WebSocket URL 轉換為帶 token 的 HTTP URL
|
||||
// ws://host:port/tunnel/connect → http://host:port/?token=xxx
|
||||
func relayWebURL(wsURL, token string) string
|
||||
|
||||
// openBrowser 使用系統預設瀏覽器開啟 URL
|
||||
// darwin: open、linux: xdg-open、windows: cmd /c start
|
||||
func openBrowser(url string)
|
||||
```
|
||||
|
||||
**安全考量:**
|
||||
- Token 在 log 中僅顯示前 8 字元(`relayToken[:8]`)
|
||||
- `X-Relay-Token` header 在轉發前被 `r.Header.Del("X-Relay-Token")` 移除
|
||||
- CORS middleware 新增 `X-Relay-Token` 到 `Access-Control-Allow-Headers`
|
||||
- Token 透過 URL 傳遞後立即從網址列移除(`history.replaceState`),降低被意外分享的風險
|
||||
|
||||
#### 8.5.19 新手引導體驗(F23 — Onboarding & Empty States)
|
||||
|
||||
| 前端元件 | 後端模組 | 說明 |
|
||||
|---------|---------|------|
|
||||
| `components/onboarding/tour.tsx` | — | 3 步驟首次導覽(連接裝置 → 選模型 → 推論) |
|
||||
| `components/onboarding/empty-state.tsx` | — | 可重用空狀態元件(插圖 + 引導文字 + CTA) |
|
||||
| `hooks/use-first-visit.ts` | — | localStorage 記錄首次造訪狀態 |
|
||||
| `app/devices/page.tsx` | — | 裝置頁空狀態引導 |
|
||||
| `app/models/page.tsx` | — | 模型頁空狀態引導 |
|
||||
| `app/workspace/[deviceId]/page.tsx` | — | Workspace 步驟提示 |
|
||||
| `components/ui/error-message.tsx` | — | 增強錯誤訊息(附原因 + 排除建議) |
|
||||
|
||||
**首次導覽流程:**
|
||||
```
|
||||
用戶首次進入
|
||||
→ useFirstVisit() 檢查 localStorage("onboarding-complete")
|
||||
→ 未完成 → 顯示 OnboardingTour
|
||||
→ Step 1: "連接你的 Kneron 裝置"(示意圖 + 插入 USB 提示)
|
||||
→ Step 2: "選擇 AI 模型"(模型庫預覽 + 說明)
|
||||
→ Step 3: "開始推論!"(攝影機/影片/圖片選擇提示)
|
||||
→ 完成 → localStorage.set("onboarding-complete", "true")
|
||||
```
|
||||
|
||||
**空狀態設計原則:**
|
||||
- 每個空狀態包含:插圖/icon + 主標題 + 描述文字 + 主要 CTA 按鈕
|
||||
- 裝置頁:「尚未偵測到裝置」→ [掃描裝置] 按鈕 + 疑難排解連結
|
||||
- 模型頁:「探索預訓練模型」→ [瀏覽模型庫] + [上傳自訂模型]
|
||||
- Workspace:步驟式引導 → 1. 連接裝置 ✓/✗ → 2. 載入模型 ✓/✗ → 3. 選擇輸入
|
||||
|
||||
**錯誤訊息增強:**
|
||||
```typescript
|
||||
interface EnhancedError {
|
||||
message: string; // 原始錯誤訊息
|
||||
possibleCauses: string[]; // 可能原因列表
|
||||
suggestions: string[]; // 排除建議
|
||||
helpUrl?: string; // 文件連結
|
||||
}
|
||||
```
|
||||
|
||||
#### 8.5.20 自動版本檢查(F24 — Auto Update Check)
|
||||
|
||||
| 前端元件 | 後端模組 | 說明 |
|
||||
|---------|---------|------|
|
||||
| `app/settings/page.tsx` | `internal/api/handlers/system.go` | 版本資訊 + 更新提示 |
|
||||
| `components/update-badge.tsx` | `internal/update/checker.go` | 更新提示 badge |
|
||||
| — | `main.go` | 啟動時觸發檢查 |
|
||||
|
||||
**後端版本檢查(`internal/update/checker.go`):**
|
||||
```go
|
||||
type UpdateInfo struct {
|
||||
CurrentVersion string `json:"currentVersion"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
UpdateAvailable bool `json:"updateAvailable"`
|
||||
ReleaseURL string `json:"releaseUrl"`
|
||||
ReleaseNotes string `json:"releaseNotes"`
|
||||
PublishedAt string `json:"publishedAt"`
|
||||
}
|
||||
|
||||
// Check 呼叫 Gitea API 取得最新 release,與當前版本比較
|
||||
func Check(currentVersion, giteaURL, owner, repo string) (*UpdateInfo, error) {
|
||||
// GET /api/v1/repos/{owner}/{repo}/releases/latest
|
||||
// 比較 semver,失敗時靜默返回(不影響正常使用)
|
||||
}
|
||||
```
|
||||
|
||||
**API 端點:**
|
||||
```
|
||||
GET /api/system/update-check
|
||||
→ 200 { currentVersion, latestVersion, updateAvailable, releaseUrl, releaseNotes, publishedAt }
|
||||
→ 200 { currentVersion, latestVersion: "", updateAvailable: false } // 檢查失敗時
|
||||
```
|
||||
|
||||
**前端顯示:**
|
||||
- Settings 頁面「關於」區塊顯示當前版本 + build time
|
||||
- 有新版本時:醒目 badge + 版本號對比 + 變更摘要 + 下載按鈕
|
||||
- 檢查按鈕可手動觸發 `/api/system/update-check`
|
||||
|
||||
---
|
||||
|
||||
## 9. 開發環境與工具鏈
|
||||
@ -2370,6 +2932,8 @@ edge-ai-platform/
|
||||
│ ├── go.sum
|
||||
│ ├── main.go
|
||||
│ ├── cmd/
|
||||
│ │ └── relay-server/ # Relay server 獨立 binary
|
||||
│ │ └── main.go
|
||||
│ ├── internal/
|
||||
│ │ ├── api/
|
||||
│ │ ├── device/
|
||||
@ -2377,10 +2941,24 @@ edge-ai-platform/
|
||||
│ │ ├── model/
|
||||
│ │ ├── flash/
|
||||
│ │ ├── inference/
|
||||
│ │ ├── relay/ # Relay server 核心邏輯
|
||||
│ │ │ └── server.go
|
||||
│ │ ├── tunnel/ # Tunnel client(連接 relay)
|
||||
│ │ │ └── client.go
|
||||
│ │ └── config/
|
||||
│ ├── tray/ # 系統匣啟動器(F21)
|
||||
│ │ ├── config.go # Config struct, LoadConfig(), Save()
|
||||
│ │ ├── stub.go # //go:build notray stub
|
||||
│ │ ├── tray.go # //go:build !notray — systray 實作
|
||||
│ │ ├── icons.go # //go:embed tray icon assets
|
||||
│ │ └── assets/
|
||||
│ │ ├── icon_running.png # 22x22 運行中(綠色)
|
||||
│ │ └── icon_stopped.png # 22x22 已停止(灰色)
|
||||
│ ├── pkg/
|
||||
│ │ ├── serial/
|
||||
│ │ ├── usb/
|
||||
│ │ ├── wsconn/ # WebSocket↔net.Conn 轉接器
|
||||
│ │ │ └── wsconn.go
|
||||
│ │ └── logger/
|
||||
│ ├── data/ # 內建模型中繼資料
|
||||
│ │ └── models.json
|
||||
@ -2448,6 +3026,23 @@ edge-ai-platform/
|
||||
| Windows | windows/amd64 | `edge-ai-platform.exe` | `.msi` 安裝包 |
|
||||
| Linux | linux/amd64 | `edge-ai-platform-linux-amd64` | `.deb` / `.rpm` / 直接執行 |
|
||||
|
||||
#### Relay Server(雲端部署)
|
||||
|
||||
| 平台 | 架構 | 輸出檔案 | 部署方式 |
|
||||
|------|------|---------|---------|
|
||||
| Linux (EC2) | linux/amd64 | `relay-server` | `deploy-ec2.sh --relay` + systemd service |
|
||||
|
||||
```bash
|
||||
# 建置 relay-server
|
||||
make build-relay
|
||||
|
||||
# 部署到 EC2(自動上傳 binary + 建立 systemd service + 設定 nginx proxy)
|
||||
bash scripts/deploy-ec2.sh user@host --key key.pem --build --relay --relay-token SECRET
|
||||
|
||||
# 本地 edge-ai-server 連接到雲端 relay
|
||||
./edge-ai-server --relay-url ws://ec2-host:3800/tunnel/connect --relay-token SECRET
|
||||
```
|
||||
|
||||
### 11.3 GoReleaser 設定
|
||||
|
||||
```yaml
|
||||
@ -2465,6 +3060,8 @@ builds:
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
tags:
|
||||
- notray # CLI 發行版使用 stub(無系統匣)
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}}
|
||||
|
||||
@ -2473,8 +3070,20 @@ archives:
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- src: server/data/**/* # 模型定義 + NEF 檔案
|
||||
strip_parent: true
|
||||
- src: server/scripts/**/* # Python bridge + detect
|
||||
strip_parent: true
|
||||
- src: server/firmware/**/* # Kneron firmware 檔案
|
||||
strip_parent: true
|
||||
```
|
||||
|
||||
**打包內容說明:**
|
||||
- `data/`: models.json、custom-models/、nef/ 預訓練模型
|
||||
- `scripts/`: kneron_bridge.py(JSON-RPC bridge)、kneron_detect.py(USB 裝置偵測)
|
||||
- `firmware/`: KL520 fw_scpu.bin/fw_ncpu.bin、KL720 韌體更新檔
|
||||
|
||||
### 11.4 Makefile
|
||||
|
||||
```makefile
|
||||
@ -2497,7 +3106,18 @@ build-frontend:
|
||||
cp -r frontend/out server/frontend/out
|
||||
|
||||
build-backend: build-frontend
|
||||
cd server && go build -o ../dist/edge-ai-platform
|
||||
cd server && CGO_ENABLED=0 go build -tags notray -o ../dist/edge-ai-platform
|
||||
|
||||
# Tray-enabled build(安裝程式 payload 使用)
|
||||
build-server-tray: build-frontend
|
||||
cd server && CGO_ENABLED=1 go build -o ../dist/edge-ai-server-tray
|
||||
|
||||
# Relay server
|
||||
build-relay:
|
||||
cd server && go build -o ../dist/relay-server ./cmd/relay-server
|
||||
|
||||
# Installer payload(使用 tray-enabled build)
|
||||
installer-payload: build-frontend build-server-tray
|
||||
|
||||
# 跨平台打包
|
||||
release:
|
||||
@ -2839,4 +3459,15 @@ go.uber.org/zap // 結構化日誌
|
||||
|
||||
---
|
||||
|
||||
*文件版本:v1.6 | 日期:2026-03-02 | 狀態:更新中*
|
||||
*文件版本:v2.0 | 日期:2026-03-05 | 狀態:更新中*
|
||||
|
||||
---
|
||||
|
||||
## 變更紀錄
|
||||
|
||||
| 版本 | 日期 | 變更內容 |
|
||||
|------|------|---------|
|
||||
| v2.0 | 2026-03-05 | 更新 8.5.18 token 傳遞方式為 URL query param(因 Chrome PNA 限制移除 localhost fetch);新增 8.5.19 新手引導體驗(F23);新增 8.5.20 自動版本檢查(F24);GoReleaser 打包加入 firmware + detect script |
|
||||
| v1.9 | 2026-03-04 | 更新 8.5.16 中繼隧道為多租戶架構;新增 8.5.18 多租戶裝置隔離(F22):硬體 ID token 自動產生、relay multi-session map、前端自動 token 偵測、stream URL token 注入 |
|
||||
| v1.8 | 2026-03-04 | 新增 8.5.17 系統匣啟動器(F21);更新 8.5.14 安裝程式:新增 Step 3 Relay 中繼設定、i18n 雙語支援(EN/zh-TW)、config.json 寫入步驟、開機自動啟動改用 `--tray` flag;更新專案目錄結構新增 `server/tray/`;Makefile 新增 `build-server-tray` 與 `installer-payload` 目標;GoReleaser 新增 `-tags notray` |
|
||||
| v1.7 | 2026-03-04 | 前次更新 |
|
||||
|
||||
116
edge-ai-platform/README.md
Normal file
116
edge-ai-platform/README.md
Normal file
@ -0,0 +1,116 @@
|
||||
# Edge AI Platform
|
||||
|
||||
邊緣 AI 開發平台 — 管理 AI 模型、連接邊緣裝置(Kneron KL720/KL730)、即時攝影機推論。
|
||||
|
||||
單一執行檔,下載即可使用。
|
||||
|
||||
## Quick Start
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
# 安裝(下載至 ~/.edge-ai-platform)
|
||||
curl -fsSL https://gitea.innovedus.com/warrenchen/web_academy_prototype/raw/branch/main/scripts/install.sh | bash
|
||||
|
||||
# 啟動(Mock 模式,不需硬體)
|
||||
edge-ai-server --mock --mock-devices=3
|
||||
|
||||
# 開啟瀏覽器
|
||||
open http://127.0.0.1:3721
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
# 安裝
|
||||
irm https://gitea.innovedus.com/warrenchen/web_academy_prototype/raw/branch/main/scripts/install.ps1 | iex
|
||||
|
||||
# 啟動(Mock 模式)
|
||||
edge-ai-server.exe --mock --mock-devices=3
|
||||
|
||||
# 開啟瀏覽器
|
||||
Start-Process http://127.0.0.1:3721
|
||||
```
|
||||
|
||||
### 手動下載
|
||||
|
||||
從 [Releases](https://gitea.innovedus.com/warrenchen/web_academy_prototype/releases) 下載對應平台的壓縮檔:
|
||||
|
||||
| 平台 | 檔案 |
|
||||
|:-----|:-----|
|
||||
| macOS Intel | `edge-ai-platform_vX.Y.Z_darwin_amd64.tar.gz` |
|
||||
| macOS Apple Silicon | `edge-ai-platform_vX.Y.Z_darwin_arm64.tar.gz` |
|
||||
| Windows x64 | `edge-ai-platform_vX.Y.Z_windows_amd64.zip` |
|
||||
|
||||
解壓後執行:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
tar xzf edge-ai-platform_*.tar.gz
|
||||
cd edge-ai-platform_*/
|
||||
./edge-ai-server --mock --mock-devices=3
|
||||
|
||||
# Windows: 解壓 zip,在資料夾中開啟 PowerShell
|
||||
.\edge-ai-server.exe --mock --mock-devices=3
|
||||
```
|
||||
|
||||
然後開啟瀏覽器 http://127.0.0.1:3721
|
||||
|
||||
## 命令列選項
|
||||
|
||||
| Flag | 預設值 | 說明 |
|
||||
|:-----|:------|:-----|
|
||||
| `--port` | `3721` | 伺服器連接埠 |
|
||||
| `--host` | `127.0.0.1` | 伺服器位址 |
|
||||
| `--mock` | `false` | 啟用模擬裝置驅動 |
|
||||
| `--mock-camera` | `false` | 啟用模擬攝影機 |
|
||||
| `--mock-devices` | `1` | 模擬裝置數量 |
|
||||
| `--log-level` | `info` | 日誌等級(debug/info/warn/error) |
|
||||
| `--dev` | `false` | 開發模式(停用嵌入式前端) |
|
||||
|
||||
## 可選依賴
|
||||
|
||||
以下工具可增強功能,但**非必要**:
|
||||
|
||||
| 工具 | 用途 | macOS 安裝 | Windows 安裝 |
|
||||
|:-----|:-----|:----------|:------------|
|
||||
| `ffmpeg` | 攝影機擷取、影片處理 | `brew install ffmpeg` | `winget install Gyan.FFmpeg` |
|
||||
| `yt-dlp` | YouTube / 影片 URL 解析 | `brew install yt-dlp` | `winget install yt-dlp` |
|
||||
| `python3` | Kneron KL720 硬體驅動 | `brew install python3` | `winget install Python.Python.3.12` |
|
||||
|
||||
啟動時會自動檢查並提示缺少的工具。
|
||||
|
||||
## 解除安裝
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
rm -rf ~/.edge-ai-platform
|
||||
sudo rm -f /usr/local/bin/edge-ai-server
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\EdgeAIPlatform"
|
||||
# 手動從系統環境變數移除 PATH 中的 EdgeAIPlatform 路徑
|
||||
```
|
||||
|
||||
## 開發
|
||||
|
||||
```bash
|
||||
# 安裝依賴
|
||||
make install
|
||||
|
||||
# 啟動開發伺服器(前端 :3000 + 後端 :3721)
|
||||
make dev
|
||||
|
||||
# 編譯單一 binary
|
||||
make build
|
||||
|
||||
# 跨平台打包(本機測試,不發佈)
|
||||
make release-snapshot
|
||||
|
||||
# 發佈至 Gitea Release
|
||||
make release
|
||||
```
|
||||
1459
edge-ai-platform/docs/PRD-Integrated.md
Normal file
1459
edge-ai-platform/docs/PRD-Integrated.md
Normal file
File diff suppressed because it is too large
Load Diff
2842
edge-ai-platform/docs/TDD.md
Normal file
2842
edge-ai-platform/docs/TDD.md
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user