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