Compare commits

...

2 Commits

Author SHA1 Message Date
gaoziman
30156458ba feat(代码执行): 增强远程引擎多语言 Unicode 支持
- 改进错误信息处理,提供更清晰的错误提示
- 添加 Go 语言非 ASCII 字符转义支持
- 添加 Kotlin、C#、C/C++ 标准 Unicode 转义
- 添加 Rust、Swift 大括号格式转义 \u{XXXX}
- 添加 Ruby、PHP 字符串转义支持
2025-12-21 16:34:07 +08:00
gaoziman
c2dcf9b23f perf(代码执行): 优化 Pyodide 引擎加载机制
- 升级 Pyodide 版本从 v0.24.1 到 v0.27.0
- 添加多 CDN 备用机制,提高加载成功率
- 加载失败时自动切换到备用 CDN
- 改进加载进度提示和错误处理
2025-12-21 16:33:48 +08:00
2 changed files with 314 additions and 31 deletions

View File

@ -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<PyodideInterface> | 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<PyodideInterface>(async (resolve, reject) => {
try {
// 动态加载 Pyodide 脚本
if (typeof window !== 'undefined' && !(window as unknown as Record<string, unknown>).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<PyodideInterface> }).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<string, unknown>).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<PyodideInterface> }).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);

View File

@ -110,14 +110,29 @@ class RemoteEngine implements IExecutionEngine {
}
// 处理运行结果
const hasError = result.run.code !== 0 || result.run.signal !== null;
const runCode = result.run.code;
const hasError = runCode !== 0 || result.run.signal !== null;
const output = this.formatOutput(result.run.stdout || result.run.output);
const errorOutput = result.run.stderr ? this.formatOutput(result.run.stderr) : undefined;
// 构建错误信息
let errorMessage: string | undefined;
if (hasError) {
if (errorOutput) {
errorMessage = errorOutput;
} else if (result.run.signal) {
errorMessage = `进程被信号终止: ${result.run.signal}`;
} else if (runCode === null || runCode === undefined) {
errorMessage = '执行异常: 未获取到退出代码';
} else {
errorMessage = `退出代码: ${runCode}`;
}
}
return {
success: !hasError || (output.length > 0 && !errorOutput),
output: output.slice(0, MAX_OUTPUT_LENGTH),
error: hasError ? errorOutput || `退出代码: ${result.run.code}` : undefined,
error: errorMessage,
executionTime,
engine: 'remote',
};
@ -170,11 +185,51 @@ class RemoteEngine implements IExecutionEngine {
private preprocessCode(code: string, language: string): string {
const lang = language.toLowerCase();
// 只对 Java 进行处理Java 字符串支持 \uXXXX 转义)
// Java\uXXXX 格式 + UTF-8 输出流注入
if (lang === 'java') {
return this.escapeNonAsciiForJava(code);
}
// Go\uXXXX 格式
if (lang === 'go' || lang === 'golang') {
return this.escapeNonAsciiForGo(code);
}
// Kotlin\uXXXX 格式(类似 Java但不需要注入 UTF-8 设置)
if (lang === 'kotlin' || lang === 'kt') {
return this.escapeNonAsciiStandard(code);
}
// C#\uXXXX 格式
if (lang === 'csharp' || lang === 'cs') {
return this.escapeNonAsciiStandard(code);
}
// C/C++\uXXXX 格式 (C11/C++11)
if (lang === 'c' || lang === 'cpp' || lang === 'c++') {
return this.escapeNonAsciiStandard(code);
}
// Rust\u{XXXX} 格式(大括号语法)
if (lang === 'rust' || lang === 'rs') {
return this.escapeNonAsciiBraceFormat(code);
}
// Swift\u{XXXX} 格式(大括号语法)
if (lang === 'swift') {
return this.escapeNonAsciiBraceFormat(code);
}
// Ruby\u{XXXX} 格式(大括号语法)
if (lang === 'ruby' || lang === 'rb') {
return this.escapeNonAsciiRuby(code);
}
// PHP\u{XXXX} 格式PHP 7+,仅双引号字符串)
if (lang === 'php') {
return this.escapeNonAsciiPHP(code);
}
return code;
}
@ -234,6 +289,85 @@ class RemoteEngine implements IExecutionEngine {
return quote + escaped + quote;
}
/**
* Go ASCII Unicode
*/
private escapeNonAsciiForGo(code: string): string {
// 处理双引号字符串
let result = code.replace(/"([^"\\]|\\.)*"/g, (match) => {
return this.escapeGoStringContent(match);
});
// 处理反引号字符串Go 的原始字符串,不支持转义,需要特殊处理)
// 对于反引号字符串,我们将其转换为双引号字符串并添加转义
result = result.replace(/`([^`]*)`/g, (match, content) => {
// 检查是否包含非 ASCII 字符
if (/[^\x00-\x7F]/.test(content)) {
// 转换为双引号字符串,并转义非 ASCII 字符
let escaped = '';
for (let i = 0; i < content.length; i++) {
const char = content[i];
const code = char.charCodeAt(0);
if (char === '"') {
escaped += '\\"';
} else if (char === '\\') {
escaped += '\\\\';
} else if (char === '\n') {
escaped += '\\n';
} else if (char === '\r') {
escaped += '\\r';
} else if (char === '\t') {
escaped += '\\t';
} else if (code > 127) {
escaped += '\\u' + code.toString(16).padStart(4, '0');
} else {
escaped += char;
}
}
return '"' + escaped + '"';
}
return match;
});
return result;
}
/**
* Go ASCII
*/
private escapeGoStringContent(str: string): string {
const content = str.slice(1, -1); // 移除引号
let escaped = '';
for (let i = 0; i < content.length; i++) {
const char = content[i];
const code = char.charCodeAt(0);
// 处理转义序列(保留原样)
if (char === '\\' && i + 1 < content.length) {
escaped += char + content[i + 1];
i++;
continue;
}
// 非 ASCII 字符转换为 \uXXXX 或 \UXXXXXXXX
if (code > 127) {
// 对于 BMP 范围内的字符使用 \uXXXX
if (code <= 0xFFFF) {
escaped += '\\u' + code.toString(16).padStart(4, '0');
} else {
// 超出 BMP 的字符使用 \UXXXXXXXX
escaped += '\\U' + code.toString(16).padStart(8, '0');
}
} else {
escaped += char;
}
}
return '"' + escaped + '"';
}
private formatOutput(output: string, prefix?: string): string {
let result = output.trim();
@ -246,6 +380,113 @@ class RemoteEngine implements IExecutionEngine {
return result;
}
/**
* \uXXXX Kotlin, C#, C/C++
*
*/
private escapeNonAsciiStandard(code: string): string {
return code.replace(/"([^"\\]|\\.)*"/g, (match) => {
return this.escapeStringContent(match, '"');
});
}
/**
* \u{XXXX} Rust, Swift
*/
private escapeNonAsciiBraceFormat(code: string): string {
return code.replace(/"([^"\\]|\\.)*"/g, (match) => {
const content = match.slice(1, -1);
let escaped = '';
for (let i = 0; i < content.length; i++) {
const char = content[i];
const charCode = char.charCodeAt(0);
// 处理转义序列(保留原样)
if (char === '\\' && i + 1 < content.length) {
escaped += char + content[i + 1];
i++;
continue;
}
// 非 ASCII 字符转换为 \u{XXXX}
if (charCode > 127) {
escaped += '\\u{' + charCode.toString(16) + '}';
} else {
escaped += char;
}
}
return '"' + escaped + '"';
});
}
/**
* Ruby
* \u{XXXX}
*/
private escapeNonAsciiRuby(code: string): string {
// 只处理双引号字符串
return code.replace(/"([^"\\]|\\.)*"/g, (match) => {
const content = match.slice(1, -1);
let escaped = '';
for (let i = 0; i < content.length; i++) {
const char = content[i];
const charCode = char.charCodeAt(0);
// 处理转义序列(保留原样)
if (char === '\\' && i + 1 < content.length) {
escaped += char + content[i + 1];
i++;
continue;
}
// 非 ASCII 字符转换为 \u{XXXX}
if (charCode > 127) {
escaped += '\\u{' + charCode.toString(16).padStart(4, '0') + '}';
} else {
escaped += char;
}
}
return '"' + escaped + '"';
});
}
/**
* PHP
* \u{XXXX}
*/
private escapeNonAsciiPHP(code: string): string {
// 只处理双引号字符串
return code.replace(/"([^"\\]|\\.)*"/g, (match) => {
const content = match.slice(1, -1);
let escaped = '';
for (let i = 0; i < content.length; i++) {
const char = content[i];
const charCode = char.charCodeAt(0);
// 处理转义序列(保留原样)
if (char === '\\' && i + 1 < content.length) {
escaped += char + content[i + 1];
i++;
continue;
}
// 非 ASCII 字符转换为 \u{XXXX}
if (charCode > 127) {
escaped += '\\u{' + charCode.toString(16) + '}';
} else {
escaped += char;
}
}
return '"' + escaped + '"';
});
}
}
// 导出单例