From 7965b9c06e799091c279e0fac99982deb95f866f Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:55:37 +0800 Subject: [PATCH] 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> --- electron/gateway/manager.ts | 16 +++++ electron/main/index.ts | 4 ++ electron/utils/uv-env.ts | 120 ++++++++++++++++++++++++++++++++++++ electron/utils/uv-setup.ts | 43 ++++++++++++- 4 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 electron/utils/uv-env.ts diff --git a/electron/gateway/manager.ts b/electron/gateway/manager.ts index 3eeb1fb98..d9f2fd3d1 100644 --- a/electron/gateway/manager.ts +++ b/electron/gateway/manager.ts @@ -20,6 +20,8 @@ import { getApiKey } from '../utils/secure-storage'; import { getProviderEnvVar } from '../utils/openclaw-auth'; import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol'; import { logger } from '../utils/logger'; +import { getUvMirrorEnv } from '../utils/uv-env'; +import { isPythonReady, setupManagedPython } from '../utils/uv-setup'; /** * Gateway connection status @@ -149,6 +151,17 @@ export class GatewayManager extends EventEmitter { this.setStatus({ state: 'starting', reconnectAttempts: 0 }); 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 logger.info('Checking for existing Gateway...'); const existing = await this.findExistingGateway(); @@ -470,12 +483,15 @@ export class GatewayManager extends EventEmitter { logger.warn(`Failed to load API key for ${providerType}:`, err); } } + + const uvEnv = await getUvMirrorEnv(); return new Promise((resolve, reject) => { const spawnEnv: Record = { ...process.env, PATH: finalPath, ...providerEnv, + ...uvEnv, OPENCLAW_GATEWAY_TOKEN: gatewayToken, OPENCLAW_SKIP_CHANNELS: '', CLAWDBOT_SKIP_CHANNELS: '', diff --git a/electron/main/index.ts b/electron/main/index.ts index 206467ba9..a9e3bf4d0 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -11,6 +11,7 @@ import { createMenu } from './menu'; import { appUpdater, registerUpdateHandlers } from './updater'; import { logger } from '../utils/logger'; +import { warmupNetworkOptimization } from '../utils/uv-env'; import { ClawHubService } from '../gateway/clawhub'; @@ -111,6 +112,9 @@ async function initialize(): Promise { logger.info(`Resources path: ${process.resourcesPath}`); logger.info(`Exec path: ${process.execPath}`); + // Warm up network optimization (non-blocking) + void warmupNetworkOptimization(); + // Set application menu createMenu(); diff --git a/electron/utils/uv-env.ts b/electron/utils/uv-env.ts new file mode 100644 index 000000000..c25b5e909 --- /dev/null +++ b/electron/utils/uv-env.ts @@ -0,0 +1,120 @@ +import { app } from 'electron'; +import { request } from 'https'; +import { logger } from './logger'; + +const UV_MIRROR_ENV: Record = { + 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 | 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 { + 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 { + 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 { + 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> { + const isOptimized = await shouldOptimizeNetwork(); + return isOptimized ? { ...UV_MIRROR_ENV } : {}; +} + +export async function warmupNetworkOptimization(): Promise { + try { + await shouldOptimizeNetwork(); + } catch { + // Ignore warmup failures + } +} diff --git a/electron/utils/uv-setup.ts b/electron/utils/uv-setup.ts index acf7c1377..fb349b3f1 100644 --- a/electron/utils/uv-setup.ts +++ b/electron/utils/uv-setup.ts @@ -2,6 +2,7 @@ import { app } from 'electron'; import { spawn } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; +import { getUvMirrorEnv } from './uv-env'; /** * Get the path to the bundled uv binary @@ -53,6 +54,33 @@ export async function installUv(): Promise { 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 { + // Use 'uv' if in PATH, otherwise use full bundled path + const inPath = await new Promise((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((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) * Automatically picks the best available uv binary @@ -69,10 +97,15 @@ export async function setupManagedPython(): Promise { const uvBin = inPath ? 'uv' : getBundledUvPath(); console.log(`Setting up python with: ${uvBin}`); + const uvEnv = await getUvMirrorEnv(); await new Promise((resolve, reject) => { 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) => { @@ -96,7 +129,11 @@ export async function setupManagedPython(): Promise { try { const findPath = await new Promise((resolve) => { const child = spawn(uvBin, ['python', 'find', '3.12'], { - shell: process.platform === 'win32' + shell: process.platform === 'win32', + env: { + ...process.env, + ...uvEnv, + }, }); let output = ''; child.stdout?.on('data', (data) => { output += data; }); @@ -111,4 +148,4 @@ export async function setupManagedPython(): Promise { } catch (err) { console.warn('Could not determine Python path:', err); } -} \ No newline at end of file +}