diff --git a/src/lib/code-runner/engines/pyodide.ts b/src/lib/code-runner/engines/pyodide.ts new file mode 100644 index 0000000..336947b --- /dev/null +++ b/src/lib/code-runner/engines/pyodide.ts @@ -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; + loadPackage(packages: string | string[]): Promise; + loadPackagesFromImports(code: string): Promise; + globals: Map; +} + +// 加载状态回调 +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 | 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 { + if (!this.pyodide && !this.loading) { + await this.loadPyodide(); + } + } + + async execute(code: string, language: string): Promise { + 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 { + 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(async (resolve, reject) => { + try { + // 动态加载 Pyodide 脚本 + if (typeof window !== 'undefined' && !(window as unknown as Record).loadPyodide) { + this.loadingCallbacks?.onLoadingProgress?.('正在下载 Pyodide...', 20); + await this.loadScript(`${PYODIDE_CDN}pyodide.js`); + } + + this.loadingCallbacks?.onLoadingProgress?.('正在初始化 Python 环境...', 50); + + // 初始化 Pyodide + const loadPyodide = (window as unknown as { loadPyodide: (config: { indexURL: string }) => Promise }).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 { + 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 { + 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 "", /g, '') + .trim(); + + return message; + } + return String(error); + } + + // 加载额外的 Python 包 + async loadPackages(packages: string[]): Promise { + if (!this.pyodide) { + await this.loadPyodide(); + } + + if (this.pyodide) { + await this.pyodide.loadPackage(packages); + } + } +} + +// 导出单例 +export const pyodideEngine = new PyodideEngine(); diff --git a/src/lib/code-runner/engines/remote.ts b/src/lib/code-runner/engines/remote.ts new file mode 100644 index 0000000..d49706e --- /dev/null +++ b/src/lib/code-runner/engines/remote.ts @@ -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 { + 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(); diff --git a/src/lib/code-runner/engines/sandbox.ts b/src/lib/code-runner/engines/sandbox.ts new file mode 100644 index 0000000..857437e --- /dev/null +++ b/src/lib/code-runner/engines/sandbox.ts @@ -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 { + 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 { + 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(``); + 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(); diff --git a/src/lib/code-runner/index.ts b/src/lib/code-runner/index.ts new file mode 100644 index 0000000..ed7252b --- /dev/null +++ b/src/lib/code-runner/index.ts @@ -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 { + 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 { + 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, +}; diff --git a/src/lib/code-runner/types.ts b/src/lib/code-runner/types.ts new file mode 100644 index 0000000..58fa3f7 --- /dev/null +++ b/src/lib/code-runner/types.ts @@ -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 = { + // 前端沙箱执行 + 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; + + // 停止执行(可选) + stop?(): void; +}