feat: add uv mirror detection and python bootstrap (#24)
Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
This commit is contained in:
@@ -20,6 +20,8 @@ import { getApiKey } from '../utils/secure-storage';
|
|||||||
import { getProviderEnvVar } from '../utils/openclaw-auth';
|
import { getProviderEnvVar } from '../utils/openclaw-auth';
|
||||||
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
|
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { getUvMirrorEnv } from '../utils/uv-env';
|
||||||
|
import { isPythonReady, setupManagedPython } from '../utils/uv-setup';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gateway connection status
|
* Gateway connection status
|
||||||
@@ -149,6 +151,17 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.setStatus({ state: 'starting', reconnectAttempts: 0 });
|
this.setStatus({ state: 'starting', reconnectAttempts: 0 });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if Python environment is ready (self-healing)
|
||||||
|
const pythonReady = await isPythonReady();
|
||||||
|
if (!pythonReady) {
|
||||||
|
logger.info('Python environment missing or incomplete, attempting background repair...');
|
||||||
|
// We don't await this to avoid blocking Gateway startup,
|
||||||
|
// as uv run will handle it if needed, but this pre-warms it.
|
||||||
|
void setupManagedPython().catch(err => {
|
||||||
|
logger.error('Background Python repair failed:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check if Gateway is already running
|
// Check if Gateway is already running
|
||||||
logger.info('Checking for existing Gateway...');
|
logger.info('Checking for existing Gateway...');
|
||||||
const existing = await this.findExistingGateway();
|
const existing = await this.findExistingGateway();
|
||||||
@@ -470,12 +483,15 @@ export class GatewayManager extends EventEmitter {
|
|||||||
logger.warn(`Failed to load API key for ${providerType}:`, err);
|
logger.warn(`Failed to load API key for ${providerType}:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uvEnv = await getUvMirrorEnv();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const spawnEnv: Record<string, string | undefined> = {
|
const spawnEnv: Record<string, string | undefined> = {
|
||||||
...process.env,
|
...process.env,
|
||||||
PATH: finalPath,
|
PATH: finalPath,
|
||||||
...providerEnv,
|
...providerEnv,
|
||||||
|
...uvEnv,
|
||||||
OPENCLAW_GATEWAY_TOKEN: gatewayToken,
|
OPENCLAW_GATEWAY_TOKEN: gatewayToken,
|
||||||
OPENCLAW_SKIP_CHANNELS: '',
|
OPENCLAW_SKIP_CHANNELS: '',
|
||||||
CLAWDBOT_SKIP_CHANNELS: '',
|
CLAWDBOT_SKIP_CHANNELS: '',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { createMenu } from './menu';
|
|||||||
|
|
||||||
import { appUpdater, registerUpdateHandlers } from './updater';
|
import { appUpdater, registerUpdateHandlers } from './updater';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { warmupNetworkOptimization } from '../utils/uv-env';
|
||||||
|
|
||||||
import { ClawHubService } from '../gateway/clawhub';
|
import { ClawHubService } from '../gateway/clawhub';
|
||||||
|
|
||||||
@@ -111,6 +112,9 @@ async function initialize(): Promise<void> {
|
|||||||
logger.info(`Resources path: ${process.resourcesPath}`);
|
logger.info(`Resources path: ${process.resourcesPath}`);
|
||||||
logger.info(`Exec path: ${process.execPath}`);
|
logger.info(`Exec path: ${process.execPath}`);
|
||||||
|
|
||||||
|
// Warm up network optimization (non-blocking)
|
||||||
|
void warmupNetworkOptimization();
|
||||||
|
|
||||||
// Set application menu
|
// Set application menu
|
||||||
createMenu();
|
createMenu();
|
||||||
|
|
||||||
|
|||||||
120
electron/utils/uv-env.ts
Normal file
120
electron/utils/uv-env.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
import { request } from 'https';
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
const UV_MIRROR_ENV: Record<string, string> = {
|
||||||
|
UV_PYTHON_INSTALL_MIRROR: 'https://registry.npmmirror.com/-/binary/python-build-standalone/',
|
||||||
|
UV_INDEX_URL: 'https://pypi.tuna.tsinghua.edu.cn/simple/',
|
||||||
|
};
|
||||||
|
|
||||||
|
const GOOGLE_204_HOST = 'www.google.com';
|
||||||
|
const GOOGLE_204_PATH = '/generate_204';
|
||||||
|
const GOOGLE_204_TIMEOUT_MS = 2000;
|
||||||
|
|
||||||
|
let cachedOptimized: boolean | null = null;
|
||||||
|
let cachedPromise: Promise<boolean> | null = null;
|
||||||
|
let loggedOnce = false;
|
||||||
|
|
||||||
|
function getLocaleAndTimezone(): { locale: string; timezone: string } {
|
||||||
|
const locale = app.getLocale?.() || '';
|
||||||
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
|
||||||
|
return { locale, timezone };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRegionOptimized(locale: string, timezone: string): boolean {
|
||||||
|
// Prefer timezone when available to reduce false positives from locale alone.
|
||||||
|
if (timezone) return timezone === 'Asia/Shanghai';
|
||||||
|
return locale === 'zh-CN';
|
||||||
|
}
|
||||||
|
|
||||||
|
function probeGoogle204(timeoutMs: number): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let done = false;
|
||||||
|
const finish = (value: boolean) => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = request(
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
hostname: GOOGLE_204_HOST,
|
||||||
|
path: GOOGLE_204_PATH,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
const status = res.statusCode || 0;
|
||||||
|
res.resume();
|
||||||
|
finish(status >= 200 && status < 300);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.setTimeout(timeoutMs, () => {
|
||||||
|
req.destroy(new Error('google_204_timeout'));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', () => finish(false));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeOptimization(): Promise<boolean> {
|
||||||
|
const { locale, timezone } = getLocaleAndTimezone();
|
||||||
|
|
||||||
|
if (isRegionOptimized(locale, timezone)) {
|
||||||
|
if (!loggedOnce) {
|
||||||
|
logger.info(`Region optimization enabled via locale/timezone (locale=${locale || 'unknown'}, tz=${timezone || 'unknown'})`);
|
||||||
|
loggedOnce = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachable = await probeGoogle204(GOOGLE_204_TIMEOUT_MS);
|
||||||
|
const isOptimized = !reachable;
|
||||||
|
|
||||||
|
if (!loggedOnce) {
|
||||||
|
const reason = reachable ? 'google_204_reachable' : 'google_204_unreachable';
|
||||||
|
logger.info(`Network optimization probe: ${reason} (locale=${locale || 'unknown'}, tz=${timezone || 'unknown'})`);
|
||||||
|
loggedOnce = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isOptimized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function shouldOptimizeNetwork(): Promise<boolean> {
|
||||||
|
if (cachedOptimized !== null) return cachedOptimized;
|
||||||
|
if (cachedPromise) return cachedPromise;
|
||||||
|
|
||||||
|
if (!app.isReady()) {
|
||||||
|
await app.whenReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedPromise = computeOptimization()
|
||||||
|
.then((result) => {
|
||||||
|
cachedOptimized = result;
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.warn('Network optimization check failed, defaulting to enabled:', err);
|
||||||
|
cachedOptimized = true;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
cachedPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return cachedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUvMirrorEnv(): Promise<Record<string, string>> {
|
||||||
|
const isOptimized = await shouldOptimizeNetwork();
|
||||||
|
return isOptimized ? { ...UV_MIRROR_ENV } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function warmupNetworkOptimization(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await shouldOptimizeNetwork();
|
||||||
|
} catch {
|
||||||
|
// Ignore warmup failures
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { app } from 'electron';
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { getUvMirrorEnv } from './uv-env';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to the bundled uv binary
|
* Get the path to the bundled uv binary
|
||||||
@@ -53,6 +54,33 @@ export async function installUv(): Promise<void> {
|
|||||||
console.log('uv is available and ready to use');
|
console.log('uv is available and ready to use');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a managed Python 3.12 is ready and accessible
|
||||||
|
*/
|
||||||
|
export async function isPythonReady(): Promise<boolean> {
|
||||||
|
// Use 'uv' if in PATH, otherwise use full bundled path
|
||||||
|
const inPath = await new Promise<boolean>((resolve) => {
|
||||||
|
const cmd = process.platform === 'win32' ? 'where.exe' : 'which';
|
||||||
|
const child = spawn(cmd, ['uv']);
|
||||||
|
child.on('close', (code) => resolve(code === 0));
|
||||||
|
child.on('error', () => resolve(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
const uvBin = inPath ? 'uv' : getBundledUvPath();
|
||||||
|
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
try {
|
||||||
|
const child = spawn(uvBin, ['python', 'find', '3.12'], {
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
});
|
||||||
|
child.on('close', (code) => resolve(code === 0));
|
||||||
|
child.on('error', () => resolve(false));
|
||||||
|
} catch {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use bundled uv to install a managed Python version (default 3.12)
|
* Use bundled uv to install a managed Python version (default 3.12)
|
||||||
* Automatically picks the best available uv binary
|
* Automatically picks the best available uv binary
|
||||||
@@ -69,10 +97,15 @@ export async function setupManagedPython(): Promise<void> {
|
|||||||
const uvBin = inPath ? 'uv' : getBundledUvPath();
|
const uvBin = inPath ? 'uv' : getBundledUvPath();
|
||||||
|
|
||||||
console.log(`Setting up python with: ${uvBin}`);
|
console.log(`Setting up python with: ${uvBin}`);
|
||||||
|
const uvEnv = await getUvMirrorEnv();
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const child = spawn(uvBin, ['python', 'install', '3.12'], {
|
const child = spawn(uvBin, ['python', 'install', '3.12'], {
|
||||||
shell: process.platform === 'win32'
|
shell: process.platform === 'win32',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...uvEnv,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
child.stdout?.on('data', (data) => {
|
||||||
@@ -96,7 +129,11 @@ export async function setupManagedPython(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const findPath = await new Promise<string>((resolve) => {
|
const findPath = await new Promise<string>((resolve) => {
|
||||||
const child = spawn(uvBin, ['python', 'find', '3.12'], {
|
const child = spawn(uvBin, ['python', 'find', '3.12'], {
|
||||||
shell: process.platform === 'win32'
|
shell: process.platform === 'win32',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...uvEnv,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let output = '';
|
let output = '';
|
||||||
child.stdout?.on('data', (data) => { output += data; });
|
child.stdout?.on('data', (data) => { output += data; });
|
||||||
@@ -111,4 +148,4 @@ export async function setupManagedPython(): Promise<void> {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Could not determine Python path:', err);
|
console.warn('Could not determine Python path:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user