feat: implement telemetry system for application usage tracking

- Added telemetry utility to capture application events and metrics.
- Integrated PostHog for event tracking with distinct user identification.
- Implemented telemetry initialization, event capturing, and shutdown procedures.

feat: add UV environment setup for Python management

- Created utilities to manage Python installation and configuration.
- Implemented network optimization checks for Python installation mirrors.
- Added functions to set up managed Python environments with error handling.

feat: enhance host API communication with token management

- Introduced host API token retrieval and management for secure requests.
- Updated host API fetch functions to include token in headers.
- Added support for creating event sources with authentication.

test: add comprehensive tests for gateway protocol and startup helpers

- Implemented unit tests for gateway protocol helpers, event dispatching, and state management.
- Added tests for startup recovery strategies and process policies.
- Ensured coverage for connection monitoring and restart governance logic.
This commit is contained in:
DEV_DSW
2026-04-23 17:21:57 +08:00
parent 655e7c51d2
commit 71bcc3b3c5
39 changed files with 5504 additions and 313 deletions

View File

@@ -4,11 +4,16 @@ import { logout, readPersistedAuthToken } from '../router/auth-session';
type RequestInitLike = Pick<RequestInit, 'method' | 'headers' | 'body'>;
const HOST_API_PORT = 13210;
const HOST_API_BASE = `http://127.0.0.1:${HOST_API_PORT}`;
type LooseIpcBridge = {
invoke<T = unknown>(channel: string, ...args: any[]): Promise<T>;
on?(channel: string, callback: (...args: any[]) => void): () => void;
};
let cachedHostApiToken: string | null = null;
function normalizeHeaders(headers?: HeadersInit): Headers {
return new Headers(headers ?? {});
}
@@ -67,6 +72,58 @@ function handleUnauthorized(): void {
logout({ reason: 'unauthorized', from });
}
async function getHostApiToken(): Promise<string> {
if (cachedHostApiToken) {
return cachedHostApiToken;
}
cachedHostApiToken = await invokeIpc<string>(IPC_EVENTS.HOST_API_TOKEN);
return cachedHostApiToken;
}
async function fetchViaLocalHostApi<T>(
path: string,
method: string,
headers: Headers,
body: BodyInit | null | undefined,
): Promise<T> {
const hostApiToken = await getHostApiToken();
const localHeaders = new Headers(headers);
if (hostApiToken && !localHeaders.has('X-Host-Api-Token')) {
localHeaders.set('X-Host-Api-Token', hostApiToken);
}
if (body != null && !localHeaders.has('Content-Type')) {
localHeaders.set('Content-Type', 'application/json');
}
const response = await fetch(`${HOST_API_BASE}${path}`, {
method,
headers: localHeaders,
body,
});
if (!response.ok) {
if (isUnauthorizedStatus(response.status)) {
handleUnauthorized();
}
const text = await response.text();
if (!isUnauthorizedStatus(response.status) && isUnauthorizedMessage(text)) {
handleUnauthorized();
}
throw new Error(text || response.statusText || `Request failed with ${response.status}`);
}
const contentType = response.headers.get('content-type') ?? '';
if (response.status === 204) {
return undefined as T;
}
if (contentType.includes('application/json')) {
return (await response.json()) as T;
}
return (await response.text()) as unknown as T;
}
export function hasHostApiBridge(): boolean {
return typeof window !== 'undefined' && Boolean(window.api?.invoke);
}
@@ -89,6 +146,15 @@ export function onIpc(channel: string, callback: (...args: any[]) => void): () =
return bridge.on ? bridge.on(channel, callback) : () => {};
}
function shouldFallbackToBrowser(message: string): boolean {
const normalized = message.toLowerCase();
return normalized.includes('invalid ipc channel: hostapi:fetch')
|| normalized.includes("no handler registered for 'hostapi:fetch'")
|| normalized.includes('no handler registered for "hostapi:fetch"')
|| normalized.includes('no handler registered for hostapi:fetch')
|| normalized.includes('window is not defined');
}
export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Promise<T> {
const method = init?.method ?? 'GET';
const headers = normalizeHeaders(init?.headers);
@@ -112,8 +178,17 @@ export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Pro
};
if (hasHostApiBridge()) {
const response = await invokeIpc(IPC_EVENTS.HOST_API_FETCH, request);
return extractResult<T>(response);
try {
const response = await invokeIpc(IPC_EVENTS.HOST_API_FETCH, request);
return extractResult<T>(response);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!shouldFallbackToBrowser(message)) {
throw error;
}
return fetchViaLocalHostApi<T>(path, method, headers, normalizeBody(init?.body));
}
}
if (typeof fetch === 'function') {
@@ -135,6 +210,9 @@ export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Pro
}
const contentType = response.headers.get('content-type') ?? '';
if (response.status === 204) {
return undefined as T;
}
if (contentType.includes('application/json')) {
return (await response.json()) as T;
}
@@ -144,3 +222,13 @@ export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Pro
throw new Error(`No HTTP bridge available for ${path}`);
}
export async function createHostEventSource(path = '/api/events'): Promise<EventSource> {
const token = await getHostApiToken();
const separator = path.includes('?') ? '&' : '?';
return new EventSource(`${HOST_API_BASE}${path}${separator}token=${encodeURIComponent(token)}`);
}
export function getHostApiBase(): string {
return HOST_API_BASE;
}