Compare commits
No commits in common. "30156458ba70edb5d308595b59d7ab3ad7c3c32e" and "79b871d2031eedfe16bec48e7cc837e0080fa9aa" have entirely different histories.
30156458ba
...
79b871d203
@ -28,18 +28,14 @@ const EXECUTION_TIMEOUT = 30000;
|
|||||||
// 最大输出长度
|
// 最大输出长度
|
||||||
const MAX_OUTPUT_LENGTH = 50000;
|
const MAX_OUTPUT_LENGTH = 50000;
|
||||||
|
|
||||||
// Pyodide CDN URLs(按优先级排序)
|
// Pyodide CDN URL
|
||||||
const PYODIDE_CDN_LIST = [
|
const PYODIDE_CDN = 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/';
|
||||||
'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 {
|
class PyodideEngine implements IExecutionEngine {
|
||||||
private pyodide: PyodideInterface | null = null;
|
private pyodide: PyodideInterface | null = null;
|
||||||
private loading = false;
|
private loading = false;
|
||||||
private loadPromise: Promise<PyodideInterface> | null = null;
|
private loadPromise: Promise<PyodideInterface> | null = null;
|
||||||
private loadingCallbacks: PyodideLoadingCallback | null = null;
|
private loadingCallbacks: PyodideLoadingCallback | null = null;
|
||||||
private currentCdnIndex = 0;
|
|
||||||
|
|
||||||
supports(language: string): boolean {
|
supports(language: string): boolean {
|
||||||
const lang = language.toLowerCase();
|
const lang = language.toLowerCase();
|
||||||
@ -124,49 +120,26 @@ class PyodideEngine implements IExecutionEngine {
|
|||||||
this.loadingCallbacks?.onLoadingProgress?.('正在加载 Python 运行时...', 0);
|
this.loadingCallbacks?.onLoadingProgress?.('正在加载 Python 运行时...', 0);
|
||||||
|
|
||||||
this.loadPromise = new Promise<PyodideInterface>(async (resolve, reject) => {
|
this.loadPromise = new Promise<PyodideInterface>(async (resolve, reject) => {
|
||||||
let lastError: Error | null = null;
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
// 尝试所有 CDN
|
this.loadingCallbacks?.onLoadingProgress?.('正在初始化 Python 环境...', 50);
|
||||||
for (let i = 0; i < PYODIDE_CDN_LIST.length; i++) {
|
|
||||||
const cdnUrl = PYODIDE_CDN_LIST[i];
|
|
||||||
this.currentCdnIndex = i;
|
|
||||||
|
|
||||||
try {
|
// 初始化 Pyodide
|
||||||
// 动态加载 Pyodide 脚本
|
const loadPyodide = (window as unknown as { loadPyodide: (config: { indexURL: string }) => Promise<PyodideInterface> }).loadPyodide;
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// 移除之前可能加载失败的脚本
|
|
||||||
const existingScript = document.querySelector('script[data-pyodide]');
|
|
||||||
if (existingScript) {
|
|
||||||
existingScript.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置 loadPyodide 函数
|
const pyodide = await loadPyodide({
|
||||||
delete (window as unknown as Record<string, unknown>).loadPyodide;
|
indexURL: PYODIDE_CDN,
|
||||||
|
});
|
||||||
|
|
||||||
this.loadingCallbacks?.onLoadingProgress?.(
|
this.loadingCallbacks?.onLoadingProgress?.('Python 环境准备就绪', 100);
|
||||||
`正在下载 Pyodide (CDN ${i + 1}/${PYODIDE_CDN_LIST.length})...`,
|
|
||||||
20
|
|
||||||
);
|
|
||||||
await this.loadScript(`${cdnUrl}pyodide.js`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadingCallbacks?.onLoadingProgress?.('正在初始化 Python 环境...', 50);
|
// 设置标准输出重定向
|
||||||
|
await pyodide.runPythonAsync(`
|
||||||
// 初始化 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
|
import sys
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
@ -190,34 +163,20 @@ class OutputCapture:
|
|||||||
__output_capture__ = OutputCapture()
|
__output_capture__ = OutputCapture()
|
||||||
sys.stdout = __output_capture__
|
sys.stdout = __output_capture__
|
||||||
sys.stderr = __output_capture__
|
sys.stderr = __output_capture__
|
||||||
`);
|
`);
|
||||||
|
|
||||||
this.pyodide = pyodide;
|
this.pyodide = pyodide;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.loadingCallbacks?.onLoadingComplete?.();
|
this.loadingCallbacks?.onLoadingComplete?.();
|
||||||
|
|
||||||
resolve(pyodide);
|
resolve(pyodide);
|
||||||
return; // 成功加载,退出循环
|
} catch (error) {
|
||||||
} catch (error) {
|
this.loading = false;
|
||||||
lastError = error instanceof Error ? error : new Error(String(error));
|
this.loadPromise = null;
|
||||||
console.warn(`Pyodide CDN ${i + 1} 加载失败:`, lastError.message);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
this.loadingCallbacks?.onLoadingError?.(errorMsg);
|
||||||
// 如果还有其他 CDN,继续尝试
|
reject(new Error(`Pyodide 加载失败: ${errorMsg}`));
|
||||||
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;
|
return this.loadPromise;
|
||||||
@ -227,7 +186,6 @@ sys.stderr = __output_capture__
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = src;
|
script.src = src;
|
||||||
script.setAttribute('data-pyodide', 'true');
|
|
||||||
script.onload = () => resolve();
|
script.onload = () => resolve();
|
||||||
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|||||||
@ -110,29 +110,14 @@ class RemoteEngine implements IExecutionEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理运行结果
|
// 处理运行结果
|
||||||
const runCode = result.run.code;
|
const hasError = result.run.code !== 0 || result.run.signal !== null;
|
||||||
const hasError = runCode !== 0 || result.run.signal !== null;
|
|
||||||
const output = this.formatOutput(result.run.stdout || result.run.output);
|
const output = this.formatOutput(result.run.stdout || result.run.output);
|
||||||
const errorOutput = result.run.stderr ? this.formatOutput(result.run.stderr) : undefined;
|
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 {
|
return {
|
||||||
success: !hasError || (output.length > 0 && !errorOutput),
|
success: !hasError || (output.length > 0 && !errorOutput),
|
||||||
output: output.slice(0, MAX_OUTPUT_LENGTH),
|
output: output.slice(0, MAX_OUTPUT_LENGTH),
|
||||||
error: errorMessage,
|
error: hasError ? errorOutput || `退出代码: ${result.run.code}` : undefined,
|
||||||
executionTime,
|
executionTime,
|
||||||
engine: 'remote',
|
engine: 'remote',
|
||||||
};
|
};
|
||||||
@ -185,51 +170,11 @@ class RemoteEngine implements IExecutionEngine {
|
|||||||
private preprocessCode(code: string, language: string): string {
|
private preprocessCode(code: string, language: string): string {
|
||||||
const lang = language.toLowerCase();
|
const lang = language.toLowerCase();
|
||||||
|
|
||||||
// Java:\uXXXX 格式 + UTF-8 输出流注入
|
// 只对 Java 进行处理(Java 字符串支持 \uXXXX 转义)
|
||||||
if (lang === 'java') {
|
if (lang === 'java') {
|
||||||
return this.escapeNonAsciiForJava(code);
|
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;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,85 +234,6 @@ class RemoteEngine implements IExecutionEngine {
|
|||||||
return quote + escaped + quote;
|
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 {
|
private formatOutput(output: string, prefix?: string): string {
|
||||||
let result = output.trim();
|
let result = output.trim();
|
||||||
|
|
||||||
@ -380,113 +246,6 @@ class RemoteEngine implements IExecutionEngine {
|
|||||||
|
|
||||||
return result;
|
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 + '"';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出单例
|
// 导出单例
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user