jim800121chen c54f16fca0 Initial commit: visionA monorepo with local-tool subproject
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>
2026-04-11 22:10:38 +08:00

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;