Compare commits

...

4 Commits

Author SHA1 Message Date
gaoziman
269fc798aa feat(代码块): 集成代码运行功能
- 添加运行/停止按钮交互
- 显示 Pyodide 加载进度(Python)
- 集成执行结果展示组件
- 添加更多语言的 Prism 语法高亮支持
- 优化高亮失败的降级处理
2025-12-21 03:20:37 +08:00
gaoziman
70902a2541 refactor(执行结果): 重构代码执行结果组件
- 使用 CSS 变量适配亮暗主题
- 添加结果折叠/展开功能
- 添加输出复制功能
- 优化成功/错误状态显示样式
- 显示执行时间和引擎信息
2025-12-21 03:20:31 +08:00
gaoziman
d6f2c47ddc feat(API): 添加代码执行 API 路由
- 创建 /api/code/execute 接口
- 集成 Piston API 支持多语言远程执行
- 添加执行超时和错误处理
2025-12-21 03:20:24 +08:00
gaoziman
192cd175da feat(代码运行): 添加多语言代码执行引擎
- 实现 sandbox 引擎支持 JavaScript/TypeScript 执行
- 实现 pyodide 引擎支持 Python 浏览器端执行
- 实现 remote 引擎支持 Java/Go/C/C++/Rust 等远程执行
- 添加语言配置和入口点检测逻辑
- 支持执行状态回调和加载进度显示
2025-12-21 03:20:16 +08:00
8 changed files with 1583 additions and 62 deletions

View File

@ -0,0 +1,143 @@
/**
* Piston API
* CORS Piston API
*/
import { NextRequest, NextResponse } from 'next/server';
// Piston API 公共端点
const PISTON_API_URL = 'https://emkc.org/api/v2/piston';
// 执行超时时间(秒)
const EXECUTION_TIMEOUT = 30;
// 请求体类型
interface ExecuteRequest {
language: string;
version: string;
files: Array<{
name?: string;
content: string;
}>;
stdin?: string;
args?: string[];
compile_args?: string[];
run_args?: string[];
compile_timeout?: number;
run_timeout?: number;
compile_memory_limit?: number;
run_memory_limit?: number;
}
// 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;
};
}
export async function POST(request: NextRequest) {
try {
const body: ExecuteRequest = await request.json();
// 验证请求
if (!body.language || !body.files || body.files.length === 0) {
return NextResponse.json(
{ error: '缺少必要参数: language, files' },
{ status: 400 }
);
}
// 设置超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), EXECUTION_TIMEOUT * 1000);
// 构建请求
const pistonRequest: ExecuteRequest = {
language: body.language,
version: body.version || '*',
files: body.files,
stdin: body.stdin || '',
args: body.args || [],
compile_args: body.compile_args || [],
run_args: body.run_args || [],
compile_timeout: body.compile_timeout || 10000,
run_timeout: body.run_timeout || 10000,
compile_memory_limit: body.compile_memory_limit || -1,
run_memory_limit: body.run_memory_limit || -1,
};
// 发送请求到 Piston API
const response = await fetch(`${PISTON_API_URL}/execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(pistonRequest),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
return NextResponse.json(
{ error: `Piston API 错误: ${response.status} - ${errorText}` },
{ status: response.status }
);
}
const result: PistonResponse = await response.json();
return NextResponse.json(result);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return NextResponse.json(
{ error: `执行超时(${EXECUTION_TIMEOUT}秒)` },
{ status: 408 }
);
}
console.error('Piston API proxy error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : '执行失败' },
{ status: 500 }
);
}
}
// 获取支持的语言列表
export async function GET() {
try {
const response = await fetch(`${PISTON_API_URL}/runtimes`);
if (!response.ok) {
return NextResponse.json(
{ error: '获取语言列表失败' },
{ status: response.status }
);
}
const runtimes = await response.json();
return NextResponse.json(runtimes);
} catch (error) {
console.error('Failed to fetch runtimes:', error);
return NextResponse.json(
{ error: '获取语言列表失败' },
{ status: 500 }
);
}
}

View File

@ -2,6 +2,16 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { Check, X, Copy, ChevronDown, ChevronUp, Loader2, Clock } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ExecutionResult, EngineType } from '@/lib/code-runner/types';
// 引擎名称映射
const engineLabels: Record<EngineType, string> = {
sandbox: 'JavaScript',
pyodide: 'Python',
remote: '云端',
};
interface CodeExecutionResultProps {
/** 执行输出文本 */
@ -13,7 +23,7 @@ interface CodeExecutionResultProps {
/** 执行语言 */
language?: string;
/** 执行引擎 */
engine?: 'pyodide' | 'piston';
engine?: EngineType | 'piston';
/** 执行时间 (ms) */
executionTime?: number;
/** 是否执行成功 */
@ -34,6 +44,8 @@ export function CodeExecutionResult({
success = true,
}: CodeExecutionResultProps) {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [isExpanded, setIsExpanded] = useState(true);
const [copied, setCopied] = useState(false);
const hasOutput = output && output.trim().length > 0;
const hasError = error && error.trim().length > 0;
@ -44,78 +56,166 @@ export function CodeExecutionResult({
return null;
}
// 复制输出
const handleCopy = async () => {
const textToCopy = success ? output : error || '';
try {
await navigator.clipboard.writeText(textToCopy || '');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
// 获取引擎显示名称
const getEngineLabel = () => {
if (!engine) return '';
if (engine === 'piston') return '云端';
return engineLabels[engine] || engine;
};
return (
<div className="code-execution-result mt-3 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden bg-gray-50 dark:bg-gray-800">
<div
className={cn(
'code-execution-result mt-3 rounded border overflow-hidden',
success
? 'border-green-500/30'
: 'border-red-500/30'
)}
style={{
backgroundColor: 'var(--color-code-bg)',
borderColor: success ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)',
}}
>
{/* 头部信息 */}
<div className="flex items-center justify-between px-3 py-2 bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<div
className="flex items-center justify-between px-4 py-2.5 cursor-pointer"
style={{
backgroundColor: 'var(--color-code-toolbar-bg)',
borderBottom: '1px solid var(--color-code-border)',
}}
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${success ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{success ? '执行成功' : '执行失败'}
{/* 状态图标 */}
{success ? (
<Check size={16} className="text-green-500" />
) : (
<X size={16} className="text-red-500" />
)}
{/* 状态文本 */}
<span
className={cn(
'text-sm font-medium',
success ? 'text-green-500' : 'text-red-500'
)}
>
{success ? '输出' : '错误'}
</span>
{/* 语言 */}
{language && (
<span className="text-xs text-gray-500 dark:text-gray-400">
<span className="text-xs" style={{ color: 'var(--color-code-toolbar-text)' }}>
{language}
</span>
)}
</div>
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
{engine && (
<span className="px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-600">
{engine === 'pyodide' ? 'Pyodide' : 'Piston'}
{/* 执行时间 */}
{executionTime !== undefined && (
<span className="flex items-center gap-1 text-xs" style={{ color: 'var(--color-code-toolbar-text)' }}>
<Clock size={12} />
{executionTime}ms
</span>
)}
{executionTime && (
<span>{executionTime}ms</span>
</div>
<div className="flex items-center gap-2">
{/* 引擎标签 */}
{engine && (
<span className="text-xs hidden sm:inline" style={{ color: 'var(--color-code-toolbar-text)' }}>
{getEngineLabel()}
</span>
)}
{/* 复制按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
handleCopy();
}}
className="p-1 rounded transition-colors hover:bg-white/10"
style={{ color: 'var(--color-code-toolbar-text)' }}
title={copied ? '已复制' : '复制输出'}
>
{copied ? (
<Check size={14} className="text-green-500" />
) : (
<Copy size={14} />
)}
</button>
{/* 展开/收起按钮 */}
<button
className="p-1 rounded transition-colors"
style={{ color: 'var(--color-code-toolbar-text)' }}
>
{isExpanded ? (
<ChevronUp size={14} />
) : (
<ChevronDown size={14} />
)}
</button>
</div>
</div>
{/* 图片输出 */}
{hasImages && (
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex flex-wrap gap-3">
{images.map((img, index) => (
<div
key={index}
className="relative cursor-pointer hover:opacity-90 transition-opacity"
onClick={() => setSelectedImage(img)}
>
<Image
src={`data:image/png;base64,${img}`}
alt={`Chart ${index + 1}`}
width={400}
height={300}
className="rounded-lg shadow-md max-w-full h-auto"
style={{ maxHeight: '300px', objectFit: 'contain' }}
/>
<div className="absolute bottom-2 right-2 px-2 py-1 bg-black/50 text-white text-xs rounded">
{index + 1}
</div>
{/* 内容区域(可折叠) */}
{isExpanded && (
<>
{/* 图片输出 */}
{hasImages && (
<div className="p-3" style={{ borderBottom: '1px solid var(--color-code-border)' }}>
<div className="flex flex-wrap gap-3">
{images.map((img, index) => (
<div
key={index}
className="relative cursor-pointer hover:opacity-90 transition-opacity"
onClick={() => setSelectedImage(img)}
>
<Image
src={img.startsWith('data:') ? img : `data:image/png;base64,${img}`}
alt={`Chart ${index + 1}`}
width={400}
height={300}
className="rounded-lg shadow-md max-w-full h-auto"
style={{ maxHeight: '300px', objectFit: 'contain' }}
/>
<div className="absolute bottom-2 right-2 px-2 py-1 bg-black/50 text-white text-xs rounded">
{index + 1}
</div>
</div>
))}
</div>
))}
</div>
</div>
)}
</div>
)}
{/* 文本输出 */}
{hasOutput && (
<div className="p-3">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1"></div>
<pre className="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap font-mono bg-white dark:bg-gray-900 p-2 rounded border border-gray-200 dark:border-gray-700 max-h-60 overflow-auto">
{output}
</pre>
</div>
)}
{/* 文本输出 */}
{hasOutput && (
<div className="p-4">
<pre
className="text-sm whitespace-pre-wrap font-mono max-h-60 overflow-auto"
style={{ color: 'var(--color-code-text)' }}
>
{output}
</pre>
</div>
)}
{/* 错误信息 */}
{hasError && (
<div className="p-3 bg-red-50 dark:bg-red-900/20">
<div className="text-xs text-red-600 dark:text-red-400 mb-1"></div>
<pre className="text-sm text-red-700 dark:text-red-300 whitespace-pre-wrap font-mono">
{error}
</pre>
</div>
{/* 错误信息 */}
{hasError && (
<div className="p-4" style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)' }}>
<pre className="text-sm text-red-500 whitespace-pre-wrap font-mono max-h-60 overflow-auto">
{error}
</pre>
</div>
)}
</>
)}
{/* 图片放大模态框 */}

View File

@ -1,10 +1,20 @@
'use client';
import { useEffect, useRef, useState, useMemo } from 'react';
import { Copy, Check, Eye, ChevronDown, ChevronUp } from 'lucide-react';
import { useState, useMemo, useCallback } from 'react';
import { Copy, Check, Eye, ChevronDown, ChevronUp, Play, Square, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import Prism from 'prismjs';
import { HtmlPreviewModal } from '@/components/ui/HtmlPreviewModal';
import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult';
import {
executeCode,
stopExecution,
isRunnableLanguage,
isCodeExecutable,
getLanguageConfig,
setPyodideLoadingCallbacks,
type ExecutionResult,
} from '@/lib/code-runner';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-jsx';
@ -24,6 +34,10 @@ import 'prismjs/components/prism-markdown';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-scss';
import 'prismjs/components/prism-markup';
import 'prismjs/components/prism-kotlin';
import 'prismjs/components/prism-swift';
import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-php';
interface CodeBlockProps {
code: string;
@ -62,6 +76,15 @@ export function CodeBlock({
const [previewOpen, setPreviewOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
// 代码执行相关状态
const [isRunning, setIsRunning] = useState(false);
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null);
const [pyodideStatus, setPyodideStatus] = useState<{
stage: 'loading' | 'ready' | 'error';
message: string;
progress?: number;
} | null>(null);
// 规范化语言名称
const normalizedLanguage = languageAliases[language.toLowerCase()] || language.toLowerCase();
@ -69,6 +92,10 @@ export function CodeBlock({
const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) ||
['html', 'htm'].includes(language.toLowerCase());
// 判断是否可执行:语言支持 + 代码满足执行条件
const canRun = isRunnableLanguage(language) && isCodeExecutable(code, language);
const languageConfig = canRun ? getLanguageConfig(language) : null;
const lines = code.split('\n');
const totalLines = lines.length;
const shouldCollapse = totalLines > maxCollapsedLines;
@ -86,11 +113,20 @@ export function CodeBlock({
// 使用 useMemo 缓存高亮后的 HTML避免频繁重新高亮
const highlightedCode = useMemo(() => {
const grammar = Prism.languages[normalizedLanguage];
if (grammar) {
return Prism.highlight(displayCode, grammar, normalizedLanguage);
try {
const grammar = Prism.languages[normalizedLanguage];
if (grammar) {
return Prism.highlight(displayCode, grammar, normalizedLanguage);
}
} catch (error) {
// 某些语言的 grammar 可能不完整fallback 到纯文本
console.warn(`Prism highlight failed for ${normalizedLanguage}:`, error);
}
return displayCode;
// 对纯文本进行 HTML 转义
return displayCode
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}, [displayCode, normalizedLanguage]);
const handleCopy = async () => {
@ -108,6 +144,67 @@ export function CodeBlock({
setIsExpanded(!isExpanded);
};
// 运行代码
const handleRun = useCallback(async () => {
if (isRunning || !canRun) return;
setIsRunning(true);
setExecutionResult(null);
// 如果是 Python设置 Pyodide 加载回调
if (languageConfig?.engine === 'pyodide') {
setPyodideLoadingCallbacks({
onLoadingStart: () => {
setPyodideStatus({
stage: 'loading',
message: '正在加载 Python 运行时...',
progress: 0,
});
},
onLoadingProgress: (message, progress) => {
setPyodideStatus({
stage: 'loading',
message,
progress,
});
},
onLoadingComplete: () => {
setPyodideStatus(null);
},
onLoadingError: (error) => {
setPyodideStatus({
stage: 'error',
message: error,
});
},
});
}
try {
const result = await executeCode(code, language);
setExecutionResult(result);
} catch (error) {
setExecutionResult({
success: false,
output: '',
error: error instanceof Error ? error.message : '执行失败',
executionTime: 0,
engine: languageConfig?.engine || 'sandbox',
});
} finally {
setIsRunning(false);
setPyodideStatus(null);
setPyodideLoadingCallbacks(null);
}
}, [code, language, isRunning, canRun, languageConfig]);
// 停止执行
const handleStop = useCallback(() => {
stopExecution();
setIsRunning(false);
setPyodideStatus(null);
}, []);
return (
<div className={cn('relative group rounded overflow-hidden my-4', className)}
style={{ boxShadow: 'var(--color-code-shadow)' }}>
@ -141,6 +238,35 @@ export function CodeBlock({
{/* 右侧:操作按钮 */}
<div className="flex items-center gap-2">
{/* 运行按钮 */}
{canRun && (
<button
onClick={isRunning ? handleStop : handleRun}
disabled={!canRun}
className={cn(
'inline-flex items-center gap-1.5 px-2 py-1 rounded transition-colors',
isRunning
? 'hover:bg-red-500/20'
: 'hover:bg-white/10'
)}
style={{
color: isRunning ? '#ef4444' : 'var(--color-primary)',
}}
title={isRunning ? '停止执行' : `运行 ${languageConfig?.label || language}`}
>
{isRunning ? (
<>
<Square size={14} />
<span></span>
</>
) : (
<>
<Play size={14} />
<span></span>
</>
)}
</button>
)}
{/* HTML 预览按钮 */}
{isHtmlPreviewable && (
<button
@ -265,6 +391,47 @@ export function CodeBlock({
)}
</div>
{/* Pyodide 加载状态Python */}
{pyodideStatus && (
<PyodideLoading
stage={pyodideStatus.stage}
message={pyodideStatus.message}
progress={pyodideStatus.progress}
/>
)}
{/* 代码执行结果 */}
{(executionResult || isRunning) && (
<div className="px-0">
{isRunning && !pyodideStatus ? (
<div
className="mt-2 rounded border overflow-hidden"
style={{
backgroundColor: 'var(--color-code-bg)',
borderColor: 'var(--color-code-border)',
}}
>
<div
className="px-4 py-3 flex items-center gap-2"
style={{ color: 'var(--color-code-toolbar-text)' }}
>
<Loader2 size={16} className="animate-spin" style={{ color: 'var(--color-primary)' }} />
<span className="text-sm"> {languageConfig?.label || language} ...</span>
</div>
</div>
) : executionResult && (
<CodeExecutionResult
output={executionResult.output}
error={executionResult.error}
language={languageConfig?.label || language}
engine={executionResult.engine}
executionTime={executionResult.executionTime}
success={executionResult.success}
/>
)}
</div>
)}
{/* HTML 预览模态框 */}
{isHtmlPreviewable && (
<HtmlPreviewModal

View 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();

View File

@ -0,0 +1,252 @@
/**
*
* Piston API JavaGoC/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();

View 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();

View 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,
};

View 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;
}