Compare commits
4 Commits
2e5120dc72
...
269fc798aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
269fc798aa | ||
|
|
70902a2541 | ||
|
|
d6f2c47ddc | ||
|
|
192cd175da |
143
src/app/api/code/execute/route.ts
Normal file
143
src/app/api/code/execute/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Image from 'next/image';
|
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 {
|
interface CodeExecutionResultProps {
|
||||||
/** 执行输出文本 */
|
/** 执行输出文本 */
|
||||||
@ -13,7 +23,7 @@ interface CodeExecutionResultProps {
|
|||||||
/** 执行语言 */
|
/** 执行语言 */
|
||||||
language?: string;
|
language?: string;
|
||||||
/** 执行引擎 */
|
/** 执行引擎 */
|
||||||
engine?: 'pyodide' | 'piston';
|
engine?: EngineType | 'piston';
|
||||||
/** 执行时间 (ms) */
|
/** 执行时间 (ms) */
|
||||||
executionTime?: number;
|
executionTime?: number;
|
||||||
/** 是否执行成功 */
|
/** 是否执行成功 */
|
||||||
@ -34,6 +44,8 @@ export function CodeExecutionResult({
|
|||||||
success = true,
|
success = true,
|
||||||
}: CodeExecutionResultProps) {
|
}: CodeExecutionResultProps) {
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
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 hasOutput = output && output.trim().length > 0;
|
||||||
const hasError = error && error.trim().length > 0;
|
const hasError = error && error.trim().length > 0;
|
||||||
@ -44,78 +56,166 @@ export function CodeExecutionResult({
|
|||||||
return null;
|
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 (
|
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">
|
<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>
|
</span>
|
||||||
|
{/* 语言 */}
|
||||||
{language && (
|
{language && (
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
<span className="text-xs" style={{ color: 'var(--color-code-toolbar-text)' }}>
|
||||||
{language}
|
{language}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
{/* 执行时间 */}
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
{executionTime !== undefined && (
|
||||||
{engine && (
|
<span className="flex items-center gap-1 text-xs" style={{ color: 'var(--color-code-toolbar-text)' }}>
|
||||||
<span className="px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-600">
|
<Clock size={12} />
|
||||||
{engine === 'pyodide' ? 'Pyodide' : 'Piston'}
|
{executionTime}ms
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{executionTime && (
|
</div>
|
||||||
<span>{executionTime}ms</span>
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 图片输出 */}
|
{/* 内容区域(可折叠) */}
|
||||||
{hasImages && (
|
{isExpanded && (
|
||||||
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
|
<>
|
||||||
<div className="flex flex-wrap gap-3">
|
{/* 图片输出 */}
|
||||||
{images.map((img, index) => (
|
{hasImages && (
|
||||||
<div
|
<div className="p-3" style={{ borderBottom: '1px solid var(--color-code-border)' }}>
|
||||||
key={index}
|
<div className="flex flex-wrap gap-3">
|
||||||
className="relative cursor-pointer hover:opacity-90 transition-opacity"
|
{images.map((img, index) => (
|
||||||
onClick={() => setSelectedImage(img)}
|
<div
|
||||||
>
|
key={index}
|
||||||
<Image
|
className="relative cursor-pointer hover:opacity-90 transition-opacity"
|
||||||
src={`data:image/png;base64,${img}`}
|
onClick={() => setSelectedImage(img)}
|
||||||
alt={`Chart ${index + 1}`}
|
>
|
||||||
width={400}
|
<Image
|
||||||
height={300}
|
src={img.startsWith('data:') ? img : `data:image/png;base64,${img}`}
|
||||||
className="rounded-lg shadow-md max-w-full h-auto"
|
alt={`Chart ${index + 1}`}
|
||||||
style={{ maxHeight: '300px', objectFit: 'contain' }}
|
width={400}
|
||||||
/>
|
height={300}
|
||||||
<div className="absolute bottom-2 right-2 px-2 py-1 bg-black/50 text-white text-xs rounded">
|
className="rounded-lg shadow-md max-w-full h-auto"
|
||||||
图表 {index + 1}
|
style={{ maxHeight: '300px', objectFit: 'contain' }}
|
||||||
</div>
|
/>
|
||||||
|
<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>
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 文本输出 */}
|
{/* 文本输出 */}
|
||||||
{hasOutput && (
|
{hasOutput && (
|
||||||
<div className="p-3">
|
<div className="p-4">
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">输出</div>
|
<pre
|
||||||
<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">
|
className="text-sm whitespace-pre-wrap font-mono max-h-60 overflow-auto"
|
||||||
{output}
|
style={{ color: 'var(--color-code-text)' }}
|
||||||
</pre>
|
>
|
||||||
</div>
|
{output}
|
||||||
)}
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 错误信息 */}
|
{/* 错误信息 */}
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<div className="p-3 bg-red-50 dark:bg-red-900/20">
|
<div className="p-4" style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)' }}>
|
||||||
<div className="text-xs text-red-600 dark:text-red-400 mb-1">错误</div>
|
<pre className="text-sm text-red-500 whitespace-pre-wrap font-mono max-h-60 overflow-auto">
|
||||||
<pre className="text-sm text-red-700 dark:text-red-300 whitespace-pre-wrap font-mono">
|
{error}
|
||||||
{error}
|
</pre>
|
||||||
</pre>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 图片放大模态框 */}
|
{/* 图片放大模态框 */}
|
||||||
|
|||||||
@ -1,10 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { Copy, Check, Eye, ChevronDown, ChevronUp } from 'lucide-react';
|
import { Copy, Check, Eye, ChevronDown, ChevronUp, Play, Square, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import { HtmlPreviewModal } from '@/components/ui/HtmlPreviewModal';
|
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-javascript';
|
||||||
import 'prismjs/components/prism-typescript';
|
import 'prismjs/components/prism-typescript';
|
||||||
import 'prismjs/components/prism-jsx';
|
import 'prismjs/components/prism-jsx';
|
||||||
@ -24,6 +34,10 @@ import 'prismjs/components/prism-markdown';
|
|||||||
import 'prismjs/components/prism-css';
|
import 'prismjs/components/prism-css';
|
||||||
import 'prismjs/components/prism-scss';
|
import 'prismjs/components/prism-scss';
|
||||||
import 'prismjs/components/prism-markup';
|
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 {
|
interface CodeBlockProps {
|
||||||
code: string;
|
code: string;
|
||||||
@ -62,6 +76,15 @@ export function CodeBlock({
|
|||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
const [isExpanded, setIsExpanded] = 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();
|
const normalizedLanguage = languageAliases[language.toLowerCase()] || language.toLowerCase();
|
||||||
|
|
||||||
@ -69,6 +92,10 @@ export function CodeBlock({
|
|||||||
const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) ||
|
const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) ||
|
||||||
['html', 'htm'].includes(language.toLowerCase());
|
['html', 'htm'].includes(language.toLowerCase());
|
||||||
|
|
||||||
|
// 判断是否可执行:语言支持 + 代码满足执行条件
|
||||||
|
const canRun = isRunnableLanguage(language) && isCodeExecutable(code, language);
|
||||||
|
const languageConfig = canRun ? getLanguageConfig(language) : null;
|
||||||
|
|
||||||
const lines = code.split('\n');
|
const lines = code.split('\n');
|
||||||
const totalLines = lines.length;
|
const totalLines = lines.length;
|
||||||
const shouldCollapse = totalLines > maxCollapsedLines;
|
const shouldCollapse = totalLines > maxCollapsedLines;
|
||||||
@ -86,11 +113,20 @@ export function CodeBlock({
|
|||||||
|
|
||||||
// 使用 useMemo 缓存高亮后的 HTML,避免频繁重新高亮
|
// 使用 useMemo 缓存高亮后的 HTML,避免频繁重新高亮
|
||||||
const highlightedCode = useMemo(() => {
|
const highlightedCode = useMemo(() => {
|
||||||
const grammar = Prism.languages[normalizedLanguage];
|
try {
|
||||||
if (grammar) {
|
const grammar = Prism.languages[normalizedLanguage];
|
||||||
return Prism.highlight(displayCode, grammar, 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
}, [displayCode, normalizedLanguage]);
|
}, [displayCode, normalizedLanguage]);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
@ -108,6 +144,67 @@ export function CodeBlock({
|
|||||||
setIsExpanded(!isExpanded);
|
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 (
|
return (
|
||||||
<div className={cn('relative group rounded overflow-hidden my-4', className)}
|
<div className={cn('relative group rounded overflow-hidden my-4', className)}
|
||||||
style={{ boxShadow: 'var(--color-code-shadow)' }}>
|
style={{ boxShadow: 'var(--color-code-shadow)' }}>
|
||||||
@ -141,6 +238,35 @@ export function CodeBlock({
|
|||||||
|
|
||||||
{/* 右侧:操作按钮 */}
|
{/* 右侧:操作按钮 */}
|
||||||
<div className="flex items-center gap-2">
|
<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 预览按钮 */}
|
{/* HTML 预览按钮 */}
|
||||||
{isHtmlPreviewable && (
|
{isHtmlPreviewable && (
|
||||||
<button
|
<button
|
||||||
@ -265,6 +391,47 @@ export function CodeBlock({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 预览模态框 */}
|
{/* HTML 预览模态框 */}
|
||||||
{isHtmlPreviewable && (
|
{isHtmlPreviewable && (
|
||||||
<HtmlPreviewModal
|
<HtmlPreviewModal
|
||||||
|
|||||||
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