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,53 +1,196 @@
import { utilityProcess } from 'electron';
import { spawn } from 'node:child_process';
import { existsSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { app, utilityProcess } from 'electron';
import logManager from '@electron/service/logger';
import { appendNodeRequireToNodeOptions } from '@electron/utils/paths';
import type { GatewayLaunchContext } from './config-sync';
import { resolveGatewayLaunchStrategy, type GatewayLaunchStrategy } from './launch-strategy';
import type { GatewayProcessHandle } from './process-handle';
export interface OpenClawGatewayLaunchContext {
port: number;
token: string;
openclawDir: string;
entryScript: string;
const GATEWAY_PRELOAD_SOURCE = `'use strict';
(function () {
if (process.platform !== 'win32') return;
try {
var cp = require('child_process');
if (cp.__znAiGatewayPatched) return;
cp.__znAiGatewayPatched = true;
['spawn', 'exec', 'execFile', 'fork', 'spawnSync', 'execSync', 'execFileSync'].forEach(function (method) {
var original = cp[method];
if (typeof original !== 'function') return;
cp[method] = function () {
var args = Array.prototype.slice.call(arguments);
var optIdx = -1;
for (var i = 1; i < args.length; i++) {
var candidate = args[i];
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
optIdx = i;
break;
}
}
if (optIdx >= 0) {
args[optIdx].windowsHide = true;
} else {
var opts = { windowsHide: true };
if (typeof args[args.length - 1] === 'function') {
args.splice(args.length - 1, 0, opts);
} else {
args.push(opts);
}
}
return original.apply(this, args);
};
});
} catch {
// best-effort
}
})();
`;
function ensureGatewayPreload(): string {
const destination = path.join(app.getPath('userData'), 'gateway-preload.cjs');
try {
writeFileSync(destination, GATEWAY_PRELOAD_SOURCE, 'utf-8');
} catch {
// best-effort
}
return destination;
}
export async function launchGatewayProcess(
context: OpenClawGatewayLaunchContext,
): Promise<Electron.UtilityProcess> {
const gatewayArgs = [
'gateway',
'--port',
String(context.port),
'--token',
context.token,
'--allow-unconfigured',
];
export interface LaunchGatewayProcessOptions {
port: number;
launchContext: GatewayLaunchContext;
sanitizeSpawnArgs?: (args: string[]) => string[];
onStdoutLine?: (line: string) => void;
onStderrLine?: (line: string) => void;
onSpawn?: (pid: number | undefined) => void;
onExit?: (child: GatewayProcessHandle, code: number | null) => void;
onError?: (error: Error) => void;
}
const env: NodeJS.ProcessEnv = {
...process.env,
OPENCLAW_GATEWAY_TOKEN: context.token,
OPENCLAW_SKIP_CHANNELS: '1',
OPENCLAW_NO_RESPAWN: '1',
};
function emitProcessOutput(
child: GatewayProcessHandle,
stream: 'stdout' | 'stderr',
onLine: ((line: string) => void) | undefined,
): void {
const readable = child[stream];
if (!readable) {
return;
}
logManager.info('Starting OpenClaw Gateway process', {
port: context.port,
entryScript: context.entryScript,
cwd: context.openclawDir,
args: gatewayArgs,
readable.on('data', (data) => {
const raw = data.toString();
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
if (onLine) {
onLine(trimmed);
continue;
}
if (stream === 'stdout') {
logManager.debug(`[OpenClaw stdout] ${trimmed}`);
continue;
}
logManager.warn(`[OpenClaw] ${trimmed}`);
}
});
}
return await new Promise<Electron.UtilityProcess>((resolve, reject) => {
const child = utilityProcess.fork(context.entryScript, gatewayArgs, {
cwd: context.openclawDir,
stdio: 'pipe',
env,
serviceName: 'OpenClaw Gateway',
});
function buildGatewayRuntimeEnv(
launchContext: GatewayLaunchContext,
): Record<string, string | undefined> {
const runtimeEnv = { ...launchContext.forkEnv };
if (!app.isPackaged) {
try {
const preloadPath = ensureGatewayPreload();
if (existsSync(preloadPath)) {
runtimeEnv.NODE_OPTIONS = appendNodeRequireToNodeOptions(
runtimeEnv.NODE_OPTIONS,
preloadPath,
);
}
} catch (error) {
logManager.warn('Failed to prepare Gateway preload', error);
}
}
return runtimeEnv;
}
function resolvePreferredLaunchStrategy(
launchContext: GatewayLaunchContext,
): GatewayLaunchStrategy {
return resolveGatewayLaunchStrategy({
platform: process.platform,
mode: launchContext.mode,
forced: process.env.ZN_AI_GATEWAY_LAUNCH_STRATEGY,
});
}
function resolveNodeRuntimeHost(options: {
launchContext: GatewayLaunchContext;
runtimeEnv: Record<string, string | undefined>;
}): {
command: string;
env: NodeJS.ProcessEnv;
hostKind: 'bundled-node' | 'electron-run-as-node';
} {
const bundledNodePath = process.platform === 'win32'
? path.join(options.launchContext.binDir, 'node.exe')
: '';
if (bundledNodePath && existsSync(bundledNodePath)) {
const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...envWithoutElectronNode } = options.runtimeEnv;
return {
command: bundledNodePath,
env: envWithoutElectronNode as NodeJS.ProcessEnv,
hostKind: 'bundled-node',
};
}
return {
command: process.execPath,
env: {
...options.runtimeEnv,
ELECTRON_RUN_AS_NODE: '1',
} as NodeJS.ProcessEnv,
hostKind: 'electron-run-as-node',
};
}
async function launchWithUtilityProcess(options: {
launchContext: GatewayLaunchContext;
runtimeEnv: Record<string, string | undefined>;
lastSpawnSummary: string;
onStdoutLine?: (line: string) => void;
onStderrLine?: (line: string) => void;
onSpawn?: (pid: number | undefined) => void;
onExit?: (child: GatewayProcessHandle, code: number | null) => void;
onError?: (error: Error) => void;
}): Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }> {
return await new Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }>((resolve, reject) => {
const child = utilityProcess.fork(
options.launchContext.entryScript,
options.launchContext.gatewayArgs,
{
cwd: options.launchContext.openclawDir,
stdio: 'pipe',
env: options.runtimeEnv as NodeJS.ProcessEnv,
serviceName: 'OpenClaw Gateway',
},
) as GatewayProcessHandle;
let settled = false;
const resolveOnce = () => {
if (settled) return;
settled = true;
resolve(child);
resolve({ child, lastSpawnSummary: options.lastSpawnSummary });
};
const rejectOnce = (error: Error) => {
@@ -57,29 +200,135 @@ export async function launchGatewayProcess(
};
child.once('spawn', () => {
logManager.info('OpenClaw Gateway process spawned', { pid: child.pid });
logManager.info('OpenClaw Gateway process spawned', {
pid: child.pid,
launchStrategy: 'utility-process',
});
options.onSpawn?.(child.pid);
resolveOnce();
});
child.once('error', (error) => {
logManager.error('OpenClaw Gateway process spawn error:', error);
options.onError?.(error);
rejectOnce(error);
});
child.once('exit', (code) => {
options.onExit?.(child, code ?? null);
if (!settled) {
rejectOnce(new Error(`OpenClaw Gateway exited before spawn completed (code=${code ?? 'unknown'})`));
}
});
child.stderr?.on('data', (data) => {
const raw = data.toString();
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (trimmed) {
logManager.warn(`[OpenClaw] ${trimmed}`);
}
}
});
emitProcessOutput(child, 'stdout', options.onStdoutLine);
emitProcessOutput(child, 'stderr', options.onStderrLine);
});
}
async function launchWithNodeRuntime(options: {
launchContext: GatewayLaunchContext;
runtimeEnv: Record<string, string | undefined>;
lastSpawnSummary: string;
onStdoutLine?: (line: string) => void;
onStderrLine?: (line: string) => void;
onSpawn?: (pid: number | undefined) => void;
onExit?: (child: GatewayProcessHandle, code: number | null) => void;
onError?: (error: Error) => void;
}): Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }> {
const runtimeHost = resolveNodeRuntimeHost({
launchContext: options.launchContext,
runtimeEnv: options.runtimeEnv,
});
return await new Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }>((resolve, reject) => {
const child = spawn(
runtimeHost.command,
[options.launchContext.entryScript, ...options.launchContext.gatewayArgs],
{
cwd: options.launchContext.openclawDir,
env: runtimeHost.env,
stdio: 'pipe',
windowsHide: true,
},
) as GatewayProcessHandle;
let settled = false;
const resolveOnce = () => {
if (settled) return;
settled = true;
resolve({ child, lastSpawnSummary: options.lastSpawnSummary });
};
const rejectOnce = (error: Error) => {
if (settled) return;
settled = true;
reject(error);
};
child.once('spawn', () => {
logManager.info('OpenClaw Gateway process spawned', {
pid: child.pid,
launchStrategy: 'node-runtime',
runtimeHost: runtimeHost.hostKind,
execPath: runtimeHost.command,
});
options.onSpawn?.(child.pid);
resolveOnce();
});
child.once('error', (error) => {
logManager.error('OpenClaw Gateway process spawn error:', error);
options.onError?.(error);
rejectOnce(error);
});
child.once('exit', (code) => {
options.onExit?.(child, code ?? null);
if (!settled) {
rejectOnce(new Error(`OpenClaw Gateway exited before spawn completed (code=${code ?? 'unknown'})`));
}
});
emitProcessOutput(child, 'stdout', options.onStdoutLine);
emitProcessOutput(child, 'stderr', options.onStderrLine);
});
}
export async function launchGatewayProcess(
options: LaunchGatewayProcessOptions,
): Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }> {
const { launchContext } = options;
const sanitizedArgs = options.sanitizeSpawnArgs?.(launchContext.gatewayArgs) ?? launchContext.gatewayArgs;
const launchStrategy = resolvePreferredLaunchStrategy(launchContext);
const lastSpawnSummary = `mode=${launchContext.mode}, launcher="${launchStrategy}", entry="${launchContext.entryScript}", args="${sanitizedArgs.join(' ')}", cwd="${launchContext.openclawDir}"`;
const runtimeEnv = buildGatewayRuntimeEnv(launchContext);
logManager.info('Starting OpenClaw Gateway process', {
port: options.port,
entryScript: launchContext.entryScript,
cwd: launchContext.openclawDir,
args: sanitizedArgs,
mode: launchContext.mode,
bundledBin: launchContext.binPathExists,
launchStrategy,
});
const launchOptions = {
launchContext,
runtimeEnv,
lastSpawnSummary,
onStdoutLine: options.onStdoutLine,
onStderrLine: options.onStderrLine,
onSpawn: options.onSpawn,
onExit: options.onExit,
onError: options.onError,
};
if (launchStrategy === 'node-runtime') {
return await launchWithNodeRuntime(launchOptions);
}
return await launchWithUtilityProcess(launchOptions);
}