feat: installer improvements + CI workflow
- Auto-generate random relay token during installation - Pre-fill relay URL and dashboard URL with EC2 defaults - Fix hardware detection duplicate device parsing - Add Dashboard URL field to relay config step - Launch Server now auto-opens dashboard URL with token - Add ad-hoc codesign to Makefile installer target - Remove binary from git tracking - Add Gitea Actions CI workflow for macOS + Windows builds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8e836e6fda
commit
bf2f01d2f3
145
.gitea/workflows/build-installer.yaml
Normal file
145
.gitea/workflows/build-installer.yaml
Normal file
@ -0,0 +1,145 @@
|
||||
name: Build Installers
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install Wails
|
||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build frontend
|
||||
run: make build-frontend build-embed
|
||||
|
||||
- name: Build server (tray-enabled)
|
||||
run: make build-server-tray
|
||||
|
||||
- name: Stage installer payload
|
||||
run: make installer-payload
|
||||
|
||||
- name: Build macOS installer
|
||||
run: |
|
||||
cd installer && wails build -clean
|
||||
codesign --force --deep --sign - build/bin/EdgeAI-Installer.app
|
||||
|
||||
- name: Package macOS installer
|
||||
run: |
|
||||
cd installer/build/bin
|
||||
ditto -c -k --sequesterRsrc --keepParent EdgeAI-Installer.app EdgeAI-Installer-macOS.zip
|
||||
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: EdgeAI-Installer-macOS
|
||||
path: installer/build/bin/EdgeAI-Installer-macOS.zip
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install Wails
|
||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd frontend && pnpm build
|
||||
xcopy /E /I /Y out ..\server\web\out
|
||||
|
||||
- name: Build server (no tray on Windows CI)
|
||||
run: |
|
||||
cd server
|
||||
$env:CGO_ENABLED="0"
|
||||
go build -tags notray -ldflags="-s -w" -o ..\dist\edge-ai-server.exe main.go
|
||||
|
||||
- name: Stage installer payload
|
||||
run: |
|
||||
Remove-Item -Recurse -Force installer\payload -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path installer\payload\data\nef\kl520
|
||||
New-Item -ItemType Directory -Force -Path installer\payload\data\nef\kl720
|
||||
New-Item -ItemType Directory -Force -Path installer\payload\scripts\firmware\KL520
|
||||
New-Item -ItemType Directory -Force -Path installer\payload\scripts\firmware\KL720
|
||||
Copy-Item dist\edge-ai-server.exe installer\payload\
|
||||
Copy-Item server\data\models.json installer\payload\data\
|
||||
Copy-Item server\data\nef\kl520\*.nef installer\payload\data\nef\kl520\
|
||||
Copy-Item server\data\nef\kl720\*.nef installer\payload\data\nef\kl720\
|
||||
Copy-Item server\scripts\kneron_bridge.py installer\payload\scripts\
|
||||
Copy-Item server\scripts\requirements.txt installer\payload\scripts\
|
||||
Copy-Item server\scripts\update_kl720_firmware.py installer\payload\scripts\
|
||||
Copy-Item scripts\kneron_detect.py installer\payload\scripts\
|
||||
Copy-Item server\scripts\firmware\KL520\*.bin installer\payload\scripts\firmware\KL520\
|
||||
Copy-Item server\scripts\firmware\KL720\*.bin installer\payload\scripts\firmware\KL720\
|
||||
|
||||
- name: Build Windows installer
|
||||
run: |
|
||||
cd installer
|
||||
wails build -clean
|
||||
|
||||
- name: Package Windows installer
|
||||
run: |
|
||||
Compress-Archive -Path installer\build\bin\EdgeAI-Installer.exe -DestinationPath installer\build\bin\EdgeAI-Installer-Windows.zip
|
||||
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: EdgeAI-Installer-Windows
|
||||
path: installer/build/bin/EdgeAI-Installer-Windows.zip
|
||||
|
||||
release:
|
||||
needs: [build-macos, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
artifacts/EdgeAI-Installer-macOS/EdgeAI-Installer-macOS.zip
|
||||
artifacts/EdgeAI-Installer-Windows/EdgeAI-Installer-Windows.zip
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@
|
||||
!/installer/payload/.gitkeep
|
||||
/installer/build/
|
||||
/installer/frontend/wailsjs/
|
||||
/installer/edge-ai-installer
|
||||
|
||||
# Test coverage
|
||||
coverage.out
|
||||
|
||||
3
Makefile
3
Makefile
@ -134,7 +134,8 @@ installer-payload: build-server-tray ## Stage payload files for GUI installer
|
||||
|
||||
installer: installer-payload ## Build GUI installer app
|
||||
cd installer && wails build -clean
|
||||
@echo "Installer built! Check installer/build/"
|
||||
codesign --force --deep --sign - installer/build/bin/EdgeAI-Installer.app
|
||||
@echo "Installer built and signed! Check installer/build/"
|
||||
|
||||
installer-dev: installer-payload ## Run GUI installer in dev mode
|
||||
cd installer && wails dev
|
||||
|
||||
2
frontend
2
frontend
@ -1 +1 @@
|
||||
Subproject commit 089f5644a73fd5ac2ecf12bc55c2c9a70b93b30f
|
||||
Subproject commit 9239a97721215a14ecac520c21e64d440dd36d98
|
||||
@ -2,7 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@ -38,6 +40,7 @@ type InstallConfig struct {
|
||||
InstallLibusb bool `json:"installLibusb"`
|
||||
RelayURL string `json:"relayURL"`
|
||||
RelayToken string `json:"relayToken"`
|
||||
DashboardURL string `json:"dashboardURL"`
|
||||
ServerPort int `json:"serverPort"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
@ -413,8 +416,9 @@ func (inst *Installer) stepWriteConfig(config InstallConfig) error {
|
||||
"host": "127.0.0.1",
|
||||
},
|
||||
"relay": map[string]interface{}{
|
||||
"url": config.RelayURL,
|
||||
"token": config.RelayToken,
|
||||
"url": config.RelayURL,
|
||||
"token": config.RelayToken,
|
||||
"dashboardURL": config.DashboardURL,
|
||||
},
|
||||
"launcher": map[string]interface{}{
|
||||
"autoStart": true,
|
||||
@ -468,21 +472,30 @@ func (inst *Installer) DetectHardware() ([]HardwareDevice, error) {
|
||||
func parseDetectOutput(output string) []HardwareDevice {
|
||||
var devices []HardwareDevice
|
||||
lines := strings.Split(output, "\n")
|
||||
var current *HardwareDevice
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.Contains(line, "KL520") || strings.Contains(line, "KL720") || strings.Contains(line, "KL730") {
|
||||
dev := HardwareDevice{}
|
||||
if strings.Contains(line, "KL520") {
|
||||
dev.Model = "KL520"
|
||||
} else if strings.Contains(line, "KL720") {
|
||||
dev.Model = "KL720"
|
||||
} else if strings.Contains(line, "KL730") {
|
||||
dev.Model = "KL730"
|
||||
if strings.HasPrefix(line, "Device #") {
|
||||
// Start a new device
|
||||
if current != nil {
|
||||
devices = append(devices, *current)
|
||||
}
|
||||
current = &HardwareDevice{}
|
||||
} else if current != nil {
|
||||
if strings.HasPrefix(line, "Model:") {
|
||||
val := strings.TrimSpace(strings.TrimPrefix(line, "Model:"))
|
||||
current.Model = strings.TrimPrefix(val, "Kneron ")
|
||||
current.Product = val
|
||||
} else if strings.HasPrefix(line, "Serial:") {
|
||||
current.Serial = strings.TrimSpace(strings.TrimPrefix(line, "Serial:"))
|
||||
} else if strings.HasPrefix(line, "Bus:") {
|
||||
current.Port = strings.TrimSpace(line)
|
||||
}
|
||||
dev.Product = line
|
||||
devices = append(devices, dev)
|
||||
}
|
||||
}
|
||||
if current != nil && current.Model != "" {
|
||||
devices = append(devices, *current)
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
@ -495,7 +508,24 @@ func (inst *Installer) LaunchServer() (string, error) {
|
||||
}
|
||||
binPath := filepath.Join(installDir, binName)
|
||||
|
||||
cmd := exec.Command(binPath, "--tray")
|
||||
// Read config to get relay args
|
||||
args := []string{"--tray"}
|
||||
cfgPath := filepath.Join(platformConfigDir(), "config.json")
|
||||
if data, err := os.ReadFile(cfgPath); err == nil {
|
||||
var cfg map[string]interface{}
|
||||
if json.Unmarshal(data, &cfg) == nil {
|
||||
if relay, ok := cfg["relay"].(map[string]interface{}); ok {
|
||||
if u, ok := relay["url"].(string); ok && u != "" {
|
||||
args = append(args, "--relay-url", u)
|
||||
}
|
||||
if t, ok := relay["token"].(string); ok && t != "" {
|
||||
args = append(args, "--relay-token", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, args...)
|
||||
cmd.Dir = installDir
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", fmt.Errorf("failed to start launcher: %w", err)
|
||||
@ -504,6 +534,35 @@ func (inst *Installer) LaunchServer() (string, error) {
|
||||
return "Launcher started", nil
|
||||
}
|
||||
|
||||
// GetDashboardURL returns the configured dashboard URL from the install config.
|
||||
func (inst *Installer) GetDashboardURL() string {
|
||||
cfgPath := filepath.Join(platformConfigDir(), "config.json")
|
||||
data, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var cfg map[string]interface{}
|
||||
if json.Unmarshal(data, &cfg) != nil {
|
||||
return ""
|
||||
}
|
||||
if relay, ok := cfg["relay"].(map[string]interface{}); ok {
|
||||
if u, ok := relay["dashboardURL"].(string); ok {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GenerateToken generates a random 16-character hex token for relay authentication.
|
||||
func (inst *Installer) GenerateToken() string {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// fallback: should never happen
|
||||
return "abcdef0123456789"
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// OpenBrowser opens the given URL in the system default browser.
|
||||
func (inst *Installer) OpenBrowser(url string) error {
|
||||
return openBrowser(url)
|
||||
|
||||
Binary file not shown.
@ -49,7 +49,11 @@ const i18n = {
|
||||
'btn.back': 'Back',
|
||||
'btn.install': 'Install',
|
||||
'btn.launch': 'Launch Server',
|
||||
'btn.openDashboard': 'Open Dashboard',
|
||||
'btn.close': 'Close',
|
||||
'relay.tokenHint': 'Auto-generated random token. Both the server and browser use this to authenticate with the relay.',
|
||||
'relay.dashboardUrl': 'Dashboard URL',
|
||||
'relay.dashboardHint': 'The HTTP URL to access the dashboard via relay. Opened after server launch.',
|
||||
'existing.detected': 'Existing installation detected',
|
||||
'existing.desc': 'An existing installation was found. You can uninstall it or install over it.',
|
||||
'existing.uninstall': 'Uninstall',
|
||||
@ -113,7 +117,11 @@ const i18n = {
|
||||
'btn.back': '上一步',
|
||||
'btn.install': '安裝',
|
||||
'btn.launch': '啟動伺服器',
|
||||
'btn.openDashboard': '開啟控制台',
|
||||
'btn.close': '關閉',
|
||||
'relay.tokenHint': '自動產生的隨機 Token。伺服器和瀏覽器都透過此 Token 向 Relay 驗證身份。',
|
||||
'relay.dashboardUrl': 'Dashboard URL',
|
||||
'relay.dashboardHint': '透過 Relay 存取 Dashboard 的 HTTP URL。啟動伺服器後會自動開啟。',
|
||||
'existing.detected': '偵測到既有安裝',
|
||||
'existing.desc': '發現既有安裝。您可以解除安裝或覆蓋安裝。',
|
||||
'existing.uninstall': '解除安裝',
|
||||
@ -142,6 +150,7 @@ let installConfig = {
|
||||
installLibusb: true,
|
||||
relayURL: '',
|
||||
relayToken: '',
|
||||
dashboardURL: '',
|
||||
serverPort: 3721,
|
||||
language: 'en',
|
||||
};
|
||||
@ -351,11 +360,43 @@ document.getElementById('btn-rescan').addEventListener('click', () => {
|
||||
document.getElementById('btn-launch').addEventListener('click', async () => {
|
||||
try {
|
||||
await window.go.main.Installer.LaunchServer();
|
||||
// After launching, if dashboard URL is configured, open it automatically
|
||||
const dashUrl = installConfig.dashboardURL || await window.go.main.Installer.GetDashboardURL();
|
||||
if (dashUrl) {
|
||||
// Give the server a moment to start, then open dashboard
|
||||
setTimeout(async () => {
|
||||
// Append relay token as query param if available
|
||||
let url = dashUrl;
|
||||
if (installConfig.relayToken) {
|
||||
const sep = url.includes('?') ? '&' : '?';
|
||||
url = url + sep + 'token=' + encodeURIComponent(installConfig.relayToken);
|
||||
}
|
||||
await window.go.main.Installer.OpenBrowser(url);
|
||||
}, 2000);
|
||||
} else {
|
||||
// No relay — open local
|
||||
setTimeout(async () => {
|
||||
const port = installConfig.serverPort || 3721;
|
||||
await window.go.main.Installer.OpenBrowser('http://127.0.0.1:' + port);
|
||||
}, 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to launch: ' + err);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-open-dashboard').addEventListener('click', async () => {
|
||||
const dashUrl = installConfig.dashboardURL || await window.go.main.Installer.GetDashboardURL();
|
||||
if (dashUrl) {
|
||||
let url = dashUrl;
|
||||
if (installConfig.relayToken) {
|
||||
const sep = url.includes('?') ? '&' : '?';
|
||||
url = url + sep + 'token=' + encodeURIComponent(installConfig.relayToken);
|
||||
}
|
||||
await window.go.main.Installer.OpenBrowser(url);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-close').addEventListener('click', () => {
|
||||
if (window.runtime && window.runtime.Quit) {
|
||||
window.runtime.Quit();
|
||||
@ -427,11 +468,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
// Step 2 Install -> Step 3 (Relay Config)
|
||||
document.getElementById('btn-install').addEventListener('click', () => {
|
||||
document.getElementById('btn-install').addEventListener('click', async () => {
|
||||
installConfig.createSymlink = document.getElementById('comp-symlink').checked;
|
||||
installConfig.installPythonEnv = document.getElementById('comp-python').checked;
|
||||
installConfig.installLibusb = document.getElementById('comp-libusb').checked;
|
||||
showStep(3);
|
||||
|
||||
// Auto-generate relay token if empty
|
||||
const tokenInput = document.getElementById('relay-token');
|
||||
if (!tokenInput.value.trim()) {
|
||||
try {
|
||||
const token = await window.go.main.Installer.GenerateToken();
|
||||
tokenInput.value = token;
|
||||
} catch (err) {
|
||||
console.error('GenerateToken failed:', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3 Back -> Step 2
|
||||
@ -439,10 +491,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showStep(2);
|
||||
});
|
||||
|
||||
// Regenerate token button
|
||||
document.getElementById('btn-regen-token').addEventListener('click', async () => {
|
||||
try {
|
||||
const token = await window.go.main.Installer.GenerateToken();
|
||||
document.getElementById('relay-token').value = token;
|
||||
} catch (err) {
|
||||
console.error('GenerateToken failed:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3 Next -> collect relay fields -> Step 4 (Progress) -> start install
|
||||
document.getElementById('btn-next-3').addEventListener('click', () => {
|
||||
installConfig.relayURL = document.getElementById('relay-url').value.trim();
|
||||
installConfig.relayToken = document.getElementById('relay-token').value.trim();
|
||||
installConfig.dashboardURL = document.getElementById('dashboard-url').value.trim();
|
||||
const portVal = parseInt(document.getElementById('server-port').value, 10);
|
||||
installConfig.serverPort = (portVal >= 1024 && portVal <= 65535) ? portVal : 3721;
|
||||
startInstall();
|
||||
@ -464,5 +527,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const luEl = document.getElementById('summary-libusb');
|
||||
luEl.textContent = installConfig.installLibusb ? t('complete.installed') : t('complete.skipped');
|
||||
luEl.className = 'info-value ' + (installConfig.installLibusb ? 'status-ok' : 'status-skipped');
|
||||
|
||||
// Show "Open Dashboard" button if relay dashboard URL is configured
|
||||
if (installConfig.dashboardURL) {
|
||||
document.getElementById('btn-open-dashboard').style.display = 'inline-flex';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -155,12 +155,22 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="field-label" data-i18n="relay.url">Relay URL</label>
|
||||
<input type="text" class="input-field" id="relay-url" placeholder="ws://relay.example.com/tunnel/connect">
|
||||
<input type="text" class="input-field" id="relay-url" value="ws://ec2-13-192-170-7.ap-northeast-1.compute.amazonaws.com/tunnel/connect">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="field-label" data-i18n="relay.token">Relay Token</label>
|
||||
<input type="text" class="input-field" id="relay-token" placeholder="your-relay-token">
|
||||
<div class="path-input-group">
|
||||
<input type="text" class="input-field" id="relay-token" placeholder="auto-generated" readonly>
|
||||
<button id="btn-regen-token" class="btn btn-secondary" title="Regenerate">↻</button>
|
||||
</div>
|
||||
<p class="field-hint" data-i18n="relay.tokenHint">Auto-generated random token. Both the server and browser use this to authenticate with the relay.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="field-label" data-i18n="relay.dashboardUrl">Dashboard URL</label>
|
||||
<input type="text" class="input-field" id="dashboard-url" value="http://ec2-13-192-170-7.ap-northeast-1.compute.amazonaws.com">
|
||||
<p class="field-hint" data-i18n="relay.dashboardHint">The HTTP URL to access the dashboard via relay. Opened after server launch.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -234,6 +244,7 @@
|
||||
|
||||
<div class="actions">
|
||||
<button id="btn-launch" class="btn btn-primary" data-i18n="btn.launch">Launch Server</button>
|
||||
<button id="btn-open-dashboard" class="btn btn-secondary" style="display:none" data-i18n="btn.openDashboard">Open Dashboard</button>
|
||||
<button id="btn-close" class="btn btn-ghost" data-i18n="btn.close">Close</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user