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

@@ -1,3 +1,4 @@
import type { Server } from 'node:http';
import { app, BrowserWindow, ipcMain } from 'electron'
import { CONFIG_KEYS, IPC_EVENTS } from '@runtime/lib/constants'
import { setupMainWindow } from './wins';
@@ -12,10 +13,13 @@ import { appUpdater } from '@electron/service/updater';
import axios from 'axios';
import { onProviderChange } from '@electron/service/provider-api-service';
import { gatewayManager } from '@electron/gateway/manager';
import { dispatchLocalHostApi } from '@electron/api/router';
import { createHostApiContext, dispatchLocalHostApi } from '@electron/api/router';
import { hostEventBus } from '@electron/api/event-bus';
import { getHostApiBase, getHostApiToken, startHostApiServer } from '@electron/api/server';
import { syncProviderRuntimeSnapshot } from '@electron/service/provider-runtime-sync';
import { applyLaunchAtStartupSetting, syncLaunchAtStartupSettingFromConfig } from '@electron/service/launch-at-startup';
import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled } from '@electron/utils/skill-config';
import { initTelemetry, shutdownTelemetry } from '@electron/utils/telemetry';
import { syncGatewayConfigBeforeLaunch } from '@electron/gateway/config-sync';
// 初始化 updater确保在 app ready 之前或者之中注册好 IPC
@@ -26,6 +30,19 @@ appUpdater.init();
const HOST_API_BASE_URL = process.env['ZN_AI_HOST_API_BASE_URL']
|| process.env['VITE_SERVICE_URL']
|| 'http://8.138.234.141/ingress';
const GATEWAY_QUIT_TIMEOUT_MS = 5_000;
let gatewayEventBridgeBound = false;
let gatewayQuitCleanupInProgress = false;
let gatewayQuitCleanupCompleted = false;
let hostApiServer: Server | null = null;
type HostApiProxyRequest = {
path: string;
method?: string;
headers?: Record<string, string>;
body?: unknown;
};
function refreshProviderRuntime(): { warnings: string[] } {
try {
@@ -54,6 +71,7 @@ async function requestUpstreamHostApi(path: string, method: string, headers: Rec
return {
success: true,
ok: true,
status: response.status,
json: response.data,
data: response.data,
};
@@ -76,20 +94,187 @@ async function requestUpstreamHostApi(path: string, method: string, headers: Rec
}
}
ipcMain.handle(IPC_EVENTS.HOST_API_FETCH, async (_event, { path, method, headers, body }) => {
const normalizedMethod = method || 'GET';
async function closeHostApiServer(): Promise<void> {
if (!hostApiServer) {
return;
}
const server = hostApiServer;
hostApiServer = null;
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
function normalizeProxyBody(body: unknown): string | undefined {
if (body == null) {
return undefined;
}
if (typeof body === 'string') {
return body;
}
return JSON.stringify(body);
}
async function proxyHostApiRequest(request: HostApiProxyRequest) {
const path = typeof request.path === 'string' ? request.path : '';
if (!path || !path.startsWith('/')) {
return {
success: false,
ok: false,
status: 400,
error: `Invalid host API path: ${String(request.path)}`,
};
}
const hostApiToken = getHostApiToken();
if (!hostApiServer || !hostApiToken) {
const localResult = await dispatchLocalHostApi(request);
if (localResult) {
return localResult;
}
return requestUpstreamHostApi(
path,
request.method || 'GET',
request.headers,
request.body,
);
}
const method = (request.method || 'GET').toUpperCase();
const headers: Record<string, string> = {
...(request.headers || {}),
'X-Host-Api-Token': hostApiToken,
};
const body = normalizeProxyBody(request.body);
if (body !== undefined && !headers['Content-Type'] && !headers['content-type']) {
headers['Content-Type'] = 'application/json';
}
try {
const response = await fetch(`${getHostApiBase()}${path}`, {
method,
headers,
body,
});
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return await response.json();
}
const text = await response.text();
return {
success: response.ok,
ok: response.ok,
status: response.status,
text,
...(response.ok ? {} : { error: text || response.statusText }),
};
} catch (error) {
return {
success: false,
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function emitGatewayRendererEvent(channel: string, payload: unknown): void {
BrowserWindow.getAllWindows().forEach((window) => {
if (!window.isDestroyed()) {
window.webContents.send(channel, payload);
}
});
}
function bindGatewayLifecycleEvents(): void {
if (gatewayEventBridgeBound) {
return;
}
gatewayEventBridgeBound = true;
gatewayManager.on('status', (status) => {
hostEventBus.emit('gateway:status', status);
emitGatewayRendererEvent('gateway:status-changed', status);
});
gatewayManager.on('message', (message) => {
hostEventBus.emit('gateway:message', message);
emitGatewayRendererEvent('gateway:message', message);
});
gatewayManager.on('notification', (notification) => {
hostEventBus.emit('gateway:notification', notification);
emitGatewayRendererEvent('gateway:notification', notification);
});
gatewayManager.on('channel:status', (data) => {
hostEventBus.emit('gateway:channel-status', data);
emitGatewayRendererEvent('gateway:channel-status', data);
});
gatewayManager.on('chat:message', (data) => {
hostEventBus.emit('gateway:chat-message', data);
emitGatewayRendererEvent('gateway:chat-message', data);
});
gatewayManager.on('exit', (code) => {
hostEventBus.emit('gateway:exit', { code });
emitGatewayRendererEvent('gateway:exit', code);
});
gatewayManager.on('error', (error) => {
hostEventBus.emit('gateway:error', { message: error.message });
emitGatewayRendererEvent('gateway:error', error.message);
});
}
function requestQuitOnSignal(signal: NodeJS.Signals): void {
log.info(`Received ${signal}; requesting app quit`);
app.quit();
}
function emergencyGatewayCleanup(reason: string, error: unknown): void {
log.error(`${reason}:`, error);
hostEventBus.closeAll();
void closeHostApiServer().catch(() => {
// ignore host API server close failures during emergency cleanup
});
try {
void gatewayManager.stop().catch(() => {
// ignore stop failures during emergency cleanup
});
} catch {
// ignore stop invocation failures if state is corrupted
}
setTimeout(() => {
void shutdownTelemetry().catch(() => {
// ignore telemetry flush failures during crash shutdown
});
void gatewayManager.forceTerminateOwnedProcessForQuit().catch(() => {
// ignore forced termination failures during crash shutdown
}).finally(() => {
process.exit(1);
});
}, 3_000).unref();
}
ipcMain.handle(IPC_EVENTS.HOST_API_TOKEN, async () => getHostApiToken());
ipcMain.handle(IPC_EVENTS.HOST_API_FETCH, async (_event, request: HostApiProxyRequest) => {
return proxyHostApiRequest({ ...request, method: request.method || 'GET' });
// 1. 优先本地处理 Host API 路由(逐步对齐 ClawX
const localResult = await dispatchLocalHostApi({
path,
method: normalizedMethod,
headers,
body,
});
if (localResult) return localResult;
// 2. 其余接口代理到远端后端
return await requestUpstreamHostApi(path, normalizedMethod, headers, body);
});
// Gateway RPC IPC handler
@@ -113,10 +298,97 @@ if (started) {
// logManager.error('unhandledRejection', reason, promise);
// });
process.once('SIGINT', () => requestQuitOnSignal('SIGINT'));
process.once('SIGTERM', () => requestQuitOnSignal('SIGTERM'));
process.on('uncaughtException', (error) => {
emergencyGatewayCleanup('Uncaught exception in main process', error);
});
process.on('unhandledRejection', (reason) => {
emergencyGatewayCleanup('Unhandled promise rejection in main process', reason);
});
app.on('before-quit', (event) => {
if (gatewayQuitCleanupCompleted) {
return;
}
event.preventDefault();
if (gatewayQuitCleanupInProgress) {
return;
}
gatewayQuitCleanupInProgress = true;
hostEventBus.closeAll();
const closeServerPromise = closeHostApiServer().catch((error) => {
log.warn('Host API server close failed during quit:', error);
});
const stopPromise = Promise.all([
closeServerPromise,
gatewayManager.stop(),
]).catch((error) => {
log.warn('gatewayManager.stop() error during quit:', error);
});
const timeoutPromise = new Promise<'timeout'>((resolve) => {
setTimeout(() => resolve('timeout'), GATEWAY_QUIT_TIMEOUT_MS);
});
void Promise.race([
stopPromise.then(() => 'stopped' as const),
timeoutPromise,
]).then(async (result) => {
if (result === 'timeout') {
log.warn('Gateway shutdown timed out during app quit; proceeding with forced quit');
try {
const terminated = await gatewayManager.forceTerminateOwnedProcessForQuit();
if (terminated) {
log.warn('Forced gateway process termination completed after quit timeout');
}
} catch (error) {
log.warn('Forced gateway termination failed after quit timeout:', error);
}
}
try {
await shutdownTelemetry();
} catch (error) {
log.warn('Telemetry shutdown failed during app quit:', error);
}
gatewayQuitCleanupCompleted = true;
app.quit();
}).catch((error) => {
gatewayQuitCleanupInProgress = false;
log.warn('Gateway quit cleanup failed:', error);
gatewayQuitCleanupCompleted = true;
app.quit();
});
});
app.whenReady().then(async () => {
await configManager.init();
await syncLaunchAtStartupSettingFromConfig();
await themeManager.init();
await initTelemetry();
bindGatewayLifecycleEvents();
try {
hostApiServer = startHostApiServer({
ctx: createHostApiContext(),
dispatchRequest: dispatchLocalHostApi,
fallbackRequest: async (request) => {
return requestUpstreamHostApi(
request.path,
request.method || 'GET',
request.headers,
request.body,
);
},
});
} catch (error) {
log.error('Failed to start Host API server:', error);
}
let launchAtStartup = Boolean(configManager.get(CONFIG_KEYS.LAUNCH_AT_STARTUP));
const stopLaunchAtStartupSync = configManager.onConfigChange((config) => {
@@ -131,6 +403,9 @@ app.whenReady().then(async () => {
app.once('will-quit', () => {
stopLaunchAtStartupSync();
void closeHostApiServer().catch(() => {
// ignore host API server close failures during final teardown
});
});
void ensureBuiltinSkillsInstalled().catch((error) => {