feat(local-tool): clean build 為預設 + 藏 server 小黑窗 + 預設真實模式

Makefile:
- 新增 clean-all target:clean + wails build/ + frontend build/ + server embed
- 新增 clean-build-exe / clean-build-dmg / clean-build-appimage

bootstrap-windows.ps1:
- 預設改為 clean build(每次重做 wails + server binary + frontend embed)
- 保留 vendor/ 快取避免重下 200MB+ 第三方相依
- 需要 fast path(只重跑 iscc)時設 VISIONA_FAST=1

app.go:
- configureSysProcAttr() 注入子行程,Windows 下 CREATE_NO_WINDOW 藏掉 server 小黑窗
- 覆蓋 server spawn / tar 解壓 / venv 建立 / pip install 四個關鍵點
- mockMode 預設改 false(依使用者決策 Q8,預設走真實硬體模式)
  需要強制 mock 時設環境變數 VISIONA_MOCK=1

platform_{windows,darwin,linux}.go:
- 新增 configureSysProcAttr(cmd):Windows 設 HideWindow + CREATE_NO_WINDOW,
  macOS/Linux 空實作

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-04-12 04:25:09 +08:00
parent eb52f8c690
commit 8e7b6ae435
6 changed files with 61 additions and 6 deletions

View File

@ -28,7 +28,8 @@ PAYLOAD := visiona-local/payload
stage-macos stage-windows \
wails-macos wails-windows wails-linux \
dmg exe exe-only _run-iscc appimage \
dev dev-mock test lint fmt clean
dev dev-mock test lint fmt \
clean clean-all clean-build-exe clean-build-dmg clean-build-appimage
# ── 幫助 ───────────────────────────────────────────────────────────
help: ## 列出所有 make targets
@ -551,3 +552,19 @@ clean: ## 清除 dist/ 與 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-local/build/bin
rm -rf visiona-local/build/darwin/Resources
rm -rf visiona-local/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

View File

@ -185,16 +185,25 @@ if ($msysIsccDir) {
if ($msysIsccExe) {
$bashParts += "export ISCC=`"$msysIsccExe`""
}
# 偵測前置產物是否已齊全 —— 齊全就走 fast path只重跑 iscc省 wails rebuild 幾分鐘
# Build 模式:
# VISIONA_FAST=1 → 前置產物齊全時跳過 vendor/payload/wails只重跑 isccdebug iteration 用)
# 預設 → 每次 clean buildwails build / server binary / frontend embed 全重做)
# 保留 vendor/ 快取Python runtime / wheels / ffmpeg / yt-dlp以免重下 200MB
$fastPath = (Test-Path 'visiona-local\build\bin\visiona-local.exe') -and `
(Test-Path 'payload\windows\bin\visiona-local-server.exe') -and `
(Test-Path 'payload\windows\bin\ffmpeg.exe') -and `
(Test-Path 'payload\windows\python\python.tar.gz')
if ($fastPath -and ($Target -eq 'exe' -or -not $env:VISIONA_TARGET)) {
Log 'FAST PATH前置產物齊全,跳過 vendor/payload/wails只重跑 iscc 打包'
if ($env:VISIONA_FAST -eq '1' -and $fastPath -and ($Target -eq 'exe' -or -not $env:VISIONA_TARGET)) {
Log 'FAST PATHVISIONA_FAST=1 + 前置產物齊全,跳過 vendor/payload/wails只重跑 iscc'
$bashParts += 'make exe-only'
} else {
Log '預設 clean build每次重做 wails + server binary + frontend embedvendor cache 保留)'
# 清 wails / frontend / server/web/out但不清 vendor/(避免重下 Python / wheels / ffmpeg / yt-dlp 200MB+
$bashParts += 'rm -rf visiona-local/build/bin visiona-local/build/windows/Resources'
$bashParts += 'rm -rf frontend/out frontend/.next server/web/out'
$bashParts += 'rm -rf payload/windows/bin/visiona-local-server.exe'
$bashParts += 'rm -rf dist/visiona-local-*-windows-x64.exe'
$bashParts += 'make vendor-python-windows vendor-wheels-windows vendor-ffmpeg-windows vendor-ytdlp-windows'
$bashParts += 'make payload-windows'
switch ($Target) {

View File

@ -112,9 +112,12 @@ func NewApp() *App {
mode = PythonModeAuto
}
}
// M7預設真實硬體模式使用者決策 Q8
// 若要強制 mock 模式(無 Kneron 裝置環境下 debug設環境變數 VISIONA_MOCK=1
mock := os.Getenv("VISIONA_MOCK") == "1"
return &App{
pythonMode: mode,
mockMode: true, // M1 預設 mock沒有真的 python/hardware
mockMode: mock,
}
}
@ -359,6 +362,7 @@ func (a *App) startServer() error {
// 6. spawn
cmd := exec.Command(binPath, args...)
cmd.Dir = filepath.Dir(binPath)
configureSysProcAttr(cmd) // Windows: CREATE_NO_WINDOW 藏掉 server 小黑窗
// 注入 bundle bin dir 給 server 偵測 ffmpeg / yt-dlpM6
env := os.Environ()
@ -633,6 +637,7 @@ func (a *App) ensureBundledPython() (string, error) {
// 解壓 tarballstrip-components=1 剝掉 "python/" 前綴)
fmt.Fprintln(os.Stderr, "[visiona-local] extracting bundled python runtime...")
extract := exec.Command("tar", "-xzf", pyTarball, "-C", pyHome, "--strip-components=1")
configureSysProcAttr(extract)
if out, err := extract.CombinedOutput(); err != nil {
return "", fmt.Errorf("extract python tarball: %w (%s)", err, string(out))
}
@ -646,7 +651,9 @@ func (a *App) ensureBundledPython() (string, error) {
}
fmt.Fprintln(os.Stderr, "[visiona-local] creating venv at", venvPath)
if out, err := exec.Command(embeddedPython, "-m", "venv", venvPath).CombinedOutput(); err != nil {
venvCmd := exec.Command(embeddedPython, "-m", "venv", venvPath)
configureSysProcAttr(venvCmd)
if out, err := venvCmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("create venv: %w (%s)", err, string(out))
}
@ -669,6 +676,7 @@ func (a *App) ensureBundledPython() (string, error) {
args := []string{"-m", "pip", "install", "--no-index", "--find-links", wheelsDir, "--prefer-binary"}
args = append(args, wheels...)
pipCmd := exec.Command(pythonBin, args...)
configureSysProcAttr(pipCmd)
if out, err := pipCmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("pip install wheels: %w\n%s", err, string(out))
}

View File

@ -4,9 +4,13 @@ package main
import (
"os"
"os/exec"
"path/filepath"
)
// configureSysProcAttr 在 macOS 上不需要特殊設定。
func configureSysProcAttr(_ *exec.Cmd) {}
// platformDataDir 回傳 macOS 的應用程式資料目錄。
// ~/Library/Application Support/visiona-local
func platformDataDir() string {

View File

@ -4,9 +4,13 @@ package main
import (
"os"
"os/exec"
"path/filepath"
)
// configureSysProcAttr 在 Linux 上不需要特殊設定。
func configureSysProcAttr(_ *exec.Cmd) {}
// platformDataDir 回傳 Linux 的應用程式資料目錄(遵循 XDG
// $XDG_DATA_HOME/visiona-local 或 ~/.local/share/visiona-local
func platformDataDir() string {

View File

@ -4,9 +4,22 @@ package main
import (
"os"
"os/exec"
"path/filepath"
"syscall"
)
// configureSysProcAttr 設定子行程的 Windows 特有屬性。
// CREATE_NO_WINDOW (0x08000000) 讓 server 子行程不彈 console 視窗(小黑窗)。
// HideWindow 對某些情境也一起加保險。
func configureSysProcAttr(cmd *exec.Cmd) {
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.HideWindow = true
cmd.SysProcAttr.CreationFlags |= 0x08000000 // CREATE_NO_WINDOW
}
// platformDataDir 回傳 Windows 的應用程式資料目錄。
// %APPDATA%\visiona-local
func platformDataDir() string {