From e95c72f79a4beb23cce23d1adee25bf11c762648 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:52:03 +0800 Subject: [PATCH] fix: persist uv mirror config file (#921) --- electron/utils/uv-env.ts | 51 +++++++++++++++++++++++++++++++-- tests/unit/uv-env.test.ts | 60 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 tests/unit/uv-env.test.ts diff --git a/electron/utils/uv-env.ts b/electron/utils/uv-env.ts index c25b5e9..7f26ba0 100644 --- a/electron/utils/uv-env.ts +++ b/electron/utils/uv-env.ts @@ -1,12 +1,52 @@ import { app } from 'electron'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { request } from 'https'; +import path from 'path'; import { logger } from './logger'; +import { getOpenClawConfigDir } from './paths'; + +const UV_PYTHON_INSTALL_MIRROR_URL = 'https://registry.npmmirror.com/-/binary/python-build-standalone/'; +const UV_INDEX_URL = 'https://pypi.tuna.tsinghua.edu.cn/simple/'; 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/', + UV_PYTHON_INSTALL_MIRROR: UV_PYTHON_INSTALL_MIRROR_URL, + UV_INDEX_URL, }; +function quoteTomlString(value: string): string { + return JSON.stringify(value); +} + +function buildClawXUvConfigToml(): string { + return [ + '# This file is managed by ClawX.', + '# It lets uv use ClawX-selected mirrors even when host exec filters UV_INDEX_URL.', + `index-url = ${quoteTomlString(UV_INDEX_URL)}`, + `python-install-mirror = ${quoteTomlString(UV_PYTHON_INSTALL_MIRROR_URL)}`, + '', + ].join('\n'); +} + +export function getClawXUvConfigFilePath(): string { + return path.join(getOpenClawConfigDir(), 'clawx', 'uv.toml'); +} + +function ensureClawXUvConfigFile(): string | null { + const filePath = getClawXUvConfigFilePath(); + const content = buildClawXUvConfigToml(); + + try { + mkdirSync(path.dirname(filePath), { recursive: true }); + if (!existsSync(filePath) || readFileSync(filePath, 'utf-8') !== content) { + writeFileSync(filePath, content, { encoding: 'utf-8', mode: 0o644 }); + } + return filePath; + } catch (err) { + logger.warn('Failed to write ClawX uv config file:', err); + return null; + } +} + const GOOGLE_204_HOST = 'www.google.com'; const GOOGLE_204_PATH = '/generate_204'; const GOOGLE_204_TIMEOUT_MS = 2000; @@ -108,7 +148,12 @@ export async function shouldOptimizeNetwork(): Promise { export async function getUvMirrorEnv(): Promise> { const isOptimized = await shouldOptimizeNetwork(); - return isOptimized ? { ...UV_MIRROR_ENV } : {}; + if (!isOptimized) return {}; + + const uvConfigFile = ensureClawXUvConfigFile(); + return uvConfigFile + ? { ...UV_MIRROR_ENV, UV_CONFIG_FILE: uvConfigFile } + : { ...UV_MIRROR_ENV }; } export async function warmupNetworkOptimization(): Promise { diff --git a/tests/unit/uv-env.test.ts b/tests/unit/uv-env.test.ts new file mode 100644 index 0000000..91a151e --- /dev/null +++ b/tests/unit/uv-env.test.ts @@ -0,0 +1,60 @@ +// @vitest-environment node +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import path from 'path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +let tempHome: string; + +beforeEach(() => { + tempHome = mkdtempSync(path.join(tmpdir(), 'clawx-uv-env-')); + vi.resetModules(); + + vi.doMock('os', async () => { + const actual = await vi.importActual('os'); + return { + ...actual, + homedir: () => tempHome, + }; + }); + + vi.doMock('electron', () => ({ + app: { + getLocale: vi.fn().mockReturnValue('en-US'), + getPath: vi.fn().mockReturnValue(path.join(tempHome, '.clawx')), + getAppPath: vi.fn().mockReturnValue(process.cwd()), + getVersion: vi.fn().mockReturnValue('0.0.0-test'), + isPackaged: false, + isReady: vi.fn().mockReturnValue(true), + whenReady: vi.fn().mockResolvedValue(undefined), + }, + })); + + vi.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => ({ + resolvedOptions: () => ({ timeZone: 'Asia/Shanghai' }), + }) as Intl.DateTimeFormat); +}); + +afterEach(() => { + vi.restoreAllMocks(); + vi.resetModules(); + if (tempHome) { + rmSync(tempHome, { recursive: true, force: true }); + } +}); + +describe('uv mirror environment', () => { + it('writes a ClawX-managed uv.toml under ~/.openclaw and exposes it via UV_CONFIG_FILE', async () => { + const { getUvMirrorEnv } = await import('@electron/utils/uv-env'); + + const env = await getUvMirrorEnv(); + + const expectedPath = path.join(tempHome, '.openclaw', 'clawx', 'uv.toml'); + expect(env.UV_INDEX_URL).toBe('https://pypi.tuna.tsinghua.edu.cn/simple/'); + expect(env.UV_PYTHON_INSTALL_MIRROR).toBe('https://registry.npmmirror.com/-/binary/python-build-standalone/'); + expect(env.UV_CONFIG_FILE).toBe(expectedPath); + expect(existsSync(expectedPath)).toBe(true); + expect(readFileSync(expectedPath, 'utf-8')).toContain('index-url = "https://pypi.tuna.tsinghua.edu.cn/simple/"'); + expect(readFileSync(expectedPath, 'utf-8')).toContain('python-install-mirror = "https://registry.npmmirror.com/-/binary/python-build-standalone/"'); + }); +});