local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
// In production (embedded in Go binary), the frontend is served from the same origin as the API.
|
|
// In dev mode, Next.js runs on :3000 and Go on :3721, so we need explicit addresses.
|
|
// When deployed to cloud (e.g. AWS), the user can set a custom backend URL in Settings.
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
|
|
const BACKEND_URL_STORAGE_KEY = 'edge-ai-backend-url';
|
|
const RELAY_TOKEN_STORAGE_KEY = 'edge-ai-relay-token';
|
|
|
|
function getStoredBackendUrl(): string {
|
|
if (typeof window === 'undefined') return '';
|
|
return localStorage.getItem(BACKEND_URL_STORAGE_KEY) || '';
|
|
}
|
|
|
|
export function setBackendUrl(url: string): void {
|
|
if (typeof window === 'undefined') return;
|
|
if (url) {
|
|
localStorage.setItem(BACKEND_URL_STORAGE_KEY, url.replace(/\/+$/, ''));
|
|
} else {
|
|
localStorage.removeItem(BACKEND_URL_STORAGE_KEY);
|
|
}
|
|
}
|
|
|
|
export function getBackendUrl(): string {
|
|
if (isDev) return 'http://localhost:3721';
|
|
return getStoredBackendUrl();
|
|
}
|
|
|
|
export function getApiBaseUrl(): string {
|
|
// In dev mode, use relative path so Next.js rewrite proxy handles it
|
|
if (isDev) return '/api';
|
|
const backend = getStoredBackendUrl();
|
|
return backend ? `${backend}/api` : '/api';
|
|
}
|
|
|
|
export function getWsBaseUrl(): string {
|
|
if (typeof window === 'undefined') return '';
|
|
if (isDev) return 'ws://localhost:3721';
|
|
const backend = getStoredBackendUrl();
|
|
if (backend) {
|
|
return backend.replace(/^http/, 'ws');
|
|
}
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
return `${protocol}//${window.location.host}`;
|
|
}
|
|
|
|
// --- Relay Token ---
|
|
|
|
export function getRelayToken(): string {
|
|
if (typeof window === 'undefined') return '';
|
|
return localStorage.getItem(RELAY_TOKEN_STORAGE_KEY) || '';
|
|
}
|
|
|
|
export function setRelayToken(token: string): void {
|
|
if (typeof window === 'undefined') return;
|
|
if (token) {
|
|
localStorage.setItem(RELAY_TOKEN_STORAGE_KEY, token);
|
|
} else {
|
|
localStorage.removeItem(RELAY_TOKEN_STORAGE_KEY);
|
|
}
|
|
}
|
|
|
|
// Check URL query params for a relay token (e.g. ?token=abc123).
|
|
// If found, cache it in localStorage and remove the param from the URL.
|
|
// This is the primary mechanism: local server opens the browser with the
|
|
// token embedded in the URL, so no cross-origin fetch is needed.
|
|
export function syncRelayTokenFromURL(): string {
|
|
if (typeof window === 'undefined') return '';
|
|
const params = new URLSearchParams(window.location.search);
|
|
const token = params.get('token');
|
|
if (token) {
|
|
setRelayToken(token);
|
|
// Clean up URL — remove token param without reload
|
|
params.delete('token');
|
|
const newSearch = params.toString();
|
|
const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : '') + window.location.hash;
|
|
window.history.replaceState({}, '', newUrl);
|
|
return token;
|
|
}
|
|
return getRelayToken();
|
|
}
|
|
|
|
// Kept for backward compatibility — now simply returns cached token.
|
|
export async function fetchAndCacheRelayToken(): Promise<string> {
|
|
return syncRelayTokenFromURL();
|
|
}
|
|
|
|
// Append relay token as query parameter to a URL. Used for resources loaded
|
|
// via <img src> or other HTML elements that cannot send custom headers.
|
|
export function appendRelayToken(url: string): string {
|
|
const token = getRelayToken();
|
|
if (!token) return url;
|
|
const sep = url.includes('?') ? '&' : '?';
|
|
return `${url}${sep}token=${encodeURIComponent(token)}`;
|
|
}
|
|
|
|
export const ROUTES = {
|
|
HOME: '/',
|
|
MODELS: '/models',
|
|
MODEL_DETAIL: (id: string) => `/models/${id}`,
|
|
DEVICES: '/devices',
|
|
DEVICE_DETAIL: (id: string) => `/devices/${id}`,
|
|
WORKSPACE: (deviceId: string) => `/workspace/${deviceId}`,
|
|
} as const;
|
|
|
|
export const TASK_TYPES = {
|
|
object_detection: 'Object Detection',
|
|
classification: 'Classification',
|
|
} as const;
|
|
|
|
export const HARDWARE_OPTIONS = ['KL520', 'KL720', 'KL730'] as const;
|