diff --git a/src/lib/code-runner/engines/pyodide.ts b/src/lib/code-runner/engines/pyodide.ts index 336947b..b17dd17 100644 --- a/src/lib/code-runner/engines/pyodide.ts +++ b/src/lib/code-runner/engines/pyodide.ts @@ -28,14 +28,18 @@ const EXECUTION_TIMEOUT = 30000; // 最大输出长度 const MAX_OUTPUT_LENGTH = 50000; -// Pyodide CDN URL -const PYODIDE_CDN = 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/'; +// Pyodide CDN URLs(按优先级排序) +const PYODIDE_CDN_LIST = [ + 'https://cdn.jsdelivr.net/pyodide/v0.27.0/full/', // jsDelivr CDN + 'https://cdn.jsdelivr.net/npm/pyodide@0.27.0/', // npm CDN 备用 +]; class PyodideEngine implements IExecutionEngine { private pyodide: PyodideInterface | null = null; private loading = false; private loadPromise: Promise | null = null; private loadingCallbacks: PyodideLoadingCallback | null = null; + private currentCdnIndex = 0; supports(language: string): boolean { const lang = language.toLowerCase(); @@ -120,26 +124,49 @@ class PyodideEngine implements IExecutionEngine { this.loadingCallbacks?.onLoadingProgress?.('正在加载 Python 运行时...', 0); this.loadPromise = new Promise(async (resolve, reject) => { - try { - // 动态加载 Pyodide 脚本 - if (typeof window !== 'undefined' && !(window as unknown as Record).loadPyodide) { - this.loadingCallbacks?.onLoadingProgress?.('正在下载 Pyodide...', 20); - await this.loadScript(`${PYODIDE_CDN}pyodide.js`); - } + let lastError: Error | null = null; - this.loadingCallbacks?.onLoadingProgress?.('正在初始化 Python 环境...', 50); + // 尝试所有 CDN + for (let i = 0; i < PYODIDE_CDN_LIST.length; i++) { + const cdnUrl = PYODIDE_CDN_LIST[i]; + this.currentCdnIndex = i; - // 初始化 Pyodide - const loadPyodide = (window as unknown as { loadPyodide: (config: { indexURL: string }) => Promise }).loadPyodide; + try { + // 动态加载 Pyodide 脚本 + if (typeof window !== 'undefined') { + // 移除之前可能加载失败的脚本 + const existingScript = document.querySelector('script[data-pyodide]'); + if (existingScript) { + existingScript.remove(); + } - const pyodide = await loadPyodide({ - indexURL: PYODIDE_CDN, - }); + // 重置 loadPyodide 函数 + delete (window as unknown as Record).loadPyodide; - this.loadingCallbacks?.onLoadingProgress?.('Python 环境准备就绪', 100); + this.loadingCallbacks?.onLoadingProgress?.( + `正在下载 Pyodide (CDN ${i + 1}/${PYODIDE_CDN_LIST.length})...`, + 20 + ); + await this.loadScript(`${cdnUrl}pyodide.js`); + } - // 设置标准输出重定向 - await pyodide.runPythonAsync(` + this.loadingCallbacks?.onLoadingProgress?.('正在初始化 Python 环境...', 50); + + // 初始化 Pyodide + const loadPyodide = (window as unknown as { loadPyodide: (config: { indexURL: string }) => Promise }).loadPyodide; + + if (!loadPyodide) { + throw new Error('loadPyodide 函数未加载'); + } + + const pyodide = await loadPyodide({ + indexURL: cdnUrl, + }); + + this.loadingCallbacks?.onLoadingProgress?.('Python 环境准备就绪', 100); + + // 设置标准输出重定向 + await pyodide.runPythonAsync(` import sys from io import StringIO @@ -163,20 +190,34 @@ class OutputCapture: __output_capture__ = OutputCapture() sys.stdout = __output_capture__ sys.stderr = __output_capture__ - `); + `); - this.pyodide = pyodide; - this.loading = false; - this.loadingCallbacks?.onLoadingComplete?.(); + this.pyodide = pyodide; + this.loading = false; + this.loadingCallbacks?.onLoadingComplete?.(); - resolve(pyodide); - } catch (error) { - this.loading = false; - this.loadPromise = null; - const errorMsg = error instanceof Error ? error.message : String(error); - this.loadingCallbacks?.onLoadingError?.(errorMsg); - reject(new Error(`Pyodide 加载失败: ${errorMsg}`)); + resolve(pyodide); + return; // 成功加载,退出循环 + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + console.warn(`Pyodide CDN ${i + 1} 加载失败:`, lastError.message); + + // 如果还有其他 CDN,继续尝试 + if (i < PYODIDE_CDN_LIST.length - 1) { + this.loadingCallbacks?.onLoadingProgress?.( + `CDN ${i + 1} 加载失败,尝试备用 CDN...`, + 10 + ); + } + } } + + // 所有 CDN 都失败了 + this.loading = false; + this.loadPromise = null; + const errorMsg = lastError?.message || '未知错误'; + this.loadingCallbacks?.onLoadingError?.(errorMsg); + reject(new Error(`Pyodide 加载失败: ${errorMsg}`)); }); return this.loadPromise; @@ -186,6 +227,7 @@ sys.stderr = __output_capture__ return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; + script.setAttribute('data-pyodide', 'true'); script.onload = () => resolve(); script.onerror = () => reject(new Error(`Failed to load script: ${src}`)); document.head.appendChild(script);