feat(代码运行): 添加多语言代码执行引擎
- 实现 sandbox 引擎支持 JavaScript/TypeScript 执行 - 实现 pyodide 引擎支持 Python 浏览器端执行 - 实现 remote 引擎支持 Java/Go/C/C++/Rust 等远程执行 - 添加语言配置和入口点检测逻辑 - 支持执行状态回调和加载进度显示
This commit is contained in:
parent
2e5120dc72
commit
192cd175da
265
src/lib/code-runner/engines/pyodide.ts
Normal file
265
src/lib/code-runner/engines/pyodide.ts
Normal file
@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Python Pyodide 执行引擎
|
||||
* 使用 Pyodide (Python WebAssembly) 在浏览器中执行 Python 代码
|
||||
*/
|
||||
|
||||
import type { IExecutionEngine, ExecutionResult } from '../types';
|
||||
|
||||
// Pyodide 类型定义
|
||||
interface PyodideInterface {
|
||||
runPython(code: string): unknown;
|
||||
runPythonAsync(code: string): Promise<unknown>;
|
||||
loadPackage(packages: string | string[]): Promise<void>;
|
||||
loadPackagesFromImports(code: string): Promise<void>;
|
||||
globals: Map<string, unknown>;
|
||||
}
|
||||
|
||||
// 加载状态回调
|
||||
export interface PyodideLoadingCallback {
|
||||
onLoadingStart?: () => void;
|
||||
onLoadingProgress?: (message: string, progress?: number) => void;
|
||||
onLoadingComplete?: () => void;
|
||||
onLoadingError?: (error: string) => void;
|
||||
}
|
||||
|
||||
// 执行超时时间(毫秒)
|
||||
const EXECUTION_TIMEOUT = 30000;
|
||||
|
||||
// 最大输出长度
|
||||
const MAX_OUTPUT_LENGTH = 50000;
|
||||
|
||||
// Pyodide CDN URL
|
||||
const PYODIDE_CDN = 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/';
|
||||
|
||||
class PyodideEngine implements IExecutionEngine {
|
||||
private pyodide: PyodideInterface | null = null;
|
||||
private loading = false;
|
||||
private loadPromise: Promise<PyodideInterface> | null = null;
|
||||
private loadingCallbacks: PyodideLoadingCallback | null = null;
|
||||
|
||||
supports(language: string): boolean {
|
||||
const lang = language.toLowerCase();
|
||||
return ['python', 'py'].includes(lang);
|
||||
}
|
||||
|
||||
// 设置加载回调
|
||||
setLoadingCallbacks(callbacks: PyodideLoadingCallback | null): void {
|
||||
this.loadingCallbacks = callbacks;
|
||||
}
|
||||
|
||||
// 检查是否已加载
|
||||
isLoaded(): boolean {
|
||||
return this.pyodide !== null;
|
||||
}
|
||||
|
||||
// 检查是否正在加载
|
||||
isLoading(): boolean {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
// 预加载 Pyodide(可选,提前加载以减少首次执行延迟)
|
||||
async preload(): Promise<void> {
|
||||
if (!this.pyodide && !this.loading) {
|
||||
await this.loadPyodide();
|
||||
}
|
||||
}
|
||||
|
||||
async execute(code: string, language: string): Promise<ExecutionResult> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
// 确保 Pyodide 已加载
|
||||
if (!this.pyodide) {
|
||||
await this.loadPyodide();
|
||||
}
|
||||
|
||||
if (!this.pyodide) {
|
||||
throw new Error('Pyodide 加载失败');
|
||||
}
|
||||
|
||||
// 尝试加载代码中 import 的包
|
||||
try {
|
||||
await this.pyodide.loadPackagesFromImports(code);
|
||||
} catch {
|
||||
// 忽略包加载错误,继续执行
|
||||
}
|
||||
|
||||
// 执行代码并捕获输出
|
||||
const output = await this.executeWithTimeout(code);
|
||||
const executionTime = Math.round(performance.now() - startTime);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output.slice(0, MAX_OUTPUT_LENGTH),
|
||||
executionTime,
|
||||
engine: 'pyodide',
|
||||
};
|
||||
} catch (error) {
|
||||
const executionTime = Math.round(performance.now() - startTime);
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: this.formatPythonError(error),
|
||||
executionTime,
|
||||
engine: 'pyodide',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async loadPyodide(): Promise<PyodideInterface> {
|
||||
if (this.pyodide) {
|
||||
return this.pyodide;
|
||||
}
|
||||
|
||||
if (this.loadPromise) {
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.loadingCallbacks?.onLoadingStart?.();
|
||||
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`);
|
||||
}
|
||||
|
||||
this.loadingCallbacks?.onLoadingProgress?.('正在初始化 Python 环境...', 50);
|
||||
|
||||
// 初始化 Pyodide
|
||||
const loadPyodide = (window as unknown as { loadPyodide: (config: { indexURL: string }) => Promise<PyodideInterface> }).loadPyodide;
|
||||
|
||||
const pyodide = await loadPyodide({
|
||||
indexURL: PYODIDE_CDN,
|
||||
});
|
||||
|
||||
this.loadingCallbacks?.onLoadingProgress?.('Python 环境准备就绪', 100);
|
||||
|
||||
// 设置标准输出重定向
|
||||
await pyodide.runPythonAsync(`
|
||||
import sys
|
||||
from io import StringIO
|
||||
|
||||
class OutputCapture:
|
||||
def __init__(self):
|
||||
self.outputs = []
|
||||
|
||||
def write(self, text):
|
||||
if text and text.strip():
|
||||
self.outputs.append(str(text))
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def get_output(self):
|
||||
return ''.join(self.outputs)
|
||||
|
||||
def clear(self):
|
||||
self.outputs = []
|
||||
|
||||
__output_capture__ = OutputCapture()
|
||||
sys.stdout = __output_capture__
|
||||
sys.stderr = __output_capture__
|
||||
`);
|
||||
|
||||
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}`));
|
||||
}
|
||||
});
|
||||
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
private loadScript(src: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
private async executeWithTimeout(code: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error(`执行超时(${EXECUTION_TIMEOUT / 1000}秒)`));
|
||||
}, EXECUTION_TIMEOUT);
|
||||
|
||||
try {
|
||||
if (!this.pyodide) {
|
||||
throw new Error('Pyodide 未加载');
|
||||
}
|
||||
|
||||
// 清空之前的输出
|
||||
await this.pyodide.runPythonAsync('__output_capture__.clear()');
|
||||
|
||||
// 执行用户代码
|
||||
const result = await this.pyodide.runPythonAsync(code);
|
||||
|
||||
// 获取捕获的输出
|
||||
const capturedOutput = await this.pyodide.runPythonAsync('__output_capture__.get_output()');
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 组合输出
|
||||
let output = String(capturedOutput || '');
|
||||
|
||||
// 如果代码有返回值且不是 None,添加到输出
|
||||
if (result !== undefined && result !== null && String(result) !== 'None') {
|
||||
if (output) {
|
||||
output += '\n';
|
||||
}
|
||||
output += String(result);
|
||||
}
|
||||
|
||||
resolve(output);
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private formatPythonError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
let message = error.message;
|
||||
|
||||
// 清理 Pyodide 错误信息
|
||||
message = message
|
||||
.replace(/PythonError:\s*/g, '')
|
||||
.replace(/Traceback \(most recent call last\):\s*/g, 'Traceback:\n')
|
||||
.replace(/File "<exec>", /g, '')
|
||||
.trim();
|
||||
|
||||
return message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
// 加载额外的 Python 包
|
||||
async loadPackages(packages: string[]): Promise<void> {
|
||||
if (!this.pyodide) {
|
||||
await this.loadPyodide();
|
||||
}
|
||||
|
||||
if (this.pyodide) {
|
||||
await this.pyodide.loadPackage(packages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const pyodideEngine = new PyodideEngine();
|
||||
252
src/lib/code-runner/engines/remote.ts
Normal file
252
src/lib/code-runner/engines/remote.ts
Normal file
@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 远程代码执行引擎
|
||||
* 通过 Piston API 执行 Java、Go、C/C++、Rust 等语言
|
||||
*/
|
||||
|
||||
import type { IExecutionEngine, ExecutionResult, LanguageConfig } from '../types';
|
||||
import { RUNNABLE_LANGUAGES } from '../types';
|
||||
|
||||
// 最大输出长度
|
||||
const MAX_OUTPUT_LENGTH = 50000;
|
||||
|
||||
// Piston API 响应类型
|
||||
interface PistonResponse {
|
||||
language: string;
|
||||
version: string;
|
||||
run: {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
output: string;
|
||||
code: number;
|
||||
signal: string | null;
|
||||
};
|
||||
compile?: {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
output: string;
|
||||
code: number;
|
||||
signal: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
class RemoteEngine implements IExecutionEngine {
|
||||
private abortController: AbortController | null = null;
|
||||
|
||||
supports(language: string): boolean {
|
||||
const config = this.getConfig(language);
|
||||
return config !== null && config.engine === 'remote';
|
||||
}
|
||||
|
||||
private getConfig(language: string): LanguageConfig | null {
|
||||
return RUNNABLE_LANGUAGES[language.toLowerCase()] || null;
|
||||
}
|
||||
|
||||
async execute(code: string, language: string): Promise<ExecutionResult> {
|
||||
const startTime = performance.now();
|
||||
const config = this.getConfig(language);
|
||||
|
||||
if (!config || !config.pistonLanguage) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `不支持的语言: ${language}`,
|
||||
executionTime: 0,
|
||||
engine: 'remote',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
this.abortController = new AbortController();
|
||||
|
||||
// 确定文件名
|
||||
const fileName = this.getFileName(language, config);
|
||||
|
||||
// 对于 Java,将非 ASCII 字符转换为 Unicode 转义序列
|
||||
const processedCode = this.preprocessCode(code, language);
|
||||
|
||||
// 调用后端代理 API
|
||||
const response = await fetch('/api/code/execute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language: config.pistonLanguage,
|
||||
version: config.pistonVersion || '*',
|
||||
files: [
|
||||
{
|
||||
name: fileName,
|
||||
content: processedCode,
|
||||
},
|
||||
],
|
||||
}),
|
||||
signal: this.abortController.signal,
|
||||
});
|
||||
|
||||
const executionTime = Math.round(performance.now() - startTime);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: '执行失败' }));
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: errorData.error || `HTTP ${response.status}`,
|
||||
executionTime,
|
||||
engine: 'remote',
|
||||
};
|
||||
}
|
||||
|
||||
const result: PistonResponse = await response.json();
|
||||
|
||||
// 处理编译错误
|
||||
if (result.compile && result.compile.code !== 0) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: this.formatOutput(result.compile.stderr || result.compile.output, '编译错误'),
|
||||
executionTime,
|
||||
engine: 'remote',
|
||||
};
|
||||
}
|
||||
|
||||
// 处理运行结果
|
||||
const hasError = result.run.code !== 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;
|
||||
|
||||
return {
|
||||
success: !hasError || (output.length > 0 && !errorOutput),
|
||||
output: output.slice(0, MAX_OUTPUT_LENGTH),
|
||||
error: hasError ? errorOutput || `退出代码: ${result.run.code}` : undefined,
|
||||
executionTime,
|
||||
engine: 'remote',
|
||||
};
|
||||
} catch (error) {
|
||||
const executionTime = Math.round(performance.now() - startTime);
|
||||
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '执行已取消',
|
||||
executionTime,
|
||||
engine: 'remote',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : '执行失败',
|
||||
executionTime,
|
||||
engine: 'remote',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
private getFileName(language: string, config: LanguageConfig): string {
|
||||
const lang = language.toLowerCase();
|
||||
const ext = config.fileExtension || lang;
|
||||
|
||||
// 特殊处理 Java(需要类名匹配文件名)
|
||||
if (lang === 'java') {
|
||||
return 'Main.java';
|
||||
}
|
||||
|
||||
return `main.${ext}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预处理代码,将非 ASCII 字符转换为 Unicode 转义序列
|
||||
* 解决 Piston API 不支持 UTF-8 输出的问题
|
||||
*/
|
||||
private preprocessCode(code: string, language: string): string {
|
||||
const lang = language.toLowerCase();
|
||||
|
||||
// 只对 Java 进行处理(Java 字符串支持 \uXXXX 转义)
|
||||
if (lang === 'java') {
|
||||
return this.escapeNonAsciiForJava(code);
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Java 代码中字符串内的非 ASCII 字符转换为 Unicode 转义序列
|
||||
* 并注入 UTF-8 输出流设置
|
||||
*/
|
||||
private escapeNonAsciiForJava(code: string): string {
|
||||
// 首先处理字符串中的非 ASCII 字符
|
||||
// 处理双引号字符串
|
||||
let result = code.replace(/"([^"\\]|\\.)*"/g, (match) => {
|
||||
return this.escapeStringContent(match, '"');
|
||||
});
|
||||
|
||||
// 处理单引号字符(char)
|
||||
result = result.replace(/'([^'\\]|\\.)*'/g, (match) => {
|
||||
return this.escapeStringContent(match, "'");
|
||||
});
|
||||
|
||||
// 注入 UTF-8 输出流设置到 main 方法开头
|
||||
// 匹配 public static void main 方法
|
||||
const mainMethodRegex = /(public\s+static\s+void\s+main\s*\([^)]*\)\s*\{)/;
|
||||
const utf8Setup = `$1
|
||||
try { System.setOut(new java.io.PrintStream(System.out, true, "UTF-8")); } catch (Exception e) {}`;
|
||||
|
||||
result = result.replace(mainMethodRegex, utf8Setup);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义字符串内容中的非 ASCII 字符
|
||||
*/
|
||||
private escapeStringContent(str: string, quote: 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
|
||||
if (code > 127) {
|
||||
escaped += '\\u' + code.toString(16).padStart(4, '0');
|
||||
} else {
|
||||
escaped += char;
|
||||
}
|
||||
}
|
||||
|
||||
return quote + escaped + quote;
|
||||
}
|
||||
|
||||
private formatOutput(output: string, prefix?: string): string {
|
||||
let result = output.trim();
|
||||
|
||||
// 移除 ANSI 颜色代码
|
||||
result = result.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
|
||||
if (prefix && result) {
|
||||
return `${prefix}:\n${result}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const remoteEngine = new RemoteEngine();
|
||||
224
src/lib/code-runner/engines/sandbox.ts
Normal file
224
src/lib/code-runner/engines/sandbox.ts
Normal file
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* JavaScript/TypeScript 沙箱执行引擎
|
||||
* 在隔离的 iframe 中安全执行代码
|
||||
*/
|
||||
|
||||
import type { IExecutionEngine, ExecutionResult } from '../types';
|
||||
|
||||
// 执行超时时间(毫秒)
|
||||
const EXECUTION_TIMEOUT = 10000;
|
||||
|
||||
// 最大输出长度
|
||||
const MAX_OUTPUT_LENGTH = 50000;
|
||||
|
||||
export class SandboxEngine implements IExecutionEngine {
|
||||
private iframe: HTMLIFrameElement | null = null;
|
||||
private abortController: AbortController | null = null;
|
||||
|
||||
supports(language: string): boolean {
|
||||
const lang = language.toLowerCase();
|
||||
return ['javascript', 'js', 'typescript', 'ts'].includes(lang);
|
||||
}
|
||||
|
||||
async execute(code: string, language: string): Promise<ExecutionResult> {
|
||||
const startTime = performance.now();
|
||||
const lang = language.toLowerCase();
|
||||
|
||||
try {
|
||||
// TypeScript 需要先编译
|
||||
let executableCode = code;
|
||||
if (lang === 'typescript' || lang === 'ts') {
|
||||
executableCode = this.transpileTypeScript(code);
|
||||
}
|
||||
|
||||
// 在沙箱中执行
|
||||
const output = await this.executeInSandbox(executableCode);
|
||||
const executionTime = Math.round(performance.now() - startTime);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output.slice(0, MAX_OUTPUT_LENGTH),
|
||||
executionTime,
|
||||
engine: 'sandbox',
|
||||
};
|
||||
} catch (error) {
|
||||
const executionTime = Math.round(performance.now() - startTime);
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
executionTime,
|
||||
engine: 'sandbox',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private transpileTypeScript(code: string): string {
|
||||
// 简单的 TypeScript 转换(移除类型注解)
|
||||
// 实际项目中可以使用 @babel/standalone 或 typescript
|
||||
return code
|
||||
// 移除类型注解
|
||||
.replace(/:\s*\w+(\[\])?(\s*[=,)])/g, '$2')
|
||||
// 移除接口定义
|
||||
.replace(/interface\s+\w+\s*\{[^}]*\}/g, '')
|
||||
// 移除类型别名
|
||||
.replace(/type\s+\w+\s*=\s*[^;]+;/g, '')
|
||||
// 移除泛型
|
||||
.replace(/<[^>]+>/g, '')
|
||||
// 移除 as 类型断言
|
||||
.replace(/\s+as\s+\w+/g, '')
|
||||
// 移除 ! 非空断言
|
||||
.replace(/!\./g, '.')
|
||||
// 移除可选链前的类型
|
||||
.replace(/\?\./g, '?.');
|
||||
}
|
||||
|
||||
private executeInSandbox(code: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.abortController = new AbortController();
|
||||
const outputs: string[] = [];
|
||||
|
||||
// 创建隔离的 iframe
|
||||
this.iframe = document.createElement('iframe');
|
||||
this.iframe.style.display = 'none';
|
||||
this.iframe.sandbox.add('allow-scripts');
|
||||
document.body.appendChild(this.iframe);
|
||||
|
||||
const iframeWindow = this.iframe.contentWindow;
|
||||
if (!iframeWindow) {
|
||||
this.cleanup();
|
||||
reject(new Error('无法创建执行沙箱'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.cleanup();
|
||||
reject(new Error(`执行超时(${EXECUTION_TIMEOUT / 1000}秒)`));
|
||||
}, EXECUTION_TIMEOUT);
|
||||
|
||||
// 监听消息
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
if (event.source !== iframeWindow) return;
|
||||
|
||||
const { type, data } = event.data;
|
||||
|
||||
if (type === 'console') {
|
||||
outputs.push(data);
|
||||
} else if (type === 'done') {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
this.cleanup();
|
||||
resolve(outputs.join('\n'));
|
||||
} else if (type === 'error') {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
this.cleanup();
|
||||
reject(new Error(data));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
// 中止处理
|
||||
this.abortController.signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
this.cleanup();
|
||||
reject(new Error('执行已取消'));
|
||||
});
|
||||
|
||||
// 在 iframe 中执行代码
|
||||
const sandboxCode = `
|
||||
(function() {
|
||||
const outputs = [];
|
||||
|
||||
// 重写 console 方法
|
||||
const originalConsole = console;
|
||||
const customConsole = {
|
||||
log: (...args) => {
|
||||
const msg = args.map(a => {
|
||||
if (typeof a === 'object') {
|
||||
try {
|
||||
return JSON.stringify(a, null, 2);
|
||||
} catch {
|
||||
return String(a);
|
||||
}
|
||||
}
|
||||
return String(a);
|
||||
}).join(' ');
|
||||
parent.postMessage({ type: 'console', data: msg }, '*');
|
||||
},
|
||||
error: (...args) => customConsole.log('[Error]', ...args),
|
||||
warn: (...args) => customConsole.log('[Warn]', ...args),
|
||||
info: (...args) => customConsole.log(...args),
|
||||
debug: (...args) => customConsole.log('[Debug]', ...args),
|
||||
table: (data) => customConsole.log(JSON.stringify(data, null, 2)),
|
||||
clear: () => {},
|
||||
dir: (obj) => customConsole.log(obj),
|
||||
time: () => {},
|
||||
timeEnd: () => {},
|
||||
group: () => {},
|
||||
groupEnd: () => {},
|
||||
};
|
||||
|
||||
// 替换全局 console
|
||||
window.console = customConsole;
|
||||
|
||||
// 限制危险 API
|
||||
delete window.fetch;
|
||||
delete window.XMLHttpRequest;
|
||||
delete window.WebSocket;
|
||||
delete window.localStorage;
|
||||
delete window.sessionStorage;
|
||||
delete window.indexedDB;
|
||||
delete window.open;
|
||||
delete window.close;
|
||||
delete window.alert;
|
||||
delete window.confirm;
|
||||
delete window.prompt;
|
||||
|
||||
try {
|
||||
// 执行用户代码
|
||||
const result = eval(${JSON.stringify(code)});
|
||||
|
||||
// 如果有返回值且不是 undefined,输出它
|
||||
if (result !== undefined) {
|
||||
customConsole.log(result);
|
||||
}
|
||||
|
||||
parent.postMessage({ type: 'done' }, '*');
|
||||
} catch (error) {
|
||||
parent.postMessage({ type: 'error', data: error.message || String(error) }, '*');
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
// 写入并执行
|
||||
const iframeDoc = this.iframe.contentDocument;
|
||||
if (iframeDoc) {
|
||||
iframeDoc.open();
|
||||
iframeDoc.write(`<script>${sandboxCode}</script>`);
|
||||
iframeDoc.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
if (this.iframe && this.iframe.parentNode) {
|
||||
this.iframe.parentNode.removeChild(this.iframe);
|
||||
}
|
||||
this.iframe = null;
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const sandboxEngine = new SandboxEngine();
|
||||
96
src/lib/code-runner/index.ts
Normal file
96
src/lib/code-runner/index.ts
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 代码执行引擎主入口
|
||||
* 统一管理所有执行引擎
|
||||
*/
|
||||
|
||||
import { sandboxEngine } from './engines/sandbox';
|
||||
import { pyodideEngine, type PyodideLoadingCallback } from './engines/pyodide';
|
||||
import { remoteEngine } from './engines/remote';
|
||||
import type { ExecutionResult, IExecutionEngine, LanguageConfig } from './types';
|
||||
import { RUNNABLE_LANGUAGES, isRunnableLanguage, getLanguageConfig, isCodeExecutable } from './types';
|
||||
|
||||
// 所有引擎
|
||||
const engines: IExecutionEngine[] = [
|
||||
sandboxEngine,
|
||||
pyodideEngine,
|
||||
remoteEngine,
|
||||
];
|
||||
|
||||
/**
|
||||
* 执行代码
|
||||
*/
|
||||
export async function executeCode(
|
||||
code: string,
|
||||
language: string
|
||||
): Promise<ExecutionResult> {
|
||||
const normalizedLang = language.toLowerCase();
|
||||
|
||||
// 查找支持该语言的引擎
|
||||
const engine = engines.find((e) => e.supports(normalizedLang));
|
||||
|
||||
if (!engine) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `不支持的语言: ${language}`,
|
||||
executionTime: 0,
|
||||
engine: 'sandbox',
|
||||
};
|
||||
}
|
||||
|
||||
return engine.execute(code, normalizedLang);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有引擎的执行
|
||||
*/
|
||||
export function stopExecution(): void {
|
||||
engines.forEach((engine) => {
|
||||
if (engine.stop) {
|
||||
engine.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Pyodide 加载回调
|
||||
*/
|
||||
export function setPyodideLoadingCallbacks(callbacks: PyodideLoadingCallback | null): void {
|
||||
pyodideEngine.setLoadingCallbacks(callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载 Pyodide(可选)
|
||||
*/
|
||||
export async function preloadPyodide(): Promise<void> {
|
||||
await pyodideEngine.preload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Pyodide 是否已加载
|
||||
*/
|
||||
export function isPyodideLoaded(): boolean {
|
||||
return pyodideEngine.isLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Pyodide 是否正在加载
|
||||
*/
|
||||
export function isPyodideLoading(): boolean {
|
||||
return pyodideEngine.isLoading();
|
||||
}
|
||||
|
||||
// 导出类型和工具函数
|
||||
export {
|
||||
isRunnableLanguage,
|
||||
getLanguageConfig,
|
||||
isCodeExecutable,
|
||||
RUNNABLE_LANGUAGES,
|
||||
};
|
||||
|
||||
export type {
|
||||
ExecutionResult,
|
||||
LanguageConfig,
|
||||
IExecutionEngine,
|
||||
PyodideLoadingCallback,
|
||||
};
|
||||
274
src/lib/code-runner/types.ts
Normal file
274
src/lib/code-runner/types.ts
Normal file
@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 代码执行引擎类型定义
|
||||
*/
|
||||
|
||||
// 执行引擎类型
|
||||
export type EngineType = 'sandbox' | 'pyodide' | 'remote';
|
||||
|
||||
// 执行状态
|
||||
export type ExecutionStatus = 'idle' | 'running' | 'success' | 'error';
|
||||
|
||||
// 执行结果
|
||||
export interface ExecutionResult {
|
||||
success: boolean;
|
||||
output: string; // 标准输出
|
||||
error?: string; // 错误输出
|
||||
executionTime: number; // 执行时间(ms)
|
||||
engine: EngineType; // 使用的引擎
|
||||
}
|
||||
|
||||
// 语言配置
|
||||
export interface LanguageConfig {
|
||||
engine: EngineType;
|
||||
label: string;
|
||||
pistonLanguage?: string; // Piston API 的语言标识
|
||||
pistonVersion?: string; // Piston API 的版本
|
||||
fileExtension?: string; // 文件扩展名
|
||||
}
|
||||
|
||||
// 支持的可执行语言
|
||||
export const RUNNABLE_LANGUAGES: Record<string, LanguageConfig> = {
|
||||
// 前端沙箱执行
|
||||
javascript: {
|
||||
engine: 'sandbox',
|
||||
label: 'JavaScript',
|
||||
fileExtension: 'js',
|
||||
},
|
||||
js: {
|
||||
engine: 'sandbox',
|
||||
label: 'JavaScript',
|
||||
fileExtension: 'js',
|
||||
},
|
||||
typescript: {
|
||||
engine: 'sandbox',
|
||||
label: 'TypeScript',
|
||||
fileExtension: 'ts',
|
||||
},
|
||||
ts: {
|
||||
engine: 'sandbox',
|
||||
label: 'TypeScript',
|
||||
fileExtension: 'ts',
|
||||
},
|
||||
|
||||
// Pyodide 执行
|
||||
python: {
|
||||
engine: 'pyodide',
|
||||
label: 'Python',
|
||||
fileExtension: 'py',
|
||||
},
|
||||
py: {
|
||||
engine: 'pyodide',
|
||||
label: 'Python',
|
||||
fileExtension: 'py',
|
||||
},
|
||||
|
||||
// 远程 API 执行
|
||||
java: {
|
||||
engine: 'remote',
|
||||
label: 'Java',
|
||||
pistonLanguage: 'java',
|
||||
pistonVersion: '15.0.2',
|
||||
fileExtension: 'java',
|
||||
},
|
||||
go: {
|
||||
engine: 'remote',
|
||||
label: 'Go',
|
||||
pistonLanguage: 'go',
|
||||
pistonVersion: '1.16.2',
|
||||
fileExtension: 'go',
|
||||
},
|
||||
golang: {
|
||||
engine: 'remote',
|
||||
label: 'Go',
|
||||
pistonLanguage: 'go',
|
||||
pistonVersion: '1.16.2',
|
||||
fileExtension: 'go',
|
||||
},
|
||||
c: {
|
||||
engine: 'remote',
|
||||
label: 'C',
|
||||
pistonLanguage: 'c',
|
||||
pistonVersion: '10.2.0',
|
||||
fileExtension: 'c',
|
||||
},
|
||||
cpp: {
|
||||
engine: 'remote',
|
||||
label: 'C++',
|
||||
pistonLanguage: 'c++',
|
||||
pistonVersion: '10.2.0',
|
||||
fileExtension: 'cpp',
|
||||
},
|
||||
'c++': {
|
||||
engine: 'remote',
|
||||
label: 'C++',
|
||||
pistonLanguage: 'c++',
|
||||
pistonVersion: '10.2.0',
|
||||
fileExtension: 'cpp',
|
||||
},
|
||||
rust: {
|
||||
engine: 'remote',
|
||||
label: 'Rust',
|
||||
pistonLanguage: 'rust',
|
||||
pistonVersion: '1.68.2',
|
||||
fileExtension: 'rs',
|
||||
},
|
||||
rs: {
|
||||
engine: 'remote',
|
||||
label: 'Rust',
|
||||
pistonLanguage: 'rust',
|
||||
pistonVersion: '1.68.2',
|
||||
fileExtension: 'rs',
|
||||
},
|
||||
ruby: {
|
||||
engine: 'remote',
|
||||
label: 'Ruby',
|
||||
pistonLanguage: 'ruby',
|
||||
pistonVersion: '3.0.1',
|
||||
fileExtension: 'rb',
|
||||
},
|
||||
rb: {
|
||||
engine: 'remote',
|
||||
label: 'Ruby',
|
||||
pistonLanguage: 'ruby',
|
||||
pistonVersion: '3.0.1',
|
||||
fileExtension: 'rb',
|
||||
},
|
||||
php: {
|
||||
engine: 'remote',
|
||||
label: 'PHP',
|
||||
pistonLanguage: 'php',
|
||||
pistonVersion: '8.2.3',
|
||||
fileExtension: 'php',
|
||||
},
|
||||
csharp: {
|
||||
engine: 'remote',
|
||||
label: 'C#',
|
||||
pistonLanguage: 'csharp',
|
||||
pistonVersion: '6.12.0',
|
||||
fileExtension: 'cs',
|
||||
},
|
||||
cs: {
|
||||
engine: 'remote',
|
||||
label: 'C#',
|
||||
pistonLanguage: 'csharp',
|
||||
pistonVersion: '6.12.0',
|
||||
fileExtension: 'cs',
|
||||
},
|
||||
swift: {
|
||||
engine: 'remote',
|
||||
label: 'Swift',
|
||||
pistonLanguage: 'swift',
|
||||
pistonVersion: '5.3.3',
|
||||
fileExtension: 'swift',
|
||||
},
|
||||
kotlin: {
|
||||
engine: 'remote',
|
||||
label: 'Kotlin',
|
||||
pistonLanguage: 'kotlin',
|
||||
pistonVersion: '1.8.20',
|
||||
fileExtension: 'kt',
|
||||
},
|
||||
kt: {
|
||||
engine: 'remote',
|
||||
label: 'Kotlin',
|
||||
pistonLanguage: 'kotlin',
|
||||
pistonVersion: '1.8.20',
|
||||
fileExtension: 'kt',
|
||||
},
|
||||
};
|
||||
|
||||
// 检查语言是否可执行
|
||||
export function isRunnableLanguage(language: string): boolean {
|
||||
return language.toLowerCase() in RUNNABLE_LANGUAGES;
|
||||
}
|
||||
|
||||
// 获取语言配置
|
||||
export function getLanguageConfig(language: string): LanguageConfig | null {
|
||||
return RUNNABLE_LANGUAGES[language.toLowerCase()] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查代码是否满足运行的基本要求
|
||||
* 不同语言有不同的入口点要求
|
||||
*/
|
||||
export function isCodeExecutable(code: string, language: string): boolean {
|
||||
const lang = language.toLowerCase();
|
||||
const trimmedCode = code.trim();
|
||||
|
||||
// 如果代码为空,不可执行
|
||||
if (!trimmedCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (lang) {
|
||||
case 'java':
|
||||
// Java 需要 class 定义和 main 方法
|
||||
return /\bclass\s+\w+/.test(code) &&
|
||||
/public\s+static\s+void\s+main\s*\(\s*String\s*\[\s*\]\s*\w*\s*\)/.test(code);
|
||||
|
||||
case 'go':
|
||||
case 'golang':
|
||||
// Go 需要 package main 和 func main()
|
||||
return /package\s+main/.test(code) &&
|
||||
/func\s+main\s*\(\s*\)/.test(code);
|
||||
|
||||
case 'c':
|
||||
// C 需要 main 函数
|
||||
return /\bint\s+main\s*\(/.test(code) || /\bvoid\s+main\s*\(/.test(code);
|
||||
|
||||
case 'cpp':
|
||||
case 'c++':
|
||||
// C++ 需要 main 函数
|
||||
return /\bint\s+main\s*\(/.test(code) || /\bvoid\s+main\s*\(/.test(code);
|
||||
|
||||
case 'rust':
|
||||
case 'rs':
|
||||
// Rust 需要 fn main()
|
||||
return /fn\s+main\s*\(\s*\)/.test(code);
|
||||
|
||||
case 'csharp':
|
||||
case 'cs':
|
||||
// C# 需要 Main 方法或者是顶级语句(简单判断:有 class 或直接有语句)
|
||||
return /\bclass\s+\w+/.test(code) &&
|
||||
/\bstatic\s+void\s+Main\s*\(/.test(code) ||
|
||||
// 顶级语句:没有 class 但有实际代码
|
||||
(!/\bclass\s+\w+/.test(code) && /\w+\s*[;(]/.test(code));
|
||||
|
||||
case 'kotlin':
|
||||
case 'kt':
|
||||
// Kotlin 需要 fun main()
|
||||
return /fun\s+main\s*\(/.test(code);
|
||||
|
||||
case 'swift':
|
||||
// Swift 可以直接执行顶级代码,只要有语句即可
|
||||
return trimmedCode.length > 0;
|
||||
|
||||
case 'javascript':
|
||||
case 'js':
|
||||
case 'typescript':
|
||||
case 'ts':
|
||||
case 'python':
|
||||
case 'py':
|
||||
case 'ruby':
|
||||
case 'rb':
|
||||
case 'php':
|
||||
// 这些语言可以直接执行任何代码
|
||||
return true;
|
||||
|
||||
default:
|
||||
// 默认检查是否有代码
|
||||
return trimmedCode.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行引擎接口
|
||||
export interface IExecutionEngine {
|
||||
// 检查是否支持该语言
|
||||
supports(language: string): boolean;
|
||||
|
||||
// 执行代码
|
||||
execute(code: string, language: string): Promise<ExecutionResult>;
|
||||
|
||||
// 停止执行(可选)
|
||||
stop?(): void;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user