diff --git a/local-agent/.github/workflows/build.yml b/local-agent/.github/workflows/build.yml new file mode 100644 index 0000000..0b1ed0c --- /dev/null +++ b/local-agent/.github/workflows/build.yml @@ -0,0 +1,258 @@ +name: Build + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + workflow_dispatch: + +env: + GO_VERSION: '1.26' + NODE_VERSION: '22' + PNPM_VERSION: '9' + +jobs: + # ──────────────────────────────────────────────────────────────── + # macOS (Intel / x86_64) + # 使用 macos-13:目前最後的 Intel Mac runner,避免 macos-latest 被 + # 切到 Apple Silicon 後產出 ARM binary。 + # ──────────────────────────────────────────────────────────────── + build-macos: + name: Build macOS (x86_64) + runs-on: macos-13 + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install Wails CLI + run: | + go install github.com/wailsapp/wails/v2/cmd/wails@latest + echo "$HOME/go/bin" >> "$GITHUB_PATH" + + - name: Cache vendor/ + uses: actions/cache@v4 + with: + path: vendor/ + key: vendor-darwin-${{ hashFiles('Makefile') }}-${{ hashFiles('visiona-agent/wheels/macos/**') }} + restore-keys: | + vendor-darwin-${{ hashFiles('Makefile') }}- + vendor-darwin- + + - name: vendor-sync (macOS deps) + run: make vendor-sync + + - name: Build .dmg + run: make dmg + + - name: Verify .dmg + run: | + ls -lh dist/visiona-agent.dmg + file dist/visiona-agent.dmg + hdiutil imageinfo dist/visiona-agent.dmg | head -20 + + - name: Upload .dmg artifact + uses: actions/upload-artifact@v4 + with: + name: visiona-agent-macos-x64 + path: dist/visiona-agent.dmg + retention-days: 30 + if-no-files-found: error + + # ──────────────────────────────────────────────────────────────── + # Windows (x86_64) + # 使用 windows-2022。vendor-sync 的 macOS/Linux 部分會失敗, + # 因此只跑 *-windows 的 vendor targets。 + # ──────────────────────────────────────────────────────────────── + build-windows: + name: Build Windows (x86_64) + runs-on: windows-2022 + timeout-minutes: 60 + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install Wails CLI + run: | + go install github.com/wailsapp/wails/v2/cmd/wails@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Install Inno Setup + run: choco install innosetup -y --no-progress + shell: pwsh + + - name: Add Inno Setup to PATH + run: echo "/c/Program Files (x86)/Inno Setup 6" >> "$GITHUB_PATH" + + - name: Cache vendor/ + uses: actions/cache@v4 + with: + path: vendor/ + key: vendor-windows-${{ hashFiles('Makefile') }}-${{ hashFiles('visiona-agent/wheels/windows/**') }} + restore-keys: | + vendor-windows-${{ hashFiles('Makefile') }}- + vendor-windows- + + - name: vendor-sync (Windows-only deps) + run: | + make vendor-python-windows \ + vendor-wheels-windows \ + vendor-ffmpeg-windows + + - name: Build server.exe + env: + GOOS: windows + GOARCH: amd64 + run: | + mkdir -p payload/windows/bin + cd server + go build -o ../payload/windows/bin/visiona-agent-server.exe . + ls -lh ../payload/windows/bin/visiona-agent-server.exe + + - name: Build frontend (for go:embed) + run: make build-embed + + - name: Build .exe installer + run: make exe + + - name: Verify .exe + run: | + ls -lh dist/visiona-agent-*.exe || ls -lh dist/*.exe + file dist/visiona-agent-*.exe 2>/dev/null || true + + - name: Upload .exe artifact + uses: actions/upload-artifact@v4 + with: + name: visiona-agent-windows-x64 + path: dist/visiona-agent-*.exe + retention-days: 30 + if-no-files-found: error + + # ──────────────────────────────────────────────────────────────── + # Linux (x86_64) + # 使用 ubuntu-22.04(glibc 2.35,相容性比 24.04 更好)。 + # ──────────────────────────────────────────────────────────────── + build-linux: + name: Build Linux (x86_64) + runs-on: ubuntu-22.04 + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install system deps + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + libgtk-3-dev \ + libwebkit2gtk-4.1-dev \ + libusb-1.0-0-dev \ + fuse \ + libfuse2 \ + desktop-file-utils + + - name: Install Wails CLI + run: | + go install github.com/wailsapp/wails/v2/cmd/wails@latest + echo "$HOME/go/bin" >> "$GITHUB_PATH" + + - name: Install appimagetool + run: | + curl -fL -o /tmp/appimagetool \ + "https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage" + chmod +x /tmp/appimagetool + sudo mv /tmp/appimagetool /usr/local/bin/appimagetool + appimagetool --version || true + + - name: Cache vendor/ + uses: actions/cache@v4 + with: + path: vendor/ + key: vendor-linux-${{ hashFiles('Makefile') }}-${{ hashFiles('visiona-agent/wheels/linux/**') }} + restore-keys: | + vendor-linux-${{ hashFiles('Makefile') }}- + vendor-linux- + + - name: vendor-sync (Linux-only deps) + run: | + make vendor-python-linux \ + vendor-wheels-linux \ + vendor-ffmpeg-linux + + - name: Build server (Linux) + run: | + mkdir -p payload/linux/bin + cd server + GOOS=linux GOARCH=amd64 go build -o ../payload/linux/bin/visiona-agent-server . + ls -lh ../payload/linux/bin/visiona-agent-server + + - name: Build frontend (for go:embed) + run: make build-embed + + - name: Build AppImage + run: make appimage + + - name: Verify AppImage + run: | + ls -lh dist/visiona-agent-*.AppImage + file dist/visiona-agent-*.AppImage + + - name: Upload AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: visiona-agent-linux-x64 + path: dist/visiona-agent-*.AppImage + retention-days: 30 + if-no-files-found: error diff --git a/local-agent/.github/workflows/release.yml b/local-agent/.github/workflows/release.yml new file mode 100644 index 0000000..2a69a38 --- /dev/null +++ b/local-agent/.github/workflows/release.yml @@ -0,0 +1,61 @@ +# TODO: enable when first release ready +# +# 此 workflow 會在推送 v* tag 時自動建立 GitHub Release, +# 並把 build.yml 產出的三平台 artifact 上傳。 +# +# 啟用方式: +# 1. 確認 build.yml 三個 job 都能穩定成功 +# 2. 把 `on.push.tags` 區塊取消註解 +# 3. 推送 tag:`git tag v0.1.0 && git push origin v0.1.0` + +name: Release + +on: + workflow_dispatch: + # push: + # tags: ['v*'] + +jobs: + release: + name: Create GitHub Release + runs-on: ubuntu-22.04 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + # 觸發並等待 build.yml 完成(或改用 workflow_run 觸發) + # 這裡先用最簡單的做法:假設 build.yml 已經跑過,直接抓 artifact + - name: Download macOS artifact + uses: actions/download-artifact@v4 + with: + name: visiona-agent-macos-x64 + path: release/ + + - name: Download Windows artifact + uses: actions/download-artifact@v4 + with: + name: visiona-agent-windows-x64 + path: release/ + + - name: Download Linux artifact + uses: actions/download-artifact@v4 + with: + name: visiona-agent-linux-x64 + path: release/ + + - name: List release assets + run: ls -lh release/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: visionA Agent ${{ github.ref_name }} + draft: true + generate_release_notes: true + files: | + release/visiona-agent.dmg + release/visiona-agent-*.exe + release/visiona-agent-*.AppImage diff --git a/local-agent/.gitignore b/local-agent/.gitignore new file mode 100644 index 0000000..e07ffc3 --- /dev/null +++ b/local-agent/.gitignore @@ -0,0 +1,80 @@ + +# Autoflow Agent(由 autoflow-agent init 自動產生) +.claude/ +.autoflow/CLAUDE.md.backup.* +.autoflow/.backups/ + +# ── 第三方依賴(由 make vendor-sync 下載,不進 git;第三輪決策 Q-D=D2) ── +/vendor/** +!/vendor/.gitkeep +!/vendor/README.md +# R5-6b:macOS LGPL ffmpeg binary 進 git(沒有現成 LGPL binary 來源,自 build 成本高, +# commit 後開發者 clone 即可用,不必每次重 build ~15 分鐘) +!/vendor/ffmpeg/ +!/vendor/ffmpeg/macos/ +!/vendor/ffmpeg/macos/** + +# ── 建置產出 ── +/dist/ +/visiona-agent/build/bin/ +/visiona-agent/build/darwin/Resources/ +# M8-3:頂層 build/ffmpeg-macos/ 是 ffmpeg self-build 中間產物(source + obj + install,~180MB),不進 git +/build/ +/visiona-agent/payload/ +!/visiona-agent/payload/.gitkeep +# M1-11:頂層 payload/ 是 build 產物,不進 git(除了 .gitkeep) +/payload/** +!/payload/.gitkeep +/payload/*.tar.gz +/payload/*.zip + +# ── Go ── +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work.sum + +# ── Node / Next.js / pnpm ── +node_modules/ +.pnpm-store/ +.next/ +out/ +*.tsbuildinfo +.npm +.pnpm-debug.log* + +# ── Python(dev 時可能出現的 venv / cache) ── +__pycache__/ +*.py[cod] +*$py.class +.venv/ +venv/ +*.egg-info/ + +# ── Editor / IDE ── +.vscode/ +.idea/ +*.swp +*.swo + +# ── OS ── +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +desktop.ini + +# ── Env / Secrets ── +.env +.env.local +.env.*.local +*.pem +*.key + diff --git a/local-agent/Makefile b/local-agent/Makefile new file mode 100644 index 0000000..11eb131 --- /dev/null +++ b/local-agent/Makefile @@ -0,0 +1,706 @@ +# visionA Agent — Makefile(骨架,M1-1) +# +# 這份 Makefile 目前所有 targets 都只是 placeholder,實際內容會在後續任務逐步填入: +# - M1-2 複製 server core +# - M1-4 複製 frontend +# - M1-8 build-server / build-frontend 實作 +# - M1-9 visiona-agent (Wails) shell +# - M1-11 payload-macos +# - M1-12 installer-macos (dmg) +# - M2+ Windows / Linux / CI +# +# 詳見 .autoflow/04-architecture/build-pipeline.md + +SHELL := /bin/bash +VERSION ?= v0.1.0-dev +BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +OS := $(shell uname -s | tr A-Z a-z) +DIST := dist +PAYLOAD := visiona-agent/payload + +.PHONY: help \ + vendor-sync vendor-python vendor-wheels vendor-ffmpeg vendor-ffmpeg-macos-build \ + vendor-python-windows vendor-wheels-windows vendor-ffmpeg-windows \ + vendor-python-linux vendor-wheels-linux vendor-ffmpeg-linux \ + server build-server \ + frontend build-frontend build-embed \ + payload payload-macos payload-windows payload-linux \ + stage-macos stage-windows \ + wails-macos wails-windows wails-linux wails-sync-frontend \ + dmg exe exe-only _run-iscc appimage \ + dev test lint fmt \ + clean clean-all clean-build-exe clean-build-dmg clean-build-appimage + +# ── 幫助 ─────────────────────────────────────────────────────────── +help: ## 列出所有 make targets + @echo "visionA Agent — available targets (M1-1 skeleton)" + @echo "" + @echo " 依賴同步:" + @echo " vendor-sync 下載 python-build-standalone / wheels / ffmpeg → vendor/" + @echo "" + @echo " Build(單元):" + @echo " server build Go server binary (→ dist/visiona-agent-server)" + @echo " frontend pnpm build Next.js 靜態產物 (→ frontend/out)" + @echo "" + @echo " Payload 準備:" + @echo " payload-macos stage macOS payload → visiona-agent/payload/" + @echo " payload-windows stage Windows payload" + @echo " payload-linux stage Linux payload" + @echo "" + @echo " Wails 應用 build:" + @echo " wails-macos wails build darwin/amd64" + @echo " wails-windows wails build windows/amd64" + @echo " wails-linux wails build linux/amd64" + @echo "" + @echo " 安裝檔打包:" + @echo " dmg macOS .dmg(dmgbuild)" + @echo " exe Windows .exe(Inno Setup)" + @echo " appimage Linux .AppImage" + @echo "" + @echo " 工具:" + @echo " clean 清除 dist/ payload/" + @echo "" + @echo "Note: 目前所有 target 都是 placeholder(只印 TODO),尚未實作。" + +# ── 依賴同步 ─────────────────────────────────────────────────────── +PYTHON_VERSION := 3.12.9 +PBS_RELEASE := 20250317 +PBS_URL_DARWIN := https://github.com/astral-sh/python-build-standalone/releases/download/$(PBS_RELEASE)/cpython-$(PYTHON_VERSION)+$(PBS_RELEASE)-x86_64-apple-darwin-install_only.tar.gz + +# ── ffmpeg(LGPL v3,方案 B 混合)── +# v2 TDD §2.2:macOS 自 build decoder-only(~20 MB,commit 到 vendor/ffmpeg/macos/), +# Windows / Linux 用 BtbN 的 n7.1 LGPL build。 +FFMPEG_VERSION := n7.1 +FFMPEG_SRC_URL := https://github.com/FFmpeg/FFmpeg/archive/refs/tags/$(FFMPEG_VERSION).tar.gz +# sha256 於第一次 build 時由 `make vendor-ffmpeg-macos-build` 計算後填入 vendor/ffmpeg/macos/BUILD.md, +# 之後每次 build 使用此值做 integrity check(下方 Makefile 變數亦同步更新)。 +FFMPEG_SRC_SHA256 := 7ddad2d992bd250a6c56053c26029f7e728bebf0f37f80cf3f8a0e6ec706431a + +vendor-sync: vendor-python vendor-wheels vendor-ffmpeg ## 下載所有第三方依賴到 vendor/(不進 git,第三輪決策 Q-D=D2) + @echo "==> vendor-sync 完成" + +vendor-python: ## 下載 python-build-standalone tarball → vendor/python/darwin/ + @mkdir -p vendor/python/darwin + @if [ ! -f vendor/python/darwin/python.tar.gz ]; then \ + echo "==> 下載 python-build-standalone $(PYTHON_VERSION)+$(PBS_RELEASE) (macOS x86_64 install_only)..."; \ + curl -fL -o vendor/python/darwin/python.tar.gz "$(PBS_URL_DARWIN)"; \ + echo "==> tarball 大小:$$(du -sh vendor/python/darwin/python.tar.gz | cut -f1)"; \ + else \ + echo "==> python tarball 已存在,跳過下載 ($$(du -sh vendor/python/darwin/python.tar.gz | cut -f1))"; \ + fi + +vendor-wheels: ## 同步 wheels → vendor/wheels/darwin/(內部 wheel 從 visiona-agent/wheels 複製,公開相依用 pip download) + @mkdir -p vendor/wheels/darwin + @echo "==> 同步內部 wheels(KneronPLUS 等)..." + @if [ -d visiona-agent/wheels/macos ]; then \ + cp visiona-agent/wheels/macos/*.whl vendor/wheels/darwin/ 2>/dev/null || true; \ + fi + @echo "==> 從 PyPI 下載公開相依 wheels (cp312, macosx_x86_64)..." + @pip3 download \ + --only-binary=:all: \ + --platform macosx_10_9_x86_64 \ + --platform macosx_11_0_x86_64 \ + --platform macosx_12_0_x86_64 \ + --python-version 3.12 \ + --implementation cp \ + --dest vendor/wheels/darwin \ + numpy opencv-python-headless pyusb requests || echo "WARN: pip download 部分失敗(詳見上方訊息)" + @echo "==> wheels 總覽:" + @ls -1 vendor/wheels/darwin/*.whl 2>/dev/null | wc -l | xargs -I{} echo " 共 {} 個 wheel" + @du -sh vendor/wheels/darwin + +vendor-ffmpeg: ## macOS:LGPL v3 ffmpeg + ffprobe 已 commit 到 vendor/ffmpeg/macos/,本 target 只驗證存在 + LGPL 合規 + @if [ ! -f vendor/ffmpeg/macos/ffmpeg ]; then \ + echo "❌ vendor/ffmpeg/macos/ffmpeg 不存在。"; \ + echo " 第一次 build 請執行:make vendor-ffmpeg-macos-build"; \ + echo " (只需要在升級 ffmpeg 版本時跑一次;平常 clone repo 後 binary 已在 git 內)"; \ + exit 1; \ + fi + @if [ ! -f vendor/ffmpeg/macos/ffprobe ]; then \ + echo "❌ vendor/ffmpeg/macos/ffprobe 不存在。"; \ + echo " 請執行:make vendor-ffmpeg-macos-build"; \ + exit 1; \ + fi + @echo "==> vendor/ffmpeg/macos/ffmpeg 存在:$$(du -h vendor/ffmpeg/macos/ffmpeg | cut -f1)" + @echo "==> vendor/ffmpeg/macos/ffprobe 存在:$$(du -h vendor/ffmpeg/macos/ffprobe | cut -f1)" + @if vendor/ffmpeg/macos/ffmpeg -version 2>&1 | grep -qE -- '--enable-gpl|libx264|libx265'; then \ + echo "❌ LGPL 驗證失敗:binary 含 --enable-gpl / libx264 / libx265"; \ + exit 1; \ + fi + @echo "==> LGPL 驗證通過(無 --enable-gpl / libx264 / libx265)" + +# 只有升級 ffmpeg 版本時才跑此 target;binary 產出後 commit 到 git(v2 TDD R5-6b)。 +# 需要的系統依賴(macOS): +# brew install pkg-config nasm # 或 yasm +vendor-ffmpeg-macos-build: ## macOS:從源碼 build LGPL v3 decoder-only ffmpeg + ffprobe(升級時才跑,~15 分鐘) + @if [ "$$(uname -s)" != "Darwin" ]; then \ + echo "❌ vendor-ffmpeg-macos-build 只能在 macOS 上跑"; exit 1; \ + fi + @command -v pkg-config >/dev/null 2>&1 || { echo "❌ 需要 pkg-config(brew install pkg-config)"; exit 1; } + @command -v nasm >/dev/null 2>&1 || command -v yasm >/dev/null 2>&1 || { echo "❌ 需要 nasm 或 yasm(brew install nasm)"; exit 1; } + @mkdir -p vendor/ffmpeg/macos build/ffmpeg-macos + @echo "==> 下載 ffmpeg source $(FFMPEG_VERSION)..." + curl -fL -o build/ffmpeg-macos/source.tar.gz "$(FFMPEG_SRC_URL)" + @echo "==> 驗證 source tarball sha256..." + @echo "$(FFMPEG_SRC_SHA256) build/ffmpeg-macos/source.tar.gz" | shasum -a 256 -c || { \ + echo "❌ sha256 不符,請更新 Makefile 中的 FFMPEG_SRC_SHA256 或檢查來源"; \ + echo " 實際 sha256:$$(shasum -a 256 build/ffmpeg-macos/source.tar.gz | awk '{print $$1}')"; \ + exit 1; } + @rm -rf build/ffmpeg-macos/src build/ffmpeg-macos/install + @mkdir -p build/ffmpeg-macos/src + tar xzf build/ffmpeg-macos/source.tar.gz -C build/ffmpeg-macos/src --strip-components=1 + @echo "==> configure(decoder-only LGPL v3)..." + cd build/ffmpeg-macos/src && ./configure \ + --prefix="$$(pwd)/../install" \ + --enable-version3 \ + --disable-debug \ + --disable-doc \ + --disable-ffplay \ + --disable-network \ + --disable-autodetect \ + --disable-shared \ + --enable-static \ + --disable-everything \ + --enable-small \ + --enable-protocol=file,pipe \ + --enable-demuxer=mov,avi,mpegps,mpegts,matroska,image2 \ + --enable-decoder=h264,hevc,mpeg1video,mpeg2video,mpeg4,mjpeg,prores,vp8,vp9,aac,mp2,mp3,pcm_s16le,pcm_s16be \ + --enable-parser=h264,hevc,mpeg4video,mpegaudio,aac \ + --enable-filter=scale,format,fps,null,anull \ + --enable-muxer=image2pipe,image2,null \ + --enable-encoder=mjpeg \ + --enable-swscale \ + --enable-swresample \ + --extra-cflags="-arch x86_64 -mmacosx-version-min=10.15" \ + --extra-ldflags="-arch x86_64 -mmacosx-version-min=10.15 -Wl,-search_paths_first" \ + --arch=x86_64 \ + --target-os=darwin \ + --cc="clang -arch x86_64" + cd build/ffmpeg-macos/src && make -j$$(sysctl -n hw.ncpu) + cd build/ffmpeg-macos/src && make install + @echo "==> 複製 ffmpeg + ffprobe 到 vendor/ffmpeg/macos/..." + cp build/ffmpeg-macos/install/bin/ffmpeg vendor/ffmpeg/macos/ffmpeg + cp build/ffmpeg-macos/install/bin/ffprobe vendor/ffmpeg/macos/ffprobe + @strip -S -x vendor/ffmpeg/macos/ffmpeg + @strip -S -x vendor/ffmpeg/macos/ffprobe + @chmod +x vendor/ffmpeg/macos/ffmpeg vendor/ffmpeg/macos/ffprobe + @echo "==> ad-hoc 簽章..." + codesign --force --sign - vendor/ffmpeg/macos/ffmpeg + codesign --force --sign - vendor/ffmpeg/macos/ffprobe + @echo "==> 驗證授權(configuration line 不含 --enable-gpl / libx264 / libx265)..." + @if vendor/ffmpeg/macos/ffmpeg -version 2>&1 | grep -E -- '--enable-gpl|libx264|libx265'; then \ + echo "❌ LGPL 驗證失敗:build 不該出現 gpl / x264 / x265"; exit 1; \ + fi + @echo "==> 複製 COPYING.LGPLv3 到 vendor/ffmpeg/macos/..." + cp build/ffmpeg-macos/src/COPYING.LGPLv3 vendor/ffmpeg/macos/COPYING.LGPLv3 + @echo "==> ffmpeg 大小:$$(du -h vendor/ffmpeg/macos/ffmpeg | cut -f1)" + @echo "==> ffprobe 大小:$$(du -h vendor/ffmpeg/macos/ffprobe | cut -f1)" + @echo "" + @echo "✅ macOS LGPL ffmpeg build 完成。接下來請:" + @echo " 1) 更新 vendor/ffmpeg/macos/BUILD.md 的 Binary sha256 區塊" + @echo " 2) git add vendor/ffmpeg/macos/{ffmpeg,ffprobe,COPYING.LGPLv3,BUILD.md}" + +# ── Build(單元) ────────────────────────────────────────────────── +server: build-server ## alias for build-server + +build-server: build-embed ## build Go server binary → dist/visiona-agent-server(會先 build frontend + embed) + @mkdir -p $(DIST) + cd server && go build -o ../$(DIST)/visiona-agent-server . + @echo "built: $(DIST)/visiona-agent-server" + +build-server-windows: build-embed ## 交叉/原生 build Windows server → payload/windows/bin/visiona-agent-server.exe + @mkdir -p payload/windows/bin + cd server && GOOS=windows GOARCH=amd64 go build -o ../payload/windows/bin/visiona-agent-server.exe . + @echo "built: payload/windows/bin/visiona-agent-server.exe" + +build-server-linux: build-embed ## 交叉/原生 build Linux server → payload/linux/bin/visiona-agent-server + @mkdir -p payload/linux/bin + cd server && GOOS=linux GOARCH=amd64 go build -o ../payload/linux/bin/visiona-agent-server . + @echo "built: payload/linux/bin/visiona-agent-server" + +frontend: build-frontend ## alias for build-frontend + +build-frontend: ## pnpm build → frontend/out/ + @echo "==> pnpm build frontend..." + cd frontend && pnpm install --frozen-lockfile && pnpm build + @echo "==> frontend/out 完成:" + @du -sh frontend/out + +build-embed: build-frontend ## 同步 frontend/out → server/web/out 供 go:embed + @echo "==> 同步 frontend/out → server/web/out..." + rm -rf server/web/out + mkdir -p server/web/out + cp -R frontend/out/. server/web/out/ + @du -sh server/web/out + +# ── Payload 準備 ─────────────────────────────────────────────────── +payload: payload-$(OS) ## 依當前 OS 準備 payload + +payload-macos: build-server vendor-python vendor-wheels vendor-ffmpeg ## 準備 macOS payload → payload/darwin/(含 python runtime + wheels + ffmpeg) + @echo "==> 建立 macOS payload (binary + models + scripts + python + wheels + ffmpeg + ffprobe)..." + rm -rf payload/darwin + mkdir -p payload/darwin/bin payload/darwin/data payload/darwin/scripts payload/darwin/python payload/darwin/wheels + cp dist/visiona-agent-server payload/darwin/bin/ + cp vendor/ffmpeg/macos/ffmpeg payload/darwin/bin/ + cp vendor/ffmpeg/macos/ffprobe payload/darwin/bin/ + cp vendor/ffmpeg/macos/COPYING.LGPLv3 payload/darwin/bin/ffmpeg-COPYING.LGPLv3 + chmod +x payload/darwin/bin/ffmpeg payload/darwin/bin/ffprobe + cp -R server/data/* payload/darwin/data/ + cp -R server/scripts/* payload/darwin/scripts/ + cp vendor/python/darwin/python.tar.gz payload/darwin/python/ + @cp vendor/wheels/darwin/*.whl payload/darwin/wheels/ 2>/dev/null || true + @echo "==> macOS payload 完成:" + @du -sh payload/darwin + @echo "" + @echo "payload/darwin 結構:" + @find payload/darwin -maxdepth 2 -type d | sed 's|^| |' + @echo "" + @echo "payload/darwin/python:" + @ls -lh payload/darwin/python + @echo "payload/darwin/wheels:" + @ls -1 payload/darwin/wheels | head -20 + +stage-macos: payload-macos ## 將 payload/darwin/ 放到 Wails build/darwin/Resources/ + @echo "==> 放置 payload 到 Wails build/darwin/Resources..." + rm -rf visiona-agent/build/darwin/Resources + mkdir -p visiona-agent/build/darwin/Resources + cp -R payload/darwin/. visiona-agent/build/darwin/Resources/ + @echo "==> stage 完成:" + @du -sh visiona-agent/build/darwin/Resources + +# ── M4:Windows vendor + payload + wails + exe ───────────────────── +# 注意:wails-windows 與 exe 必須在 Windows runner 上跑;在 macOS 上會明確 fail。 +# payload-windows / vendor-*-windows 是 curl 下載,跨平台可跑(server.exe 步驟除外)。 + +PBS_URL_WINDOWS := https://github.com/astral-sh/python-build-standalone/releases/download/$(PBS_RELEASE)/cpython-$(PYTHON_VERSION)+$(PBS_RELEASE)-x86_64-pc-windows-msvc-install_only.tar.gz +# LGPL v3 build(BtbN n7.1 穩定分支,含 LGPL-safe extra libs)— v2 TDD §3 +FFMPEG_URL_WINDOWS := https://github.com/BtbN/FFmpeg-Builds/releases/latest/download/ffmpeg-n7.1-latest-win64-lgpl-7.1.zip + +vendor-python-windows: ## 下載 python-build-standalone Windows x86_64 → vendor/python/windows/ + @mkdir -p vendor/python/windows + @if [ ! -f vendor/python/windows/python.tar.gz ]; then \ + echo "==> 下載 python-build-standalone $(PYTHON_VERSION)+$(PBS_RELEASE) (Windows x86_64 install_only)..."; \ + curl -fL -o vendor/python/windows/python.tar.gz "$(PBS_URL_WINDOWS)"; \ + echo "==> tarball 大小:$$(du -sh vendor/python/windows/python.tar.gz | cut -f1)"; \ + else \ + echo "==> python (Windows) tarball 已存在,跳過 ($$(du -sh vendor/python/windows/python.tar.gz | cut -f1))"; \ + fi + +vendor-wheels-windows: ## 同步 Windows wheels → vendor/wheels/windows/ + @mkdir -p vendor/wheels/windows + @echo "==> 同步內部 wheels (Windows, KneronPLUS 等)..." + @if [ -d visiona-agent/wheels/windows ]; then \ + cp visiona-agent/wheels/windows/*.whl vendor/wheels/windows/ 2>/dev/null || true; \ + fi + @echo "==> 從 PyPI 下載公開相依 wheels (cp312, win_amd64)..." + @PY=""; \ + for candidate in "$$VISIONA_PYTHON" "py -3" python3 python; do \ + [ -z "$$candidate" ] && continue; \ + resolved=$$(command -v $${candidate%% *} 2>/dev/null || true); \ + case "$$resolved" in *WindowsApps*) continue ;; esac; \ + if $$candidate --version >/dev/null 2>&1; then PY="$$candidate"; break; fi; \ + done; \ + if [ -z "$$PY" ]; then \ + echo "WARN: 找不到真實 python(WindowsApps stub 不算),跳過 PyPI 下載(僅使用內部 wheels)"; \ + else \ + echo "==> 使用 $$PY -m pip"; \ + $$PY -m pip download \ + --only-binary=:all: \ + --platform win_amd64 \ + --python-version 3.12 \ + --implementation cp \ + --dest vendor/wheels/windows \ + numpy opencv-python-headless pyusb requests || echo "WARN: pip download 部分失敗(詳見上方訊息)"; \ + fi + @echo "==> Windows wheels 總覽:" + @ls -1 vendor/wheels/windows/*.whl 2>/dev/null | wc -l | xargs -I{} echo " 共 {} 個 wheel" + @du -sh vendor/wheels/windows 2>/dev/null || true + +vendor-ffmpeg-windows: ## 下載 ffmpeg Windows LGPL v3 build (n7.1) → vendor/ffmpeg/windows/ + @mkdir -p vendor/ffmpeg/windows + @if [ -f vendor/ffmpeg/windows/ffmpeg.exe ] && [ -f vendor/ffmpeg/windows/ffprobe.exe ]; then \ + echo "==> ffmpeg.exe + ffprobe.exe 已存在,跳過"; \ + else \ + echo "==> 下載 BtbN LGPL ffmpeg (Windows, n7.1)..."; \ + curl -fL -o vendor/ffmpeg/windows/ffmpeg-win.zip "$(FFMPEG_URL_WINDOWS)"; \ + PY=""; \ + for candidate in "$$VISIONA_PYTHON" "py -3" python3 python; do \ + [ -z "$$candidate" ] && continue; \ + resolved=$$(command -v $${candidate%% *} 2>/dev/null || true); \ + case "$$resolved" in *WindowsApps*) continue ;; esac; \ + if $$candidate --version >/dev/null 2>&1; then PY="$$candidate"; break; fi; \ + done; \ + if [ -z "$$PY" ]; then echo "ERROR: 需要真實 python 來解壓 zip(WindowsApps stub 無法使用)"; exit 1; fi; \ + echo "==> 使用 $$PY 解壓 ffmpeg zip(取出 ffmpeg.exe / ffprobe.exe / LICENSE.txt)"; \ + $$PY -c "import zipfile, os; z=zipfile.ZipFile('vendor/ffmpeg/windows/ffmpeg-win.zip'); \ +wanted=['/bin/ffmpeg.exe','/bin/ffprobe.exe','/LICENSE.txt','/COPYING.LGPLv3']; \ +members=[n for n in z.namelist() if any(n.endswith(w) for w in wanted)]; \ +assert any(n.endswith('/bin/ffmpeg.exe') for n in members), 'ffmpeg.exe not found in zip'; \ +assert any(n.endswith('/bin/ffprobe.exe') for n in members), 'ffprobe.exe not found in zip'; \ +os.makedirs('vendor/ffmpeg/windows', exist_ok=True); \ +[open('vendor/ffmpeg/windows/'+m.rsplit('/',1)[1],'wb').write(z.read(m)) for m in members]; \ +print('extracted:', [m.rsplit('/',1)[1] for m in members])" || { echo "ERROR: python 解壓失敗"; exit 1; }; \ + rm -f vendor/ffmpeg/windows/ffmpeg-win.zip; \ + [ -f vendor/ffmpeg/windows/ffmpeg.exe ] || { echo "ERROR: ffmpeg.exe 沒被寫出"; exit 1; }; \ + [ -f vendor/ffmpeg/windows/ffprobe.exe ] || { echo "ERROR: ffprobe.exe 沒被寫出"; exit 1; }; \ + echo "==> ffmpeg.exe 大小:$$(du -h vendor/ffmpeg/windows/ffmpeg.exe | cut -f1)"; \ + echo "==> ffprobe.exe 大小:$$(du -h vendor/ffmpeg/windows/ffprobe.exe | cut -f1)"; \ + fi + +payload-windows: build-server-windows vendor-python-windows vendor-wheels-windows vendor-ffmpeg-windows ## 準備 Windows payload → payload/windows/ + @echo "==> 建立 Windows payload (binary + models + scripts + python + wheels + ffmpeg)..." + @# 注意:不 rm -rf payload/windows,因為 build-server-windows 已先把 .exe 放進去 + mkdir -p payload/windows/bin payload/windows/data payload/windows/scripts payload/windows/python payload/windows/wheels + @if [ ! -f payload/windows/bin/visiona-agent-server.exe ]; then \ + echo "!! ERROR: payload/windows/bin/visiona-agent-server.exe 不存在,build-server-windows 可能失敗 !!"; \ + exit 1; \ + fi + cp vendor/ffmpeg/windows/ffmpeg.exe payload/windows/bin/ + cp vendor/ffmpeg/windows/ffprobe.exe payload/windows/bin/ + @# LGPL 授權條款(BtbN build 自帶 LICENSE.txt;COPYING.LGPLv3 不一定在壓縮檔內,失敗不致命) + @cp vendor/ffmpeg/windows/LICENSE.txt payload/windows/bin/ffmpeg-LICENSE.txt 2>/dev/null || true + @cp vendor/ffmpeg/windows/COPYING.LGPLv3 payload/windows/bin/ffmpeg-COPYING.LGPLv3 2>/dev/null || true + cp -R server/data/. payload/windows/data/ + cp -R server/scripts/. payload/windows/scripts/ + cp vendor/python/windows/python.tar.gz payload/windows/python/ + @cp vendor/wheels/windows/*.whl payload/windows/wheels/ 2>/dev/null || true + @echo "==> Windows payload 完成:" + @du -sh payload/windows + @echo "" + @echo "payload/windows 結構:" + @find payload/windows -maxdepth 2 -type d | sed 's|^| |' + +stage-windows: payload-windows ## 將 payload/windows/ 放到 Wails build/windows/Resources/ + @echo "==> 放置 payload 到 Wails build/windows/Resources..." + rm -rf visiona-agent/build/windows/Resources + mkdir -p visiona-agent/build/windows/Resources + cp -R payload/windows/. visiona-agent/build/windows/Resources/ + @echo "==> stage 完成:" + @du -sh visiona-agent/build/windows/Resources + +# ── M5:Linux vendor + payload + wails + AppImage ────────────────── +# 注意:wails-linux 與 appimage 必須在 Linux runner 上跑;在 macOS 上會明確 fail。 +# payload-linux / vendor-*-linux 是 curl 下載,跨平台可跑(server 步驟除外)。 + +PBS_URL_LINUX := https://github.com/astral-sh/python-build-standalone/releases/download/$(PBS_RELEASE)/cpython-$(PYTHON_VERSION)+$(PBS_RELEASE)-x86_64-unknown-linux-gnu-install_only.tar.gz +# LGPL v3 build(BtbN n7.1 穩定分支)— v2 TDD §4 +FFMPEG_URL_LINUX := https://github.com/BtbN/FFmpeg-Builds/releases/latest/download/ffmpeg-n7.1-latest-linux64-lgpl-7.1.tar.xz + +vendor-python-linux: ## 下載 python-build-standalone Linux x86_64 → vendor/python/linux/ + @mkdir -p vendor/python/linux + @if [ ! -f vendor/python/linux/python.tar.gz ]; then \ + echo "==> 下載 python-build-standalone $(PYTHON_VERSION)+$(PBS_RELEASE) (Linux x86_64 install_only)..."; \ + curl -fL -o vendor/python/linux/python.tar.gz "$(PBS_URL_LINUX)"; \ + echo "==> tarball 大小:$$(du -sh vendor/python/linux/python.tar.gz | cut -f1)"; \ + else \ + echo "==> python (Linux) tarball 已存在,跳過 ($$(du -sh vendor/python/linux/python.tar.gz | cut -f1))"; \ + fi + +vendor-wheels-linux: ## 同步 Linux wheels → vendor/wheels/linux/ + @mkdir -p vendor/wheels/linux + @echo "==> 同步內部 wheels (Linux, KneronPLUS 等)..." + @if [ -d visiona-agent/wheels/linux ]; then \ + cp visiona-agent/wheels/linux/*.whl vendor/wheels/linux/ 2>/dev/null || true; \ + fi + @echo "==> 從 PyPI 下載公開相依 wheels (cp312, manylinux2014_x86_64)..." + @pip3 download \ + --only-binary=:all: \ + --platform manylinux2014_x86_64 \ + --python-version 3.12 \ + --implementation cp \ + --dest vendor/wheels/linux \ + numpy opencv-python-headless pyusb requests || echo "WARN: pip download 部分失敗(詳見上方訊息)" + @echo "==> Linux wheels 總覽:" + @ls -1 vendor/wheels/linux/*.whl 2>/dev/null | wc -l | xargs -I{} echo " 共 {} 個 wheel" + @du -sh vendor/wheels/linux 2>/dev/null || true + +vendor-ffmpeg-linux: ## 下載 ffmpeg Linux LGPL v3 build (n7.1) → vendor/ffmpeg/linux/ + @mkdir -p vendor/ffmpeg/linux + @if [ -f vendor/ffmpeg/linux/ffmpeg ] && [ -f vendor/ffmpeg/linux/ffprobe ]; then \ + echo "==> ffmpeg + ffprobe (Linux) 已存在,跳過"; \ + else \ + echo "==> 下載 BtbN LGPL ffmpeg (Linux, n7.1)..."; \ + curl -fL -o /tmp/ffmpeg-linux.tar.xz "$(FFMPEG_URL_LINUX)"; \ + rm -rf /tmp/ffmpeg-linux-extract; \ + mkdir -p /tmp/ffmpeg-linux-extract; \ + tar xf /tmp/ffmpeg-linux.tar.xz -C /tmp/ffmpeg-linux-extract --strip-components=1; \ + cp /tmp/ffmpeg-linux-extract/bin/ffmpeg vendor/ffmpeg/linux/; \ + cp /tmp/ffmpeg-linux-extract/bin/ffprobe vendor/ffmpeg/linux/; \ + cp /tmp/ffmpeg-linux-extract/LICENSE.txt vendor/ffmpeg/linux/LICENSE.txt 2>/dev/null || true; \ + chmod +x vendor/ffmpeg/linux/ffmpeg vendor/ffmpeg/linux/ffprobe; \ + rm -rf /tmp/ffmpeg-linux* ; \ + echo "==> ffmpeg (Linux) 大小:$$(du -h vendor/ffmpeg/linux/ffmpeg | cut -f1)"; \ + echo "==> ffprobe (Linux) 大小:$$(du -h vendor/ffmpeg/linux/ffprobe | cut -f1)"; \ + fi + +payload-linux: build-server-linux vendor-python-linux vendor-wheels-linux vendor-ffmpeg-linux ## 準備 Linux payload → payload/linux/ + @echo "==> 建立 Linux payload (binary + models + scripts + python + wheels + ffmpeg)..." + mkdir -p payload/linux/bin payload/linux/data payload/linux/scripts payload/linux/python payload/linux/wheels + @if [ ! -f payload/linux/bin/visiona-agent-server ]; then \ + echo "!! ERROR: payload/linux/bin/visiona-agent-server 不存在,build-server-linux 可能失敗 !!"; \ + exit 1; \ + fi + @chmod +x payload/linux/bin/visiona-agent-server + @cp vendor/ffmpeg/linux/ffmpeg payload/linux/bin/ 2>/dev/null && chmod +x payload/linux/bin/ffmpeg || echo "!! WARN: ffmpeg 缺失" + @cp vendor/ffmpeg/linux/ffprobe payload/linux/bin/ 2>/dev/null && chmod +x payload/linux/bin/ffprobe || echo "!! WARN: ffprobe 缺失" + @cp vendor/ffmpeg/linux/LICENSE.txt payload/linux/bin/ffmpeg-LICENSE.txt 2>/dev/null || true + @if [ -d server/data ]; then cp -R server/data/. payload/linux/data/; fi + @if [ -d server/scripts ]; then cp -R server/scripts/. payload/linux/scripts/; fi + @if [ ! -f vendor/python/linux/python.tar.gz ]; then \ + echo "!! ERROR: vendor/python/linux/python.tar.gz 不存在,vendor-python-linux 應該已先跑過 !!"; \ + exit 1; \ + fi + @cp vendor/python/linux/python.tar.gz payload/linux/python/ + @cp vendor/wheels/linux/*.whl payload/linux/wheels/ 2>/dev/null || true + @wheel_count=$$(ls -1 payload/linux/wheels/*.whl 2>/dev/null | wc -l); \ + if [ "$$wheel_count" -lt 4 ]; then \ + echo "!! ERROR: payload/linux/wheels 只找到 $$wheel_count 個 wheel(預期至少 4 個:numpy / opencv / pyusb / kp)"; \ + echo " 請檢查 vendor/wheels/linux/ 並重跑 make vendor-wheels-linux"; \ + exit 1; \ + fi + @echo "==> Linux payload 完成:包含 $$(ls payload/linux/wheels/*.whl 2>/dev/null | wc -l) 個 wheel + python tarball" + @du -sh payload/linux 2>/dev/null || true + @echo "" + @echo "payload/linux 結構:" + @find payload/linux -maxdepth 2 -type d 2>/dev/null | sed 's|^| |' + +# ── Wails build ──────────────────────────────────────────────────── +# +# AF6:Wails 使用 visiona-agent/main.go 的 `//go:embed all:frontend` 嵌入前端產物。 +# 新的 Next.js 靜態匯出在 `frontend/out/`,需要在 wails build 前同步到 `visiona-agent/frontend/`, +# 否則打包後的 app 會仍嵌入舊的 local-tool 遺留控制台 JS。 +# +# 選擇「Makefile 同步」而非改 Go embed 路徑的理由: +# 1. `//go:embed` 不允許走出 module root(visiona-agent/);相對路徑 ../frontend/out 不合法 +# 2. 保持 Go 原始碼穩定,避免為了 build pipeline 去碰語意性的 embed 宣告 +# 3. 同步腳本單純(rm + cp),失敗時容易 debug + +wails-sync-frontend: build-frontend ## 同步 frontend/out/ → visiona-agent/frontend/(AF6:取代舊的 local-tool JS 控制台) + @echo "==> 同步 frontend/out → visiona-agent/frontend (Wails go:embed 嵌入來源) ..." + @# 保留 wailsjs/ 目錄(Wails CLI 產出的 TS bindings,不應被 frontend/out 覆蓋) + @find visiona-agent/frontend -mindepth 1 -maxdepth 1 ! -name 'wailsjs' -exec rm -rf {} + + cp -R frontend/out/. visiona-agent/frontend/ + @du -sh visiona-agent/frontend + +wails-macos: stage-macos wails-sync-frontend ## wails build darwin/amd64 → visiona-agent/build/bin/visiona-agent.app + @# -s:跳過 Wails 內建 frontend install/build(我們已在 wails-sync-frontend 處理好, + @# visiona-agent/frontend 只放靜態產物,沒有 package.json,Wails 若自己跑 pnpm 會失敗) + cd visiona-agent && wails build -platform darwin/amd64 -clean -s + cp -R visiona-agent/build/darwin/Resources/bin \ + visiona-agent/build/darwin/Resources/data \ + visiona-agent/build/darwin/Resources/scripts \ + visiona-agent/build/darwin/Resources/python \ + visiona-agent/build/darwin/Resources/wheels \ + visiona-agent/build/bin/visiona-agent.app/Contents/Resources/ + codesign --force --deep --sign - visiona-agent/build/bin/visiona-agent.app + codesign --verify --verbose visiona-agent/build/bin/visiona-agent.app + @du -sh visiona-agent/build/bin/visiona-agent.app + +wails-windows: stage-windows wails-sync-frontend ## ⚠️ 必須在 Windows runner 上執行:wails build -platform windows/amd64 + @case "$$(uname -s 2>/dev/null)" in \ + MINGW*|CYGWIN*|MSYS*) : ;; \ + *) \ + echo ""; \ + echo "❌ wails-windows 只能在 Windows runner 上 build(偵測到 $$(uname -s))"; \ + echo " 請在 Windows 機器上執行此 target,或用 CI 的 Windows runner"; \ + echo " 若只是要檢查前置步驟,可單獨跑 make stage-windows"; \ + echo ""; \ + exit 1 ;; \ + esac + @# -s:跳過 Wails 內建 frontend install/build(見 wails-macos 的說明) + cd visiona-agent && wails build -platform windows/amd64 -clean -s + @du -sh visiona-agent/build/bin/visiona-agent.exe + +wails-linux: payload-linux wails-sync-frontend ## ⚠️ 必須在 Linux runner 上執行:wails build -platform linux/amd64 + @if [ "$$(uname -s)" != "Linux" ]; then \ + echo ""; \ + echo "❌ wails-linux 只能在 Linux 上 build(偵測到 $$(uname -s))"; \ + echo " 請在 Linux x86_64 runner(GitHub Actions ubuntu-latest 即可)執行"; \ + echo " 若只是要檢查前置步驟,可單獨跑 make payload-linux"; \ + echo ""; \ + exit 1; \ + fi + # webkit2gtk-4.0 從 Ubuntu 22.10+ / Debian 12+ 起被 webkit2gtk-4.1 取代。 + # 偵測 pkg-config 哪個存在:優先用 4.1(加 -tags webkit2_41),4.0 則 + # 不加 tag(Wails 預設)。 + @# -s:跳過 Wails 內建 frontend install/build(見 wails-macos 的說明) + @if pkg-config --exists webkit2gtk-4.1 2>/dev/null; then \ + echo "==> webkit2gtk-4.1 detected → wails build with -tags webkit2_41"; \ + cd visiona-agent && wails build -platform linux/amd64 -clean -s -tags webkit2_41; \ + elif pkg-config --exists webkit2gtk-4.0 2>/dev/null; then \ + echo "==> webkit2gtk-4.0 detected → wails build (default)"; \ + cd visiona-agent && wails build -platform linux/amd64 -clean -s; \ + else \ + echo ""; \ + echo "❌ 找不到 webkit2gtk-4.0 或 webkit2gtk-4.1 dev header"; \ + echo " 請執行:sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev"; \ + echo " (舊版 Ubuntu 20.04 可改用 libwebkit2gtk-4.0-dev)"; \ + echo ""; \ + exit 1; \ + fi + @du -sh visiona-agent/build/bin/visiona-agent + +# ── 安裝檔打包 ───────────────────────────────────────────────────── +dmg: wails-macos ## 美化 DMG(create-dmg 有裝)或 plain DMG(fallback)→ dist/visiona-agent.dmg + @mkdir -p $(DIST) + @rm -f $(DIST)/visiona-agent.dmg + @if command -v create-dmg > /dev/null 2>&1; then \ + $(MAKE) --no-print-directory dmg-fancy; \ + else \ + echo "⚠️ create-dmg 未安裝,使用 plain DMG(hdiutil UDZO)"; \ + echo " 想要美化版本請執行:brew install create-dmg"; \ + $(MAKE) --no-print-directory dmg-plain; \ + fi + @du -sh $(DIST)/visiona-agent.dmg + @file $(DIST)/visiona-agent.dmg + +dmg-plain: ## hdiutil UDZO → dist/visiona-agent.dmg(無背景圖,CI / fallback 用) + @mkdir -p $(DIST) + rm -f $(DIST)/visiona-agent.dmg + hdiutil create -volname "visionA Agent" \ + -srcfolder visiona-agent/build/bin/visiona-agent.app \ + -ov -format UDZO \ + $(DIST)/visiona-agent.dmg + +dmg-fancy: ## create-dmg 美化版 → dist/visiona-agent.dmg(需 brew install create-dmg) + @if [ ! -d visiona-agent/build/bin/visiona-agent.app ]; then \ + echo "❌ visiona-agent/build/bin/visiona-agent.app 不存在,請先跑 make wails-macos"; exit 1; \ + fi + @if ! command -v create-dmg > /dev/null 2>&1; then \ + echo "❌ create-dmg 未安裝,請執行:brew install create-dmg"; exit 1; \ + fi + @mkdir -p $(DIST) + rm -f $(DIST)/visiona-agent.dmg + create-dmg \ + --volname "visionA Agent" \ + --background installer/macos/background.png \ + --window-pos 200 120 \ + --window-size 640 400 \ + --icon-size 128 \ + --icon "visiona-agent.app" 180 200 \ + --app-drop-link 460 200 \ + --hide-extension "visiona-agent.app" \ + --no-internet-enable \ + $(DIST)/visiona-agent.dmg \ + visiona-agent/build/bin/visiona-agent.app + +exe-only: ## 只跑 iscc 打包 installer(前置產物必須已存在),不重 build wails/payload + @if [ ! -f visiona-agent/build/bin/visiona-agent.exe ]; then \ + echo "❌ visiona-agent/build/bin/visiona-agent.exe 不存在,請先跑 make wails-windows"; exit 1; \ + fi + @if [ ! -f payload/windows/bin/visiona-agent-server.exe ]; then \ + echo "❌ payload/windows/bin/visiona-agent-server.exe 不存在,請先跑 make payload-windows"; exit 1; \ + fi + @$(MAKE) --no-print-directory _run-iscc + +exe: wails-windows _run-iscc ## ⚠️ 必須在 Windows 上跑:Inno Setup → dist/visiona-agent-*-windows-x64.exe + +_run-iscc: + @echo "DEBUG _run-iscc: ISCC='$$ISCC'" + @echo "DEBUG _run-iscc: PATH first entries: $$(echo $$PATH | tr ':' '\n' | head -5)" + @ISCC_BIN="$$ISCC"; \ + if [ -z "$$ISCC_BIN" ]; then \ + if command -v iscc > /dev/null 2>&1; then ISCC_BIN=iscc; \ + elif command -v iscc.exe > /dev/null 2>&1; then ISCC_BIN=iscc.exe; \ + else \ + for p in "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" \ + "/c/Program Files/Inno Setup 6/ISCC.exe" \ + "$$LOCALAPPDATA/Programs/Inno Setup 6/ISCC.exe" \ + "$$USERPROFILE/AppData/Local/Programs/Inno Setup 6/ISCC.exe" \ + "/c/Program Files (x86)/Inno Setup 5/ISCC.exe"; do \ + if [ -f "$$p" ]; then ISCC_BIN="$$p"; break; fi; \ + done; \ + fi; \ + fi; \ + ISCC_OK=0; \ + if [ -n "$$ISCC_BIN" ]; then \ + if [ -e "$$ISCC_BIN" ] || command -v "$$ISCC_BIN" > /dev/null 2>&1; then ISCC_OK=1; fi; \ + fi; \ + if [ "$$ISCC_OK" = "0" ]; then \ + echo ""; \ + echo "❌ Inno Setup Compiler (iscc) 未安裝 / 找不到"; \ + echo " 已嘗試偵測的路徑:"; \ + echo " - \$$ISCC 環境變數"; \ + echo " - PATH 上的 iscc / iscc.exe"; \ + echo " - /c/Program Files (x86)/Inno Setup 6/ISCC.exe"; \ + echo " - /c/Program Files/Inno Setup 6/ISCC.exe"; \ + echo " 請從 https://jrsoftware.org/isdl.php 下載並安裝 Inno Setup 6"; \ + echo " 或設定 ISCC 環境變數指向 ISCC.exe 絕對路徑"; \ + echo ""; \ + exit 1; \ + fi; \ + echo "==> 使用 ISCC: $$ISCC_BIN"; \ + mkdir -p $(DIST); \ + echo "==> 執行 iscc(cwd: $$(pwd))..."; \ + "$$ISCC_BIN" installer/windows/visiona-agent.iss; \ + ISCC_RC=$$?; \ + echo "==> iscc exit code: $$ISCC_RC"; \ + if [ $$ISCC_RC -ne 0 ]; then exit $$ISCC_RC; fi + @echo "==> 產出:" + @ls -lh $(DIST)/ 2>/dev/null || echo "dist 目錄不存在" + @ls -lh $(DIST)/visiona-agent-*-windows-x64.exe 2>/dev/null || echo "未找到 .exe 產出檔" + +appimage: wails-linux ## ⚠️ 必須在 Linux 上跑:build-appimage.sh → dist/visiona-agent-*-linux-x64.AppImage + @if [ "$$(uname -s)" != "Linux" ]; then \ + echo ""; \ + echo "❌ appimage 只能在 Linux 上 build(偵測到 $$(uname -s))"; \ + echo " 需要 appimagetool,請在 Linux x86_64 runner 執行"; \ + echo ""; \ + exit 1; \ + fi + @mkdir -p $(DIST) + VERSION=$(VERSION) bash installer/linux/build-appimage.sh + @echo "==> 產出:" + @ls -lh $(DIST)/visiona-agent-*-linux-x64.AppImage 2>/dev/null || echo "未找到產出檔" + +# ── 開發 ─────────────────────────────────────────────────────────── +dev-agent: ## AB6:build server binary + 以 go run 啟動 visiona-agent(skip Wails GUI,方便 CLI 迭代) + @echo "==> building server binary → $(DIST)/visiona-agent-server ..." + cd server && go build -o ../$(DIST)/visiona-agent-server . + @echo "==> starting visiona-agent (ctrl-c 停止)..." + @echo " tunnel 會等 VISIONA_RELAY_URL 設定後才啟動;" + @echo " 設 VISIONA_SESSION_TOKEN 自動跳過 Pair。" + cd visiona-agent && go run . + +dev: dev-agent ## alias for dev-agent + +test: ## go test -race(full agent module + server module) + cd visiona-agent && go test -race -count=1 ./... + cd server && go test -race -count=1 ./... + +test-tunnel: ## 只跑 tunnel 整合測試(AB6) + cd visiona-agent && go test -race -count=1 -v ./internal/tunnel/ + +lint: ## go vet + (optional) pnpm lint + cd visiona-agent && go vet ./... + cd server && go vet ./... + @echo "TODO: pnpm lint" + +fmt: ## go fmt + cd visiona-agent && go fmt ./... + cd server && go fmt ./... + +# ── 清理 ─────────────────────────────────────────────────────────── +clean: ## 清除 dist/ 與 payload/ 產物 + @echo "Cleaning dist/ and payload artifacts..." + rm -rf $(DIST) + rm -rf $(PAYLOAD) + @mkdir -p $(DIST) $(PAYLOAD) + @touch $(DIST)/.gitkeep $(PAYLOAD)/.gitkeep + @echo "Done." + +clean-all: clean ## 完整清除:dist/ payload/ wails build/ frontend build/ server embed + @echo "==> 清除 Wails build 產物..." + rm -rf visiona-agent/build/bin + rm -rf visiona-agent/build/darwin/Resources + rm -rf visiona-agent/build/windows/Resources + @echo "==> 清除 frontend build 產物..." + rm -rf frontend/out + rm -rf frontend/.next + @echo "==> 清除 server 內嵌前端..." + rm -rf server/web/out + @echo "==> clean-all 完成" + +clean-build-exe: clean-all exe ## Windows:完整 clean + 從頭 build installer .exe(最乾淨的 build) +clean-build-dmg: clean-all dmg ## macOS:完整 clean + 從頭 build installer .dmg +clean-build-appimage: clean-all appimage ## Linux:完整 clean + 從頭 build installer .AppImage diff --git a/local-agent/README.md b/local-agent/README.md new file mode 100644 index 0000000..d1ee959 --- /dev/null +++ b/local-agent/README.md @@ -0,0 +1,104 @@ +# visionA Agent + +> **Phase 0 雛形 — 正在建置中** +> +> 此專案於 2026-04-22 從 `local-tool/` fork,baseline commit:`b71ff4cd3c72e879435f773ae15b23bf8b70841e` +> +> **fork 後獨立演進,不主動與 local-tool 同步。** 需要時手動 cherry-pick bug fix。 + +## 產品定位 + +visionA Agent 是 visionA 雲端版的 local 端代理程式: + +**它是什麼** +- 一個完整的 local server(沿用 local-tool 的 KneronPLUS / camera / inference / device / model / Python runtime / ffmpeg 邏輯) +- 加上 **tunnel client**,把自己 reverse-tunnel 到雲端 `visionA-backend`,讓雲端 web UI 可以透過 tunnel 操作使用者桌機上的 Kneron 裝置 +- 加上 **3 頁極簡配置 UI**(狀態 / 配對 / 設定),給使用者看「Agent 是否在線」和「配對雲端帳號」用 + +**它不是什麼** +- 不是 local-tool 的修改版(local-tool 不動;visionA Agent fork 後獨立演進) +- 不保留 local-tool 原本的裝置 / 模型 / 推論 UI(這些由雲端 web UI 負責;本機 UI 只管配對/設定) + +## 目前進度(Phase 0 雛形) + +| 任務 | 狀態 | 說明 | +|------|------|------| +| AB1 | ✅ | fork + 改名(visiona-local → visiona-agent / bundle ID / Makefile 變數 78 處替換) | +| AB2+AB3 | ✅ | 裁剪 Wails 業務 UI binding、刪除舊資料目錄遷移;新增 pairing / connection / settings 的 stub binding(回 ErrNotImplemented),為 AB4-AB10 預留擴點 | +| AB4 | ⏳ | 複製 tunnel client(從 POC)到 `internal/tunnel/` | +| AB5 | ⏳ | Agent config (YAML) 讀寫 | +| AB6 | ⏳ | Internal HTTP server(wrapper 綁 127.0.0.1:0) | +| AB7 | ⏳ | Pairing Exchanger(呼叫雲端 `/api/pairing/exchange`) | +| AB8 | ⏳ | connstate broadcaster + Wails event 推送 | +| AB9 | ⏳ | autostart 三平台實作 | +| AB10 | ⏳ | logexport(壓縮最近 7 天 log) | +| AB11-12 | ⏳ | Wails 整合 + 整合測試 | + +## 架構圖(簡版) + +``` +[ Browser ] ── HTTPS ──► [ visionA-backend ] + │ (內部 HTTP forward) + ▼ + [ remote-proxy ] + │ (WSS + yamux 出站長連線) + ▼ +┌───────────────────────────────────────────────────────┐ +│ visionA Agent(使用者桌機,Wails app) │ +│ │ +│ [Tunnel client] [3 頁配置 UI] │ +│ │ │ Wails bindings │ +│ ▼ ▼ │ +│ [Internal HTTP server 127.0.0.1:] │ +│ (沿用 local-tool 的 Gin handler:裝置/模型/推論/...) │ +│ │ │ +│ ▼ │ +│ [Python runtime + KneronPLUS SDK + ffmpeg] │ +└───────────────────────────────────────────────────────┘ + │ + ▼ + Kneron USB 裝置 +``` + +## 文件 + +- 架構 TDD:[`.autoflow/04-architecture/visiona-agent-tdd.md`](./.autoflow/04-architecture/visiona-agent-tdd.md) +- UI 規格:[`.autoflow/03-design/visiona-agent-spec.md`](./.autoflow/03-design/visiona-agent-spec.md) +- ADR:[`.autoflow/04-architecture/adr/adr-007-*.md`](./.autoflow/04-architecture/adr/)(架構)、`adr-008-*.md`(tunnel 複用)、`adr-009-*.md`(token 儲存) +- 開發進度:[`.autoflow/progress.md`](./.autoflow/progress.md) +- Build / 共存驗證:[`docs/BUILD-VERIFICATION.md`](./docs/BUILD-VERIFICATION.md) + +## fork baseline + +本專案於 2026-04-22 從 `local-tool/`(commit `b71ff4cd3c72e879435f773ae15b23bf8b70841e`)一次性 fork 後獨立演進,**不主動 sync**(ADR-007)。 + +local-tool 原始 README 已封存於 [`docs/legacy-local-tool-readme.md`](./docs/legacy-local-tool-readme.md),僅作 cherry-pick bug fix 時的歷史對照,不再代表 visionA Agent 的產品定位。 + +## 開發指令 + +```bash +# 下載第三方依賴到 vendor/ +make vendor-sync + +# 本機 build + 產出 macOS DMG +make dmg + +# 列出所有 targets +make help +``` + +主要 targets(沿用 local-tool 結構,但所有產物名稱已替換為 `visiona-agent`): + +| Target | 作用 | +|--------|------| +| `vendor-sync` | 下載 python-build-standalone / wheels / ffmpeg | +| `build-frontend` | pnpm build Next.js 靜態產物 → `frontend/out/` | +| `wails-sync-frontend` | 同步 `frontend/out/` → `visiona-agent/frontend/`(Wails embed) | +| `wails-macos` / `wails-windows` / `wails-linux` | Wails build + ad-hoc codesign(macOS) | +| `dmg` | 產出 `dist/visiona-agent.dmg`(缺 create-dmg 時 fallback hdiutil) | +| `exe` | Inno Setup → `dist/visiona-agent-*-windows-x64.exe`(需 Windows runner) | +| `appimage` | `dist/visiona-agent-*-x86_64.AppImage`(需 Linux runner) | +| `dev-agent` | Wails dev mode | +| `test-tunnel` | tunnel client 整合測試 | + +三平台 build 細節與 local-tool 共存對照表:[`docs/BUILD-VERIFICATION.md`](./docs/BUILD-VERIFICATION.md)。 diff --git a/local-agent/branding/README.md b/local-agent/branding/README.md new file mode 100644 index 0000000..f8b2ebc --- /dev/null +++ b/local-agent/branding/README.md @@ -0,0 +1,61 @@ +# visionA Agent — Branding Assets + +此目錄存放 visionA Agent 的品牌視覺資產。 + +## 檔案 + +| 檔案 | 用途 | +|------|------| +| `logo.svg` | 向量原始設計稿(1024×1024) | +| `icon-1024.png` | Wails build 的 appicon(會被 Wails 複製到 `visiona-agent/build/appicon.png`) | +| `icon-512.png` / `icon-256.png` / `icon-128.png` | 各尺寸 PNG 備用 | +| `icon.ico` | Windows 多解析度 ICO(16/24/32/48/64/96/128/256,PNG-in-ICO 格式) | +| `icon.icns` | macOS .app bundle icon | + +## 如何更新 logo + +1. 改 `logo.svg` 或改 `gen_icon.go` 的繪圖函數 +2. 跑 `go run gen_icon.go ` 產出各尺寸 PNG +3. 跑 `go run gen_ico.go icon.ico 16,24,32,48,64,96,128,256` 產 Windows ICO +4. macOS icns 用 `iconutil`: + ```bash + mkdir icon.iconset + cp icon-16.png icon.iconset/icon_16x16.png + cp icon-32.png icon.iconset/icon_16x16@2x.png + cp icon-32.png icon.iconset/icon_32x32.png + cp icon-64.png icon.iconset/icon_32x32@2x.png + cp icon-128.png icon.iconset/icon_128x128.png + cp icon-256.png icon.iconset/icon_128x128@2x.png + cp icon-256.png icon.iconset/icon_256x256.png + cp icon-512.png icon.iconset/icon_256x256@2x.png + cp icon-512.png icon.iconset/icon_512x512.png + cp icon-1024.png icon.iconset/icon_512x512@2x.png + iconutil -c icns icon.iconset -o icon.icns + ``` +5. 部署: + - `cp icon-1024.png ../visiona-agent/build/appicon.png` — Wails build + - `cp icon-256.png ../visiona-agent/frontend/icon.png` — splash page + - `cp icon.ico ../frontend/src/app/favicon.ico` — Next.js favicon + - `cp icon-256.png ../frontend/src/app/icon.png` — Next.js App Router icon +6. 重 build:`make clean-all && make exe`(Windows)或 `make clean-all && make dmg`(macOS) + +## 設計理念 + +- 圓角方形背景(符合現代 app icon 容器標準) +- 深藍漸層底(`#1A1F36` → `#0E1222`)傳達專業、科技感 +- 雙層同心圓環 = 相機鏡頭 / 視覺感測器隱喻 +- 中央「V」字形 = vision 首字母 +- 三個 pixel 點 + 右上 active indicator = Edge AI / pixel-level 運算的視覺語彙 +- 主色 `#4F7EFF`(電子藍)搭配 `#6EF3C5`(mint)點綴,避免純藍的冰冷 + +## 色票 + +| 用途 | HEX | +|------|-----| +| 主色 | `#4F7EFF` | +| 主色亮色 | `#6EA8FF` | +| 點綴 | `#6EF3C5` | +| 深色背景頂 | `#1A1F36` | +| 深色背景底 | `#0E1222` | +| 警示 | `#FF6B6B` | +| 中性灰 | `#8890B0` | diff --git a/local-agent/branding/icon-1024.png b/local-agent/branding/icon-1024.png new file mode 100644 index 0000000..98c6d04 Binary files /dev/null and b/local-agent/branding/icon-1024.png differ diff --git a/local-agent/branding/icon-128.png b/local-agent/branding/icon-128.png new file mode 100644 index 0000000..0f9be24 Binary files /dev/null and b/local-agent/branding/icon-128.png differ diff --git a/local-agent/branding/icon-256.png b/local-agent/branding/icon-256.png new file mode 100644 index 0000000..09717b5 Binary files /dev/null and b/local-agent/branding/icon-256.png differ diff --git a/local-agent/branding/icon-512.png b/local-agent/branding/icon-512.png new file mode 100644 index 0000000..9c51f5a Binary files /dev/null and b/local-agent/branding/icon-512.png differ diff --git a/local-agent/branding/icon.icns b/local-agent/branding/icon.icns new file mode 100644 index 0000000..0e00b8b Binary files /dev/null and b/local-agent/branding/icon.icns differ diff --git a/local-agent/branding/icon.ico b/local-agent/branding/icon.ico new file mode 100644 index 0000000..403176c Binary files /dev/null and b/local-agent/branding/icon.ico differ diff --git a/local-agent/branding/logo.svg b/local-agent/branding/logo.svg new file mode 100644 index 0000000..b7462ea --- /dev/null +++ b/local-agent/branding/logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local-agent/branding/tools/gen_ico.go b/local-agent/branding/tools/gen_ico.go new file mode 100644 index 0000000..bd90883 --- /dev/null +++ b/local-agent/branding/tools/gen_ico.go @@ -0,0 +1,95 @@ +// 把多張 PNG 打包成 Windows .ico(PNG-in-ICO 格式,Vista+ 支援) +// 用法:go run gen_ico.go +// 範例:go run gen_ico.go icon.ico ../ 16,24,32,48,64,96,128,256 +//go:build ignore + +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +type iconDir struct { + Reserved uint16 + Type uint16 + Count uint16 +} + +type iconDirEntry struct { + Width uint8 + Height uint8 + ColorCount uint8 + Reserved uint8 + Planes uint16 + BitCount uint16 + SizeBytes uint32 + Offset uint32 +} + +func main() { + if len(os.Args) < 4 { + fmt.Fprintln(os.Stderr, "usage: gen_ico ") + os.Exit(1) + } + outPath := os.Args[1] + pngDir := os.Args[2] + sizesCSV := os.Args[3] + + var sizes []int + for _, s := range strings.Split(sizesCSV, ",") { + n, err := strconv.Atoi(strings.TrimSpace(s)) + if err != nil { + fmt.Fprintln(os.Stderr, "bad size:", s) + os.Exit(1) + } + sizes = append(sizes, n) + } + + pngs := make([][]byte, len(sizes)) + for i, s := range sizes { + data, err := os.ReadFile(filepath.Join(pngDir, "icon-"+strconv.Itoa(s)+".png")) + if err != nil { + fmt.Fprintln(os.Stderr, "read", s, err) + os.Exit(1) + } + pngs[i] = data + } + + var buf bytes.Buffer + binary.Write(&buf, binary.LittleEndian, iconDir{Reserved: 0, Type: 1, Count: uint16(len(sizes))}) + headerSize := 6 + 16*len(sizes) + offset := uint32(headerSize) + for i, s := range sizes { + w := uint8(s) + h := uint8(s) + if s >= 256 { + w = 0 + h = 0 + } + entry := iconDirEntry{ + Width: w, + Height: h, + Planes: 1, + BitCount: 32, + SizeBytes: uint32(len(pngs[i])), + Offset: offset, + } + binary.Write(&buf, binary.LittleEndian, entry) + offset += uint32(len(pngs[i])) + } + for _, p := range pngs { + buf.Write(p) + } + + if err := os.WriteFile(outPath, buf.Bytes(), 0644); err != nil { + fmt.Fprintln(os.Stderr, "write", err) + os.Exit(1) + } + fmt.Println("wrote", outPath, "containing", len(sizes), "images") +} diff --git a/local-agent/branding/tools/gen_icon.go b/local-agent/branding/tools/gen_icon.go new file mode 100644 index 0000000..acca960 --- /dev/null +++ b/local-agent/branding/tools/gen_icon.go @@ -0,0 +1,289 @@ +// 一次性 icon 生成工具 — 產 visionA Agent logo 的多種解析度 PNG +// 用法:go run gen_icon.go +// +// 此檔案為 standalone 工具,不屬於 visiona-agent app。 +// 用 build tag 避免被一般 go build 抓到(只有明確 go run 才執行)。 +//go:build ignore + +package main + +import ( + "fmt" + "image" + "image/color" + "image/png" + "math" + "os" + "path/filepath" + "strconv" +) + +var ( + bgTop = color.RGBA{0x1A, 0x1F, 0x36, 0xFF} + bgBottom = color.RGBA{0x0E, 0x12, 0x22, 0xFF} + lensTop = color.RGBA{0x6E, 0xA8, 0xFF, 0xFF} + lensBot = color.RGBA{0x4F, 0x7E, 0xFF, 0xFF} + lensMid = color.RGBA{0x4F, 0x7E, 0xFF, 0x99} + centerGlow = color.RGBA{0x6E, 0xF3, 0xC5, 0xE0} + white = color.RGBA{0xFF, 0xFF, 0xFF, 0xFF} + mint = color.RGBA{0x6E, 0xF3, 0xC5, 0xFF} + lightBlue = color.RGBA{0x6E, 0xA8, 0xFF, 0xFF} +) + +func lerpColor(a, b color.RGBA, t float64) color.RGBA { + if t < 0 { + t = 0 + } + if t > 1 { + t = 1 + } + return color.RGBA{ + R: uint8(float64(a.R)*(1-t) + float64(b.R)*t), + G: uint8(float64(a.G)*(1-t) + float64(b.G)*t), + B: uint8(float64(a.B)*(1-t) + float64(b.B)*t), + A: uint8(float64(a.A)*(1-t) + float64(b.A)*t), + } +} + +func blendAlpha(dst, src color.RGBA) color.RGBA { + if src.A == 0 { + return dst + } + if src.A == 0xFF { + return src + } + sa := float64(src.A) / 255.0 + da := float64(dst.A) / 255.0 + out := 1.0 - (1.0-sa)*(1.0-da) + if out == 0 { + return dst + } + return color.RGBA{ + R: uint8((float64(src.R)*sa + float64(dst.R)*da*(1-sa)) / out), + G: uint8((float64(src.G)*sa + float64(dst.G)*da*(1-sa)) / out), + B: uint8((float64(src.B)*sa + float64(dst.B)*da*(1-sa)) / out), + A: uint8(out * 255), + } +} + +func rgba(c color.Color) color.RGBA { + if r, ok := c.(color.RGBA); ok { + return r + } + r, g, b, a := c.RGBA() + return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)} +} + +func drawRoundedRect(img *image.RGBA, size int, radius float64, colorAt func(y int) color.RGBA) { + cx, cy := float64(size)/2, float64(size)/2 + for y := 0; y < size; y++ { + c := colorAt(y) + for x := 0; x < size; x++ { + dx := math.Max(0, math.Abs(float64(x)-cx+0.5)-(float64(size)/2-radius)) + dy := math.Max(0, math.Abs(float64(y)-cy+0.5)-(float64(size)/2-radius)) + d := math.Hypot(dx, dy) + if d <= radius { + a := 1.0 + if d > radius-1 { + a = radius - d + } + if a < 0 { + continue + } + if a > 1 { + a = 1 + } + cc := c + cc.A = uint8(float64(cc.A) * a) + img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc)) + } + } + } +} + +func drawCircleRing(img *image.RGBA, cx, cy, rOuter, rInner float64, c color.RGBA) { + size := img.Bounds().Dx() + for y := int(cy-rOuter-1) - 1; y <= int(cy+rOuter+1)+1; y++ { + for x := int(cx-rOuter-1) - 1; x <= int(cx+rOuter+1)+1; x++ { + if x < 0 || y < 0 || x >= size || y >= size { + continue + } + d := math.Hypot(float64(x)-cx+0.5, float64(y)-cy+0.5) + if d <= rOuter && d >= rInner { + a := 1.0 + if d > rOuter-1 { + a = rOuter - d + } else if d < rInner+1 { + a = d - rInner + } + if a < 0 { + continue + } + if a > 1 { + a = 1 + } + cc := c + cc.A = uint8(float64(cc.A) * a) + img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc)) + } + } + } +} + +func drawFilledCircle(img *image.RGBA, cx, cy, r float64, c color.RGBA) { + size := img.Bounds().Dx() + for y := int(cy-r-1) - 1; y <= int(cy+r+1)+1; y++ { + for x := int(cx-r-1) - 1; x <= int(cx+r+1)+1; x++ { + if x < 0 || y < 0 || x >= size || y >= size { + continue + } + d := math.Hypot(float64(x)-cx+0.5, float64(y)-cy+0.5) + if d <= r { + a := 1.0 + if d > r-1 { + a = r - d + } + if a < 0 { + continue + } + cc := c + cc.A = uint8(float64(cc.A) * a) + img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc)) + } + } + } +} + +func drawRadialGlow(img *image.RGBA, cx, cy, r float64, c color.RGBA) { + size := img.Bounds().Dx() + for y := int(cy-r-1) - 1; y <= int(cy+r+1)+1; y++ { + for x := int(cx-r-1) - 1; x <= int(cx+r+1)+1; x++ { + if x < 0 || y < 0 || x >= size || y >= size { + continue + } + d := math.Hypot(float64(x)-cx+0.5, float64(y)-cy+0.5) + if d <= r { + t := 1.0 - d/r + t = t * t + cc := c + cc.A = uint8(float64(cc.A) * t) + img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc)) + } + } + } +} + +func drawLine(img *image.RGBA, x1, y1, x2, y2, width float64, c color.RGBA) { + size := img.Bounds().Dx() + r := width / 2 + minX := int(math.Min(x1, x2) - r - 1) + maxX := int(math.Max(x1, x2) + r + 1) + minY := int(math.Min(y1, y2) - r - 1) + maxY := int(math.Max(y1, y2) + r + 1) + dx := x2 - x1 + dy := y2 - y1 + lenSq := dx*dx + dy*dy + for y := minY; y <= maxY; y++ { + for x := minX; x <= maxX; x++ { + if x < 0 || y < 0 || x >= size || y >= size { + continue + } + px := float64(x) + 0.5 + py := float64(y) + 0.5 + var t float64 + if lenSq > 0 { + t = ((px-x1)*dx + (py-y1)*dy) / lenSq + if t < 0 { + t = 0 + } + if t > 1 { + t = 1 + } + } + cx := x1 + t*dx + cy := y1 + t*dy + d := math.Hypot(px-cx, py-cy) + if d <= r { + a := 1.0 + if d > r-1 { + a = r - d + } + if a < 0 { + continue + } + cc := c + cc.A = uint8(float64(cc.A) * a) + img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc)) + } + } + } +} + +func renderLogo(size int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, size, size)) + s := float64(size) + k := s / 1024.0 + + radius := 220.0 * k + drawRoundedRect(img, size, radius, func(y int) color.RGBA { + t := float64(y) / s + return lerpColor(bgTop, bgBottom, t) + }) + + cx := 512.0 * k + cy := 512.0 * k + outerR := 360.0 * k + outerW := 36.0 * k + drawCircleRing(img, cx, cy, outerR, outerR-outerW, lensTop) + drawCircleRing(img, cx, cy, outerR, outerR-outerW/2, lerpColor(lensTop, lensBot, 0.6)) + + midR := 296.0 * k + midW := 14.0 * k + drawCircleRing(img, cx, cy, midR, midR-midW, lensMid) + + glowR := 240.0 * k + drawRadialGlow(img, cx, cy, glowR, centerGlow) + + lineW := 44.0 * k + drawLine(img, 360*k, 360*k, 512*k, 640*k, lineW, white) + drawLine(img, 664*k, 360*k, 512*k, 640*k, lineW, white) + + drawFilledCircle(img, 360*k, 360*k, 22*k, mint) + drawFilledCircle(img, 664*k, 360*k, 22*k, lightBlue) + drawFilledCircle(img, 512*k, 640*k, 26*k, white) + + drawFilledCircle(img, 760*k, 264*k, 14*k, mint) + drawCircleRing(img, 760*k, 264*k, 24*k, 21*k, color.RGBA{0x6E, 0xF3, 0xC5, 0x66}) + + return img +} + +func savePNG(img *image.RGBA, path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + return png.Encode(f, img) +} + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: go run gen_icon.go ") + os.Exit(1) + } + outDir := os.Args[1] + sizes := []int{16, 24, 32, 48, 64, 96, 128, 256, 512, 1024} + for _, s := range sizes { + img := renderLogo(s) + path := filepath.Join(outDir, "icon-"+strconv.Itoa(s)+".png") + if err := savePNG(img, path); err != nil { + fmt.Fprintln(os.Stderr, "save", path, err) + os.Exit(1) + } + fmt.Println("wrote", path) + } +} diff --git a/local-agent/docs/BUILD-VERIFICATION.md b/local-agent/docs/BUILD-VERIFICATION.md new file mode 100644 index 0000000..bbe5392 --- /dev/null +++ b/local-agent/docs/BUILD-VERIFICATION.md @@ -0,0 +1,148 @@ +# visionA Agent — Build Pipeline 驗證報告 + +> AB12 產出。驗證三平台 build pipeline 設定是否正確,以及 macOS 實際 build 結果。 +> 最後驗證日期:2026-04-22(jimchen 的 macOS 13 開發機) + +## 三平台預期輸出 + +| 平台 | 產出路徑 | 格式 | 參考大小 | 備註 | +|------|---------|------|---------|------| +| macOS | `dist/visiona-agent.dmg` | zlib-compressed UDZO DMG | ~160 MB | x86_64 Intel,LSMinimumSystemVersion 10.13 | +| Windows | `dist/visiona-agent--windows-x64.exe` | Inno Setup self-extracting EXE | ~180 MB(估) | MinVersion 10.0.17763(Win10 1809+) | +| Linux | `dist/visiona-agent--linux-x64.AppImage` | AppImage(FUSE 可執行) | ~180 MB(估) | glibc 2.35+(Ubuntu 22.04 build) | + +## 環境需求 + +### 共通 +- Go 1.26+ +- Node 22.x + pnpm 9(或 10) +- Wails CLI v2.12+(`go install github.com/wailsapp/wails/v2/cmd/wails@latest`) +- curl + +### macOS 專屬 +- Xcode Command Line Tools(clang、codesign) +- `brew install create-dmg`(美化 DMG;未裝則 fallback 到 plain `hdiutil` DMG) +- (只有升級 ffmpeg 時)`brew install pkg-config nasm` + +### Windows 專屬(只在 Windows runner 上跑) +- Inno Setup 6+(`choco install innosetup`) +- Git Bash / MSYS(用 bash 跑 Makefile) +- 真實 Python(非 WindowsApps stub)— 給 `pip download` 公開 wheels 用 + +### Linux 專屬(只在 Linux runner 上跑) +- `libgtk-3-dev libwebkit2gtk-4.1-dev libusb-1.0-0-dev` +- `fuse libfuse2`(AppImage 執行需要) +- `desktop-file-utils` +- `appimagetool`(從 AppImage releases 下載) + +## 已驗證項目 + +| 項目 | 狀態 | 驗證方式 | +|------|------|---------| +| macOS DMG 本機 build | ✅ 通過 | 本地 `make dmg` 跑過(160 MB zlib UDZO,hdiutil attach OK) | +| macOS Info.plist Bundle ID | ✅ 通過 | `com.innovedus.visiona-agent`(與 local-tool 不衝突) | +| macOS App 內嵌 payload 完整 | ✅ 通過 | Contents/Resources/ 下 bin / data / scripts / python / wheels 齊備 | +| macOS App ad-hoc codesign | ✅ 通過 | `codesign --verify` 通過 | +| Windows .iss 語法 + 路徑 | ✅ 審閱通過 | 檔案存在,`visiona-local` 字串已清除,AppId GUID 固定 | +| Linux build-appimage.sh | ✅ 審閱通過 | 腳本存在,`visiona-local` 字串已清除 | +| CI workflow `build.yml` | ✅ 審閱通過 | 三 job(macos-13 / windows-2022 / ubuntu-22.04)設定齊備 | +| CI workflow cache key | ✅ 通過 | 使用 `visiona-agent` 路徑與 artifact 名稱 | +| Windows 實 build | ⏳ 待 CI | macOS 無法交叉 build WebView2 + iscc;依賴 `.github/workflows/build.yml` Windows runner | +| Linux 實 build | ⏳ 待 CI | macOS 無法 build webkit2gtk + appimagetool;依賴 `.github/workflows/build.yml` Linux runner | +| 三平台安裝到乾淨環境 | ⏳ 待人工 | 需要乾淨 macOS / Windows / Linux VM 或實機 | + +## 與 local-tool 共存對照表 + +兩個工具可同時安裝在同一台機器上,互不干擾。 + +| 項目 | local-tool | visionA Agent | 結論 | +|------|-----------|---------------|------| +| App Bundle Identifier(macOS) | `com.innovedus.visiona-local` | `com.innovedus.visiona-agent` | ✅ 不衝突 | +| App Bundle Name(macOS) | `visionA-local` | `visionA Agent` | ✅ 不衝突 | +| App 安裝位置(macOS) | `/Applications/visiona-local.app` | `/Applications/visiona-agent.app` | ✅ 不衝突 | +| App 安裝位置(Windows) | `Program Files\visiona-local` | `Program Files\visiona-agent`(`DefaultDirName={autopf}\visiona-agent`) | ✅ 不衝突 | +| Inno Setup AppId GUID(Windows) | local-tool 自己的 | `{8671343F-815C-4AA5-891F-1C453CE14E82}` | ✅ 不同 GUID | +| Data dir(macOS) | `~/Library/Application Support/visiona-local` | `~/Library/Application Support/visiona-agent`(`platform_darwin.go` + `appName="visiona-agent"`) | ✅ 不衝突 | +| Data dir(Linux) | `~/.local/share/visiona-local` | `~/.local/share/visiona-agent` | ✅ 不衝突 | +| Single-instance lock 檔名 | `visiona-local.lock` | `visiona-agent.lock`(`app.go:1580`) | ✅ 不衝突 | +| IPC port sentinel 檔名 | `visiona-local.ipc-port` | `visiona-agent.ipc-port`(`app.go:1539`) | ✅ 不衝突 | +| 對外網路 port | 3721(HTTP UI) | 無(server 只 bind 127.0.0.1:random) | ✅ 無衝突 | +| Tray icon 狀態列圖示 | 獨立 | 獨立 | ✅ 可同時常駐 | + +**macOS 雙安裝測試**:本機已有 `/Applications/visiona-local.app`(Bundle ID `com.innovedus.visiona-local`),本次 build 的 DMG 掛載後顯示 `visiona-agent.app`(Bundle ID `com.innovedus.visiona-agent`),LaunchServices 視為不同應用,可並存。 + +## 遇到的問題與處理 + +### 1. Wails build 因 `visiona-agent/frontend` 缺 package.json 而失敗 + +**症狀**:首次跑 `make wails-macos`,Wails 偵測 `assetdir: ./frontend` 後自動執行 `frontend:install`,在 `visiona-agent/frontend/` 跑 `pnpm install`,但該目錄只放 `//go:embed` 用的靜態產物,沒有 `package.json`,報錯: + +``` +ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND No package.json... was found in "visiona-agent/frontend". +``` + +**修復**:`Makefile` 的 `wails-macos` / `wails-windows` / `wails-linux` 三個 target 的 `wails build` 命令都加 `-s`(Skip Frontend)。理由:前端產物已由 `wails-sync-frontend` target 從 `frontend/out/` 同步過來,Wails 不需要自己再跑 pnpm。 + +### 2. 殘留 `visiona-local` / `visionA-local` 字串 + +**找到 3 處**,已全部修正: + +| 檔案 | 行數 | 原字串 | 修正後 | +|------|------|--------|--------| +| `installer/linux/99-kneron.rules` | 1 | `# Kneron USB devices — visionA-local` | `# Kneron USB devices — visionA Agent` | +| `installer/macos/make-dmg-background.py` | 49 | `title = "Drag visionA-local to Applications"` | `title = "Drag visionA Agent to Applications"` | +| `.github/workflows/release.yml` | 55 | `name: visionA-local ${{ github.ref_name }}` | `name: visionA Agent ${{ github.ref_name }}` | + +`installer/windows/visiona-agent.iss:117` 的 `edge-ai-platform` 字串是註解(「未來若要偵測舊版 edge-ai-platform...」),保留。 + +### 3. go.mod Wails 版本警告(非致命) + +Wails CLI 提示: + +``` +Warning: go.mod is using Wails '2.11.0' but the CLI is 'v2.12.0'. +``` + +Build 本身不受影響。可未來升級(`wails build -u` 或手動改 `go.mod`),AB12 不處理。 + +## Makefile 變更摘要(AB12) + +| Target | 變更 | +|--------|------| +| `wails-macos` | `wails build` 加 `-s`,並加註解說明為何(frontend 靜態產物由 wails-sync-frontend 處理) | +| `wails-windows` | 同上 | +| `wails-linux` | 同上(兩個分支都加) | + +## 完整 build 流程(macOS 本機) + +```bash +# 一次性安裝 +brew install create-dmg +go install github.com/wailsapp/wails/v2/cmd/wails@latest +export PATH="$HOME/go/bin:$PATH" + +# vendor(第一次約 5-10 分鐘,之後吃 cache) +make vendor-sync + +# build pipeline +make build-frontend # → frontend/out/ (~1 MB) +make build-server # → dist/visiona-agent-server (~31 MB) +make payload-macos # → payload/darwin/ (~203 MB) +make wails-macos # → visiona-agent/build/bin/visiona-agent.app (~216 MB) +make dmg # → dist/visiona-agent.dmg (~160 MB) + +# 或一步到位 +make clean-build-dmg +``` + +## 下一步建議 + +1. **推到 GitHub 觸發 CI**:`.github/workflows/build.yml` 會在 Windows 與 Linux runner 上完整 build,驗證 pipeline 在另兩個平台也能跑通。 +2. **乾淨環境安裝測試**: + - macOS:下載 `dist/visiona-agent.dmg`,在沒裝過 visionA Agent 的 Mac 上 drag-to-Applications,首次啟動確認 tray icon 出現、Pair 流程可用。 + - Windows:CI 產出的 `.exe` 在 Win10/11 乾淨 VM 上跑,確認 Inno Setup 安裝流程、WinUSB driver 首次啟動提權。 + - Linux:CI 產出的 `.AppImage` 在 Ubuntu 22.04 上 `chmod +x && ./visiona-agent-*.AppImage`,確認 udev rule 提示可用。 +3. **Code signing / notarization**(Phase 2,暫不做): + - macOS:Developer ID + notarize(否則使用者首次打開要在設定 > 安全性手動允許) + - Windows:EV Code Signing 憑證(避免 SmartScreen 警告) +4. **安裝與 local-tool 同機共存**驗證:在已安裝 local-tool 的機器上安裝 Agent,確認兩個 tray icon 同時出現、兩套 data dir 獨立、兩個 server 可同時運作。 diff --git a/local-agent/docs/LOCAL-TOOL-SPEC.md b/local-agent/docs/LOCAL-TOOL-SPEC.md new file mode 100644 index 0000000..98d826c --- /dev/null +++ b/local-agent/docs/LOCAL-TOOL-SPEC.md @@ -0,0 +1,313 @@ +# visionA Agent Tool — 功能規格文件 + +> 版本:v0.1.0-dev | 最後更新:2026-04-16 | 對應 commit:`9793a2e` + +## 概述 + +visionA Agent Tool 是 Kneron KL520 / KL720 邊緣 AI 推論硬體的**本機桌面應用**,讓使用者在完全離線的環境下,透過 USB 連接 Kneron 裝置,進行影像 / 影片 / 攝影機即時推論。 + +架構為 **Wails 桌面 App(Go + Vanilla JS 控制台)+ Go HTTP Server + Next.js Web UI(瀏覽器)**。Wails 控制台負責 server lifecycle 管理,瀏覽器 Web UI 負責裝置操作、模型管理、推論工作區。 + +### 三平台支援 + +| 平台 | 安裝格式 | Build target | +|------|---------|-------------| +| macOS (x86_64) | `.dmg` | `make dmg` | +| Windows (x64) | `.exe` (Inno Setup) | `make exe` | +| Linux (x86_64) | `.AppImage` | `make appimage` | + +### 完全離線安裝 + +所有依賴(Python runtime、wheels、ffmpeg、ffprobe、.nef 模型)全部 vendor 進 installer,安裝和執行過程**不需要網路連線**。 + +--- + +## 1. Wails 控制台(桌面應用程式視窗) + +### 1.1 啟動流程 — 5 階段進度面板 + +| 階段 | 名稱 | 做什麼 | +|------|------|--------| +| 1 | 初始化控制台 | 建立 dataDir、舊資料遷移、single-instance lock、IPC server、首次啟動 seed(複製內建模型到 user dataDir) | +| 2 | 檢查 Python 執行環境 | 偵測系統 Python → 或解壓內建 Python runtime(首次啟動 ~1-3 分鐘)→ 建 venv → pip install wheels(numpy/opencv/KneronPLUS 等 9 個套件) | +| 3 | 啟動本機伺服器 | spawn `visiona-agent-server` subprocess → 等 health check 通過(首次啟動 Windows Defender 掃描可能需 1-2 分鐘) | +| 4 | 偵測 Kneron 裝置 | 對 `/api/devices` 發一次 GET 確認 server 可 serve 業務 endpoint | +| 5 | 開啟瀏覽器 | 自動開啟系統預設瀏覽器到 `http://127.0.0.1:3721`(可在設定中關閉) | + +- **全屏 splash**:app 啟動最一開始顯示 logo + spinner 全屏 overlay,收到第一個 stage event 後切換到 5 階段面板 +- **每階段細步文案**:Go 端在每個 sub-step emit `startup:stage-detail` event,前端即時顯示當前正在做什麼(例如「正在解壓 Python runtime」「等待伺服器健康檢查通過」) +- **啟動完成自動收合**:5 階段全部完成後面板收合成一行 summary「✓ 啟動完成 · 點此展開檢視」,使用者點擊可展開歷史紀錄 +- **重啟 / 重試時重新展開**:按「重新啟動伺服器」或「重試」會重置面板並完整重跑 5 階段 +- **前端 snapshot 補漏**:前端 init 完成後呼叫 `GetStartupSnapshot()` 補上 race window 中漏掉的 stage events,避免順序亂跳 +- **Web UI 連線指示燈**:Stage 6「等待 Web UI 連線」從面板隱藏,改為 header 的 Web UI 連線指示燈(綠燈 = 已連線、黃燈 pulse = 等待中) + +### 1.2 Timeout 機制 + +| 類型 | 時長 | 行為 | +|------|------|------| +| 每階段 soft timeout | 20 秒 | emit「正在重試...」提示,**不中斷流程** | +| 整體 hard timeout | 5 分鐘(300 秒) | 超過則 fail 進 Error state | +| 首次 bootstrap pause | 無上限 | Stage 1 seed / Stage 2 Python bootstrap / Stage 3 waitHealthy 期間暫停 hard timeout 計時,不算進 5 分鐘 budget | + +### 1.3 控制台功能 + +- **狀態列**:顯示 server 狀態(閒置 / 啟動中 / 執行中 / 停止中 / 已停止 / 錯誤)、port、PID、uptime、Web UI 連線狀態 +- **主要控制**:在瀏覽器開啟 / 啟動 / 管理(停止 / 重新啟動 / 開啟 log 資料夾) +- **Log 面板**:server 即時 log 顯示、自動跟隨最新、關鍵字過濾、層級過濾(All / INFO / WARN / ERROR)、清空 / 複製 / 匯出 / 開啟 log 資料夾 +- **Settings**:啟動時自動開啟瀏覽器(macOS/Windows 預設 ON、Linux 預設 OFF)、語言切換(繁體中文 / English / Auto)、關於資訊 +- **Shutdown modal**:server 停止時顯示「正在停止伺服器…」overlay,15 秒 watchdog 自動 hide,可用 Esc / 點 backdrop 手動關閉 +- **Error 處理**:server 啟動失敗 → 紅 banner「伺服器無法啟動」+ 重試按鈕;runtime crash → OS 原生通知 + Error state +- **i18n 雙語**:繁體中文 + English,全 UI 元素(含 stage 細步文案、設定、錯誤訊息) + +### 1.4 Single-instance 保護 + +- 用 `visiona-agent.lock` 檔案鎖 + PID 存活檢查 +- 第二次啟動偵測到已有 instance → 嘗試 IPC raise 把現有視窗提到前景 → 自己 quietly exit +- Stale lock(PID 已死)自動清理並取得新 lock + +--- + +## 2. Web UI(瀏覽器介面) + +### 2.1 頁面路由 + +| 路由 | 功能 | +|------|------| +| `/` | 儀表板 — 已連接裝置列表、活動時間軸、統計卡片 | +| `/devices` | 裝置列表 — 掃描 / 連接 / 斷開 Kneron USB 裝置 | +| `/devices/[id]` | 裝置詳細 — 狀態、健康度、韌體版本、連線日誌、設定 | +| `/models` | 模型庫 — 7 個預設 .nef 模型(KL520 × 4 + KL720 × 3)+ 上傳自訂模型 | +| `/models/[id]` | 模型詳細 — metadata、支援硬體、效能數據 | +| `/workspace` | 工作區 — 選裝置 → 選模型 → 選來源 → 推論 | +| `/workspace/[deviceId]` | 單一裝置工作區 — 推論操作主介面 | +| `/settings` | 設定 — 語言、主題、其他偏好 | + +### 2.2 推論來源 + +| 來源 | 說明 | +|------|------| +| 攝影機 (Camera) | USB / IP Camera 即時串流推論 | +| 圖片 (Image) | 上傳單張 JPG/PNG 做單次推論 | +| 影片 (Video) | 上傳 MP4/AVI/MOV/MPEG/MPG 做逐幀推論(ffmpeg 解碼) | +| 批次圖片 (Batch Images) | 上傳多張圖片逐張推論 | + +### 2.3 推論結果顯示 + +- **MJPEG 即時串流**:`/api/camera/stream` 提供多客戶端 multipart MJPEG 串流 +- **Canvas overlay**:bounding box + label + confidence 即時疊加 +- **推論面板**:FPS、平均延遲、信心度門檻調整、分類結果展示 +- **影片進度**:目前幀 / 總幀、seek 跳轉 +- **批次進度**:已處理 / 總張數、縮圖預覽 + +### 2.4 模型管理 + +- **7 個預設模型**(NEF 格式、INT8 量化) +- **上傳自訂 .nef 模型**:`POST /api/models/upload` +- **刪除已上傳模型**:`DELETE /api/models/:id` +- **模型篩選**:按任務類型(object_detection / classification)、硬體(KL520 / KL720)、關鍵字 +- **模型對比工具**:並排比較兩個模型的規格 + +### 2.5 裝置管理 + +- **掃描 USB 裝置**:偵測已連接的 Kneron KL520 / KL720 +- **連接 / 斷開**:連接時自動載入 firmware(KL520 USB Boot 流程) +- **韌體更新**:flash .nef 到裝置 + 進度 WebSocket 即時回報 +- **WinUSB 驅動安裝**(Windows only):首次啟動自動透過 KneronPLUS SDK libwdi 安裝,需 UAC 提權 + +### 2.6 離線覆蓋層 + +- Server 關閉 / 崩潰時,瀏覽器 tab 顯示 Offline Overlay +- `role=alertdialog` + focus trap + WebSocket 重連機制 +- `boot-id` 偵測 server restart → 自動 reload(含 reload loop guard) + +--- + +## 3. 預設模型 + +### KL520(4 個) + +| 模型 ID | 名稱 | 任務 | 輸入 | +|---------|------|------|------| +| kl520-yolov5-detection | YOLOv5 Detection | 物件偵測 | 640×640 | +| kl520-fcos-detection | FCOS Detection | 物件偵測 | 512×512 | +| kl520-ssd-face-detection | SSD Face Detection | 人臉偵測 | 320×240 | +| kl520-tiny-yolov3 | Tiny YOLOv3 | 物件偵測 | 416×416 | + +### KL720(3 個) + +| 模型 ID | 名稱 | 任務 | 輸入 | +|---------|------|------|------| +| kl720-yolov5-detection | YOLOv5 Detection | 物件偵測 | 640×640 | +| kl720-resnet18-classification | ResNet18 Classification | 分類(1000 類)| 224×224 | +| kl720-fcos-detection | FCOS Detection | 物件偵測 | 512×512 | + +--- + +## 4. 內嵌依賴 + +| 依賴 | 版本 | 用途 | 授權 | +|------|------|------|------| +| Python | 3.12.9 (python-build-standalone) | Kneron Python bridge | PSF | +| numpy | 2.4.4 | 影像處理 | BSD | +| opencv-python-headless | 4.10.0 | 影像處理 | Apache-2.0 | +| KneronPLUS | 2.0.0 | Kneron SDK Python binding | Kneron | +| pyusb | 1.3.1 | USB 裝置存取 | BSD | +| requests | 2.33.1 | HTTP 工具 | Apache-2.0 | +| ffmpeg | 自建 LGPL v3 decoder-only | 影片解碼(mp4/avi/mov/mpeg/mpg)| LGPL v3 | +| ffprobe | 同上 | 影片 metadata 提取 | LGPL v3 | + +### ffmpeg LGPL 合規 + +macOS 版為自行編譯的最小 decoder-only build(~5.7MB),只啟用必要的 demuxer/decoder/parser/filter,**無 --enable-gpl / libx264 / libx265**,符合 LGPL v3。Windows/Linux 使用 BtbN 官方 LGPL 預編譯 binary。 + +--- + +## 5. Server API 摘要 + +### 系統 + +| Method | Path | 說明 | +|--------|------|------| +| GET | `/api/system/health` | 健康檢查(高頻輪詢用,無 log) | +| GET | `/api/system/info` | 版本、平台、uptime | +| GET | `/api/system/metrics` | Go runtime 記憶體統計 | +| GET | `/api/system/deps` | 外部依賴檢查(python/ffmpeg/ffprobe) | +| GET | `/api/system/boot-id` | server 啟動 ID(偵測 restart 用) | +| POST | `/api/system/restart` | 重啟 server | +| POST | `/api/system/install-driver` | 安裝 Kneron USB 驅動(Windows only) | +| POST | `/api/system/shutdown-notify` | 廣播關機通知到 WebSocket clients | + +### 模型 + +| Method | Path | 說明 | +|--------|------|------| +| GET | `/api/models` | 列出所有模型(支援 filter) | +| GET | `/api/models/:id` | 取得單一模型詳細 | +| POST | `/api/models/upload` | 上傳自訂 .nef 模型 | +| DELETE | `/api/models/:id` | 刪除已上傳模型 | + +### 裝置 + +| Method | Path | 說明 | +|--------|------|------| +| GET | `/api/devices` | 列出已偵測裝置 | +| POST | `/api/devices/scan` | 掃描 USB 裝置 | +| GET | `/api/devices/:id` | 取得裝置詳細 | +| POST | `/api/devices/:id/connect` | 連接裝置 | +| POST | `/api/devices/:id/disconnect` | 斷開裝置 | +| POST | `/api/devices/:id/flash` | 燒錄韌體 | +| POST | `/api/devices/:id/inference/start` | 啟動推論 | +| POST | `/api/devices/:id/inference/stop` | 停止推論 | + +### 媒體 + +| Method | Path | 說明 | +|--------|------|------| +| GET | `/api/camera/list` | 列出可用攝影機 | +| POST | `/api/camera/start` | 啟動攝影機推論 pipeline | +| POST | `/api/camera/stop` | 停止 pipeline | +| GET | `/api/camera/stream` | MJPEG 即時串流 | +| POST | `/api/media/upload/image` | 上傳單張圖片推論 | +| POST | `/api/media/upload/video` | 上傳影片逐幀推論 | +| POST | `/api/media/upload/batch-images` | 批次上傳圖片推論 | +| GET | `/api/media/batch-images/:index` | 取得批次中特定幀 | +| POST | `/api/media/seek` | 影片 seek 跳轉 | + +### WebSocket + +| Path | 說明 | +|------|------| +| `/ws/devices/events` | 裝置連接 / 斷開事件 | +| `/ws/devices/:id/flash-progress` | 韌體更新進度 | +| `/ws/devices/:id/inference` | 推論結果即時串流 | +| `/ws/server-logs` | server log 即時廣播 | +| `/ws/system` | 系統事件(含 shutdown-imminent) | + +--- + +## 6. 資料目錄 + +| 平台 | 位置 | +|------|------| +| macOS | `~/Library/Application Support/visiona-agent/` | +| Windows | `%APPDATA%\visiona-agent\` | +| Linux | `~/.config/visiona-agent/` 或 `$XDG_DATA_HOME/visiona-agent/` | + +### 目錄結構 + +``` +visiona-agent/ +├── visiona-agent.lock # single-instance 鎖 +├── visiona-agent.ipc-port # server port(供 raise 用) +├── visiona-agent.wails-ipc-port # Wails IPC port +├── preferences.json # 使用者偏好(autoOpenBrowser / locale) +├── models.json # 預設模型 catalog(首次啟動從 bundle seed) +├── nef/ # 預設 .nef 模型檔(首次啟動從 bundle seed) +│ ├── kl520/ +│ └── kl720/ +├── custom-models/ # 使用者上傳的自訂模型 +├── runtime/ # Python runtime(首次啟動解壓) +│ ├── python/ +│ └── venv/ +├── logs/ +│ ├── server.stdout.log # server subprocess stdout +│ ├── server.stderr.log # server subprocess stderr +│ └── wails.log # Wails app (appLog) 啟動流程日誌 +└── .first-ws-connected # WebSocket sentinel file(啟動階段用) +``` + +--- + +## 7. Installer Build 指引 + +### macOS + +```bash +cd local-tool +make vendor-sync # 下載 Python + wheels + 驗證 ffmpeg LGPL +make payload-macos # 準備 payload/darwin/ +make dmg # → dist/visiona-agent.dmg (~163MB) +``` + +### Windows(需在 Windows 機器上) + +```powershell +cd local-tool +.\scripts\bootstrap-windows.ps1 # 安裝 Go / Node / pnpm / Wails / Python / MSYS2 +make exe # → dist/visiona-agent-*-windows-x64.exe +``` + +### Linux(Ubuntu 22.04+) + +```bash +cd local-tool +bash scripts/bootstrap-linux.sh # 安裝所有依賴 + 自動 build +# 或手動: +make vendor-python-linux vendor-wheels-linux vendor-ffmpeg-linux +make payload-linux +make appimage # → dist/visiona-agent-*-linux-x64.AppImage +``` + +--- + +## 8. 除錯指引 + +### Windows 啟動問題 + +1. 檢查 `%APPDATA%\visiona-agent\logs\wails.log` + - 第一行應有 `fix marker: d946561+`(確認 build 版本) + - Stage 1-5 的細步文案會記錄每個 sub-step +2. 檢查 `server.stdout.log` / `server.stderr.log` +3. 如需更詳細的 Wails 端 log,從命令列啟動: + ``` + "C:\Program Files\visiona-agent\visiona-agent.exe" > %TEMP%\wails-debug.txt 2>&1 + ``` + +### 常見問題 + +| 症狀 | 可能原因 | 解法 | +|------|---------|------| +| 啟動 Stage 3 timeout | Windows Defender 首次掃 .exe | 等待,有 pause 機制不會 fail;或加白名單 | +| 第二次啟動閃黑窗消失 | 前一次 crash 的 lock 殘留 | 刪 `visiona-agent.lock` | +| 瀏覽器看不到裝置 | USB 權限(Linux)/ WinUSB driver 沒裝 | Linux: `sudo bash install-udev.sh`;Windows: 從 UI 點「安裝 Kneron USB 驅動程式」 | +| 模型庫是空的 | 首次啟動 seed 失敗 | 檢查 `wails.log` 的 seed 訊息;或手動複製 `models.json + nef/` 從 installer 到 user dataDir | diff --git a/local-agent/docs/legacy-local-tool-readme.md b/local-agent/docs/legacy-local-tool-readme.md new file mode 100644 index 0000000..450fa40 --- /dev/null +++ b/local-agent/docs/legacy-local-tool-readme.md @@ -0,0 +1,209 @@ +# Legacy local-tool README(已封存) + +> **此檔僅作歷史保留。** +> +> 以下內容是 visionA Agent 於 2026-04-22 從 `local-tool/`(baseline commit `b71ff4cd3c72e879435f773ae15b23bf8b70841e`)fork 時的原始 README 後半段。 +> +> 內容描述的是 **local-tool**(單機桌面 Kneron 工具)的產品定位、安裝流程、功能清單與不做的事,**並非 visionA Agent 的當前定位**。 +> +> visionA Agent 的真實定位請見 `local-agent/README.md`: +> - visionA Agent **就是** tunnel client(local-tool 明確排除「Tunnel」) +> - visionA Agent 的 UI 只剩 3 頁配置(狀態 / 配對 / 設定),不保留 local-tool 的裝置 / 模型 / 推論 UI +> - visionA Agent 不會單獨上架,靠雲端 visionA-backend 驅動 +> +> 保留此檔的目的: +> 1. 未來若需從 local-tool cherry-pick bug fix,可比對原始 README 釐清 baseline 行為 +> 2. 部分 build 流程(Makefile target、三平台 installer)仍延用 local-tool 結構,這份原文是歷史對照 +> +> 若內容已不再有參考價值,可整檔刪除。 + +--- + +> **裝起來像一般 app,離線也能跑,接上 Kneron 就推論。** +> 把 `edge-ai-platform` 的 Kneron AI 邊緣推論能力,打包成單機桌面應用。 + +![macOS x86_64](https://img.shields.io/badge/macOS_x86__64-beta-brightgreen) +![Windows x86_64](https://img.shields.io/badge/Windows_x86__64-TBD-lightgrey) +![Linux x86_64](https://img.shields.io/badge/Linux_x86__64-TBD-lightgrey) +![License](https://img.shields.io/badge/license-TBD-orange) + + + +--- + +## 這是什麼 + +local-tool 是 `edge-ai-platform`(原本要部署到 EC2 + Docker 的 Kneron 邊緣推論平台)的**單機桌面衍生版本**。為「帶著筆電做 Kneron demo 的人」而生 —— 主要服務 Innovedus 內部 FAE 與外部 Kneron 開發者。 + +三個核心承諾: + +- 🎒 **零依賴**:Python runtime、KneronPLUS SDK、ffmpeg、預置 `.nef` 模型全部內嵌 +- ✈️ **零網路**:下載一次後完全離線可用(適合客戶現場 IT 鎖得死緊的場景) +- 🖱️ **零學習成本**:雙擊安裝 → 開啟 → 插上 Kneron 裝置 30 秒內跑出第一幀推論 + +對標產品:Docker Desktop、Ollama。 + +--- + +## 安裝(使用者) + +### macOS(x86_64,beta) + +1. 從內部 Gitea Releases 下載 `visiona-local.dmg` +2. 雙擊開啟 dmg → 把 `visionA Local.app` 拖到 `Applications/` +3. **第一次啟動**:因為未做程式碼簽章,Gatekeeper 會警告「來自未識別開發者」 + - 在 Finder 中**右鍵點 `visionA Local.app` → 選「開啟」**(不是雙擊) + - 對話框出現「仍要開啟」時點確認 + - 往後直接雙擊即可 +4. **首次啟動會花 30–60 秒**解壓內嵌的 Python runtime 並離線安裝 wheels + 這是預期行為,不是卡住。之後啟動只要幾秒 + +> 📁 資料目錄:`~/Library/Application Support/visiona-local/` +> 包含 log、lock、ipc-port、自上傳模型 + +### Windows / Linux + +**Coming soon** — build script 已經寫好,等 CI runner 齊備後就會釋出。 +- Windows:Inno Setup `.exe` installer +- Linux:`.AppImage` + udev rules(需 root 裝 `99-kneron.rules`) + +--- + +## 系統需求 + +| 平台 | 最低版本 | 架構 | +|------|---------|------| +| macOS | 14 Sonoma | x86_64 ¹ | +| Windows | 10 1809 | x86_64 | +| Ubuntu | 22.04 | x86_64 | + +¹ Apple Silicon 理論上可透過 Rosetta 2 執行,但**未經測試**。 + +**離線可用**:安裝後所有核心功能(包含 Python sidecar、推論、模型管理、攝影機、影片解碼)完全不需要網路。 + +--- + +## 功能總覽 + +### ✅ 有的功能 + +- **裝置管理**:USB 自動偵測 Kneron KL520 / KL720,10 秒內連線 +- **攝影機推論**:MJPEG 串流 + 即時 overlay(首次延遲 ≤ 250ms,穩定後 ≤ 150ms) +- **模型管理**:8 個預置 `.nef` 模型(分類 / 偵測 / 臉辨)+ 自上傳切換 +- **核心推論引擎**:image classification、object detection、face recognition +- **媒體推論**:支援圖片與影片檔(本機上傳,R5 決策後不支援 URL 推論) +- **中英雙語**,跟隨系統 Dark Mode + +### ❌ 不做的事(明確排除) + +> ⚠️ 注意:以下排除清單是 **local-tool** 的設計邊界,**不適用於 visionA Agent**。 +> visionA Agent 的核心能力之一就是 Tunnel(與此清單相反)。 + +為了聚焦「個人工具」,以下功能從 `edge-ai-platform` 全數砍掉: + +- ❌ Cluster(多裝置叢集) +- ❌ Relay / Tunnel(遠端連線、反向代理) +- ❌ 韌體燒錄(firmware flash) +- ❌ 系統列 Tray 常駐 +- ❌ Auto-update +- ❌ Telemetry / 崩潰回報 +- ❌ License 啟用、憑證簽章 +- ❌ Mac App Store / Microsoft Store / Snap Store 上架 + +--- + +## 開發者區(local-tool 結構,僅供對照) + +### 專案結構 + +``` +local-tool/ +├── .autoflow/ PRD / 設計 / 架構 / 進度文件 +├── server/ Go 1.26 後端(Gin + go:embed) +├── frontend/ Next.js 16 + React 19 + shadcn +├── visiona-local/ Wails 應用殼(installer) +├── payload/ 打包暫存區 +├── vendor/ 第三方依賴(make vendor-sync 下載,不進 git) +├── dist/ 最終安裝檔(.dmg / .exe / .AppImage) +├── installer/ Inno Setup / AppImage script +├── scripts/ build 與維運腳本 +└── Makefile +``` + +> visionA Agent 沿用大部分目錄結構,但 `visiona-local/` → `visiona-agent/`、Bundle ID / 安裝路徑 / lockfile 全部獨立。詳見 `local-agent/docs/BUILD-VERIFICATION.md`。 + +### 開發流程 + +```bash +# 1. 下載全部第三方依賴到 vendor/ +make vendor-sync + +# 2. 本機 build 並產出 dmg(macOS) +make dmg + +# 查看所有可用 targets +make help +``` + +主要 make targets: + +| Target | 作用 | +|--------|------| +| `vendor-sync` | 下載 python-build-standalone、wheels、ffmpeg | +| `build-server` | 編譯 Go server binary(先 build frontend + embed) | +| `build-frontend` | pnpm build Next.js 靜態產物 | +| `payload-macos` | 準備 macOS payload(binary + python + wheels + ffmpeg + 模型) | +| `wails-macos` | Wails build + ad-hoc codesign | +| `dmg` | 產出 `dist/visiona-local.dmg`(local-tool)/ `visiona-agent.dmg`(visionA Agent) | +| `exe` | Windows installer(需在 Windows runner 執行) | +| `appimage` | Linux AppImage(需在 Linux runner 執行) | + +### 三方平台 build + +| 平台 | 指令 | 執行環境 | +|------|------|---------| +| macOS | `make dmg` | 本機(Intel Mac) | +| Windows | `make exe` | Windows runner + Inno Setup 6 | +| Linux | `make appimage` | Ubuntu 22.04+ runner + appimagetool | + +`vendor-*-windows` / `vendor-*-linux` 可在 macOS 上跑通(只有 `wails-*` 和最後一步 installer 需要對應平台)。 + +--- + +## 已知限制與 TODO(local-tool 原文) + +- 🟡 **Kneron 預置模型 re-distribution 授權**:開發階段假設可用,正式發佈前需與 Kneron 官方確認 +- 🟡 **Windows / Linux 安裝檔**:build script 就緒,等 CI runner 齊備 +- 🟡 **Apple Silicon** 未經測試(理論上 Rosetta 2 可跑) +- 🟡 **Linux Kneron USB vendor ID**:`installer/linux/99-kneron.rules` 需最終確認 +- 🟡 程式碼簽章(Developer ID / EV cert)**不做**,使用者需手動繞過 Gatekeeper / SmartScreen +- 🟡 **無 auto-update**:新版需手動從 Gitea 下載 + +--- + +## 授權 + +**License: TBD**(內部工具 / MIT / proprietary 待定,發佈前確認) + +### 第三方元件授權 + +| 元件 | 授權 | 備註 | +|------|------|------| +| ffmpeg | **LGPL v3**(方案 B 混合:macOS 自 build decoder-only / Windows & Linux 用 BtbN n7.1 LGPL) | v2 TDD §2.2 | +| KneronPLUS SDK | Kneron 商用條款 | 再次確認 re-distribution 權利 | +| python-build-standalone | MPL 2.0 / PSFL | — | +| Python 標準函式庫 | PSFL | — | +| shadcn/ui | MIT | — | +| Next.js / React | MIT | — | +| Wails | MIT | — | +| Gin | MIT | — | + +完整第三方授權清單於 `.autoflow/02-prd/PRD.md` §4.8。 + +--- + +## 致謝 / 起源 + +local-tool 衍生自 Innovedus 內部專案 `edge-ai-platform`(原為部署於 EC2 + Docker 的多人共享平台)。本專案將其改造為單機桌面版本,聚焦「一個人帶一台筆電」的使用場景。 + +感謝 Kneron、python-build-standalone(astral-sh)、shadcn 等開源社群。 diff --git a/local-agent/frontend/.gitignore b/local-agent/frontend/.gitignore new file mode 100644 index 0000000..82991c6 --- /dev/null +++ b/local-agent/frontend/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ +/dist/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files(可依需要 opt-in 提交,例如 .env.example / .env.local.example) +.env* +!.env.example +!.env.local.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/local-agent/frontend/.gitkeep b/local-agent/frontend/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/local-agent/frontend/README.md b/local-agent/frontend/README.md new file mode 100644 index 0000000..e3ca763 --- /dev/null +++ b/local-agent/frontend/README.md @@ -0,0 +1,310 @@ +# visionA Agent — Frontend + +visionA Agent 是 visionA 雲端的 **local agent / tunnel bridge** — 一個桌面應用,把本機的 Kneron 邊緣裝置安全地連上 visionA 雲端。本目錄是它的前端實作:純靜態匯出(Next.js + React),由 Wails Go runtime 透過 `go:embed` 嵌入可執行檔,三平台(macOS / Windows / Linux)共用同一份程式碼。 + +> 定位:**「開了就忘掉它」的工具**。使用者配對一次後,Agent 只要維持連線、不出問題就好。UI 極簡,沒有裝置面板、推論視窗、行銷素材 — 只有連線狀態、配對、設定三頁。 + +## 技術堆疊 + +| 層級 | 技術 | 版本 | +|------|------|------| +| 框架 | Next.js(App Router,`output: "export"` static) | 16.1.x | +| 語言 | TypeScript(strict) | 5.x | +| UI | React | 19.2.x | +| 樣式 | Tailwind CSS | 4.x | +| 基礎元件 | Radix UI(`radix-ui` 單一套件,shadcn/ui 風格) | 1.4.x | +| 圖示 | lucide-react | 0.575.x | +| 主題 | next-themes(Light / Dark / System) | 0.4.x | +| Toast | sonner | 2.x | +| 動畫 | tw-animate-css | 1.4.x | +| 狀態管理 | zustand(依賴保留;雛形階段實際未用) | 5.x | +| 測試 | Vitest + @testing-library/react + jsdom | Vitest 4 / RTL 16 | + +## 與其他前端專案的關係 + +| 專案 | 定位 | 是否為本專案的來源 | +|------|------|------------------| +| `visionA-frontend/`(雲端 Web) | 使用者的主要操作介面 — 裝置管理、模型管理、推論 | **是**。Design Tokens (`globals.css`)、Theme Provider、i18n 結構、`utils.ts`、Radix UI 基礎元件複製自此 | +| `local-tool/frontend/`(本機 GUI) | 開發者跑推論用的桌面工具 | **不是**。本專案**不是** local-tool 的 fork — 我們只借鑒其 Wails + Next.js `output:"export"` 的打包模式,UI 和邏輯完全獨立 | +| `local-agent/frontend/`(本目錄) | 連線橋樑 UI(狀態 / 配對 / 設定)| 獨立專案 | + +**核心設計決策**:視覺上與 visionA-frontend 保持一致,讓同一位使用者在雲端 Web 與桌面 Agent 之間切換「能一眼認出是同家人」。 + +## 前置需求 + +- **Node.js** ≥ 20 +- **pnpm** ≥ 9(專案使用 pnpm,**請勿** `npm install` / `yarn install`,會產生第二份 lockfile) +- (整合模式才需要)**Go** ≥ 1.22 與 Wails CLI v2 — 詳見 `../README.md`(local-agent 根目錄) + +## 兩種開發模式 + +### 模式 A — 獨立前端開發(`pnpm dev`) + +純前端迭代(調色、調文案、元件樣式、互動邏輯)時使用,**不需要** Wails binding。 + +```bash +pnpm install +pnpm dev +# → http://localhost:3000(支援 HMR) +``` + +此模式下: +- `src/hooks/use-*` 的 Wails binding 呼叫會自動走 **mock 實作**(見 `src/lib/agent-api.ts` 的 runtime 偵測) +- 連線狀態、配對結果、設定值皆為假資料;可切 `notPaired` / `online` / `reconnecting` / `error` 等變體驗證 UI +- 可直接切換 Light / Dark、zh-Hant / en 驗證 Design Tokens 與 i18n + +### 模式 B — 與 Wails 整合(`wails dev`) + +驗證與 Go backend(tunnel client / pairing / connstate)的真實互動時使用。 + +```bash +# 在 local-agent/ 根目錄 +wails dev +# → Wails 會自動啟動 `pnpm dev` 並注入 Wails runtime;前端呼叫 window.go.main.App.* 走真實 binding +``` + +此模式下: +- `isWailsRuntime()` 會回 `true`,`agent-api.ts` 改走真實 Wails `window.go.main.App.*` +- 事件(`connection:status` / `connection:log` / `settings:updated` / `pairing:result`)由 Go broadcaster 推送 +- Token 會真的寫入 OS keychain(macOS Keychain / Windows Credential Manager / Linux Secret Service) + +## Build + +```bash +pnpm build +``` + +產出到 `out/`(Next.js `output: "export"` 的靜態匯出結果)。Wails build 時透過 `wails.json` 的 `frontend:install` + `frontend:build` 呼叫此指令,然後透過 `assetdir: "./frontend/out"` 用 `//go:embed` 把 `out/` 整包嵌入最終可執行檔。 + +```bash +# 完整桌面應用 build(在 local-agent/ 根目錄) +wails build +``` + +### 為什麼是 `output: "export"`? + +| 方案 | 是否可行 | 理由 | +|------|---------|------| +| Next.js server(`standalone`) | ❌ | Wails 不跑 Node server | +| Next.js SSR / SSG | ❌ | 同上,且 Wails 桌面環境無 HTTP server | +| **Next.js `output: "export"`** | ✅ | 純靜態,可 embed;`local-tool` 已長期驗證此模式穩定 | +| Vite + SPA | ❓ | 可行但放棄 — 會引入第二套 build pipeline,且 visionA-frontend 元件要改 import / `'use client'` directive | + +詳見 `.autoflow/04-architecture/visiona-agent-tdd.md` §3.2。 + +## 可用腳本 + +| 指令 | 說明 | +|------|------| +| `pnpm dev` | 啟動開發伺服器(mock bindings,http://localhost:3000)| +| `pnpm build` | 產出靜態檔到 `out/`(供 Wails `go:embed`)| +| `pnpm lint` | ESLint 檢查(`eslint-config-next` + `react-hooks`)| +| `pnpm test` | 執行所有 Vitest 測試(one-shot)| +| `pnpm test:watch` | Watch 模式 | + +## 專案結構 + +``` +frontend/ +├── next.config.ts # output: "export" + trailingSlash + images.unoptimized +├── package.json # name: visiona-agent-frontend +├── tsconfig.json +├── postcss.config.mjs +├── eslint.config.mjs +├── vitest.config.ts +├── components.json # shadcn/ui 產生器設定(保留以利未來新增元件) +├── public/ +│ └── visiona-logo.png +└── src/ + ├── app/ + │ ├── globals.css # Design Tokens + Tailwind 層(100% 從 visionA-frontend 複製) + │ ├── layout.tsx # Root layout(LocaleProvider / ThemeProvider / TooltipProvider / Toaster) + │ └── page.tsx # AgentApp — 3 tabs(Radix Tabs)單頁切換入口 + ├── components/ + │ ├── theme-provider.tsx + │ ├── layout/ # Agent 專屬 layout:AppShell / Header / TabNav / ConnectionStatusBadge + │ ├── agent/ # 狀態頁元件:StatusHero / InfoCard / RecentLog;配對頁元件:TokenInput + │ └── ui/ # 24 個 Radix UI primitives(shadcn 風格封裝)+ EmptyState / Spinner / Sonner + ├── views/ # 3 個 tab 對應的 view 元件(不走 Next.js routing,以 useState 切換) + │ ├── status-view.tsx + │ ├── pair-view.tsx + │ └── settings-view.tsx + ├── hooks/ # Wails bindings 的 React wrapper + │ ├── use-connection-status.ts # GetStatus() + connection:status event + │ ├── use-recent-logs.ts # GetRecentLog() + connection:log event + │ ├── use-pair.ts # Pair(token) + pairing:result event + │ ├── use-settings.ts # GetSettings() + UpdateSettings(patch) + settings:updated + │ ├── use-test-connection.ts # TestRelay(url) + │ └── use-export-log.ts # ExportLog() + ├── lib/ + │ ├── utils.ts # cn() — clsx + tailwind-merge + │ ├── agent-api.ts # Wails binding 抽象層(真實 binding + mock 雙實作自動切換) + │ └── i18n/ + │ ├── context.tsx # LocaleProvider / useLocale / useT + │ ├── sync.tsx # LocaleSync — mount 後從 localStorage 拉偏好 + │ ├── index.ts # dictionaries 匯出入口 + │ ├── types.ts # Locale / Dictionary / SUPPORTED_LOCALES + │ └── dictionaries/ + │ ├── zh-Hant.ts # 繁中字典(預設,約 93 keys) + │ └── en.ts # English dictionary(key 集合與順序與 zh-Hant 完全一致) + ├── types/ # 前端型別定義 + │ ├── agent.ts # ConnectionState / ConnectionSnapshot / LogEntry / AgentSettings / PairError + │ └── api.ts # 與本地 server 互動的 envelope(預留) + └── tests/ + └── setup.ts # @testing-library/jest-dom 全域 matcher +``` + +## 三個頁面對應 + +| Tab | 路徑 | 主要元件 | 對應 spec 章節 | +|-----|------|---------|---------------| +| 狀態(`status`,預設) | `views/status-view.tsx` | `StatusHero`(80px 大狀態圓)+ `InfoCard`(帳號/Relay/Session)+ `RecentLog`(最近 10 筆事件)| spec §4 | +| 配對(`pair`) | `views/pair-view.tsx` | `TokenInput`(格式驗證 + 貼上 trim + Enter 送出)+ 錯誤 Alert + 底部安全提示 | spec §5 | +| 設定(`settings`) | `views/settings-view.tsx` | 5 區塊:連線 / 行為 / Log / 關於 / 危險區域 | spec §6 | + +Tab 切換走 **Radix Tabs**(controlled `value`/`onValueChange`),不走 Next.js routing。跨 view 的程式化切換透過 `window` CustomEvent `agent:switch-tab` 實現(見 `status-view.tsx` 與 `page.tsx`)。 + +## Design Tokens + +完全沿用 `visionA-frontend`:不動一行 `globals.css`。 + +| Token 類別 | 來源 | +|-----------|------| +| 色彩(`--background` / `--primary` / `--muted-foreground` 等)| `globals.css`(`@layer base` 定義 light / dark 值)| +| 連線狀態色(`--status-online` / `--status-offline` / `--status-reconnecting` / `--status-idle` / `--status-error`)| 同上(visionA-frontend F2 定義)| +| 字型 | `font-sans`(UI)/ `font-mono`(Token / Log / Relay URL)| +| 圓角 / 陰影 / 間距 | Tailwind 4 預設 + 少量 `@theme` 擴充 | + +**規則**:不允許在元件裡寫死 `#xxxxxx` 或 `rgb(...)`,一律透過 Tailwind utility class 引用 CSS 變數(例如 `bg-status-online`、`text-destructive`)。 + +## i18n + +採用自製輕量 i18n(不依賴 `next-intl`),以 React Context + `localStorage` 管理當前 locale。 + +- 預設 locale:`zh-Hant`(繁中) +- 支援:`zh-Hant` / `en` +- 儲存 key:`visionA.locale` +- Fallback:找不到 key 時回傳 key 本身(production 靜默;dev 印 warning) +- 測試保證:`i18n.test.ts` 驗證兩語系 key 集合完全一致,避免漏譯 + +字典採**扁平 key** 結構(例:`nav.status`),共分 8 個區塊: + +| 區塊 | 說明 | +|------|------| +| `app.*` | 產品名稱、標語 | +| `common.*` | 通用按鈕 / 文案(loading / cancel / save ...)| +| `nav.*` | Tab 導航標籤 | +| `connection.*` | 連線狀態(online / offline / reconnecting / notPaired / error) | +| `header.*` | Header 工具列(切主題、切語言)| +| `status.*` | 狀態頁(hero / info / action / confirm / log / empty)| +| `pair.*` | 配對頁(title / input / button / alert / error)| +| `settings.*` | 設定頁(section / relayUrl / behavior / log / about / reset)| + +新增 i18n key 時的 checklist: + +1. 兩個字典(`zh-Hant.ts` + `en.ts`)**同步新增** — 否則 `i18n.test.ts` 會失敗 +2. 保持兩邊區塊順序一致(便於 diff 與 review) +3. 變數內插用 `{name}` 風格(例:`Attempt {n} of 5`),呼叫端以 `String.prototype.replace` 替換 +4. 錯誤訊息要「說清楚發生什麼 + 使用者可以做什麼」 + +## 測試 + +```bash +pnpm test # one-shot 執行所有測試 +pnpm test:watch # watch 模式 +``` + +### 測試策略 + +| 層級 | 工具 | 範例 | +|------|------|------| +| 單元(元件) | Vitest + RTL | `button.test.tsx` / `token-input.test.tsx`(行為 + 格式驗證 regex)| +| 單元(hooks / utils) | Vitest | `i18n.test.ts`(字典完整性 + key 集合一致性)| +| 整合(view) | Vitest + RTL | `app-shell.test.tsx`(Header + Badge 組合)| +| E2E | — | 由 Testing Agent 負責,不在本 repo | + +### 關鍵測試 + +- **`i18n.test.ts`** — 字典完整性守門員;兩語系 key 集合差異時立即失敗 +- **`token-input.test.tsx`** — Pairing Token regex `^vAc_[a-f0-9]{32}$/i`(大小寫不敏感)+ 貼上清理 + Enter 送出 +- **`status-hero.test.tsx`** — 5 種狀態變體(online / offline / reconnecting / notPaired / error)+ aria-label 組合 + icon `aria-hidden` + +## 效能預算 + +本 Agent 不是 Web App,不需要滿足 Core Web Vitals。但仍有一些自律規則: + +| 項目 | 目標 | +|------|------| +| Build 時間 | < 10s(Next.js 16 Turbopack 目前約 3-4s) | +| `out/` 總大小 | < 5 MB(static export 後,Wails embed 成本敏感) | +| 首次渲染到互動 | < 200ms(Wails WebView 2 本機載入,無網路延遲) | +| 冷啟動到可互動 | < 1s(含 Wails process init + WebView init)| + +bundle 分析:`pnpm build` 時 Next.js 會印出每條 route 的大小。若單 route JS > 200KB,需檢討是否有多餘依賴或未做 code splitting。 + +## 無障礙規範 + +嚴格遵守 WCAG 2.2 AA(對齊 spec §9): + +- **不只靠顏色**:每個連線狀態同時有「圓點顏色 + icon + 文字標籤」 +- **語義化 HTML**:`
` / `
` / `