codeExecution.ts: - 实现 Pyodide + Piston 混合执行架构 - Python 图形代码使用 Pyodide 在浏览器端执行 - 其他代码使用 Piston API 在服务端执行 - 响应增加 images、engine、executionTime 字段 executor.ts: - 集成代码分析工具判断执行方式 - 支持返回 requiresPyodide 标记浏览器端执行需求 - 传递图片数据到执行结果
344 lines
9.8 KiB
TypeScript
344 lines
9.8 KiB
TypeScript
/**
|
||
* Code Execution 工具服务
|
||
* 混合架构:Pyodide (浏览器端 Python) + Piston API (服务端多语言)
|
||
*
|
||
* - Python + 图形绘制 → Pyodide (支持 matplotlib 渲染)
|
||
* - 其他语言/普通代码 → Piston API
|
||
*
|
||
* Piston API 文档: https://github.com/engineer-man/piston
|
||
* Pyodide 文档: https://pyodide.org/
|
||
*/
|
||
|
||
import {
|
||
shouldUsePyodide,
|
||
executePythonInPyodide,
|
||
type LoadingCallback,
|
||
type PyodideExecutionResult,
|
||
} from './pyodideRunner';
|
||
|
||
export interface CodeExecutionInput {
|
||
code: string;
|
||
language: string;
|
||
stdin?: string;
|
||
/** Pyodide 加载进度回调(可选) */
|
||
onProgress?: LoadingCallback;
|
||
}
|
||
|
||
export interface CodeExecutionResponse {
|
||
success: boolean;
|
||
output?: string;
|
||
error?: string;
|
||
language?: string;
|
||
version?: string;
|
||
/** Base64 编码的图片数组(matplotlib 输出) */
|
||
images?: string[];
|
||
/** 执行引擎: 'pyodide' | 'piston' */
|
||
engine?: 'pyodide' | 'piston';
|
||
/** 执行时间 (ms) */
|
||
executionTime?: number;
|
||
}
|
||
|
||
// Piston API 支持的语言映射
|
||
const LANGUAGE_MAP: Record<string, { language: string; version: string }> = {
|
||
python: { language: 'python', version: '3.10.0' },
|
||
python3: { language: 'python', version: '3.10.0' },
|
||
javascript: { language: 'javascript', version: '18.15.0' },
|
||
js: { language: 'javascript', version: '18.15.0' },
|
||
typescript: { language: 'typescript', version: '5.0.3' },
|
||
ts: { language: 'typescript', version: '5.0.3' },
|
||
java: { language: 'java', version: '15.0.2' },
|
||
c: { language: 'c', version: '10.2.0' },
|
||
cpp: { language: 'c++', version: '10.2.0' },
|
||
'c++': { language: 'c++', version: '10.2.0' },
|
||
go: { language: 'go', version: '1.16.2' },
|
||
rust: { language: 'rust', version: '1.68.2' },
|
||
ruby: { language: 'ruby', version: '3.0.1' },
|
||
php: { language: 'php', version: '8.2.3' },
|
||
swift: { language: 'swift', version: '5.3.3' },
|
||
kotlin: { language: 'kotlin', version: '1.8.20' },
|
||
scala: { language: 'scala', version: '3.2.2' },
|
||
r: { language: 'r', version: '4.1.1' },
|
||
bash: { language: 'bash', version: '5.2.0' },
|
||
shell: { language: 'bash', version: '5.2.0' },
|
||
sql: { language: 'sqlite3', version: '3.36.0' },
|
||
lua: { language: 'lua', version: '5.4.4' },
|
||
perl: { language: 'perl', version: '5.36.0' },
|
||
haskell: { language: 'haskell', version: '9.0.1' },
|
||
elixir: { language: 'elixir', version: '1.14.3' },
|
||
clojure: { language: 'clojure', version: '1.10.3' },
|
||
};
|
||
|
||
// Piston API 端点
|
||
const PISTON_API_URL = 'https://emkc.org/api/v2/piston/execute';
|
||
|
||
/**
|
||
* 执行代码
|
||
* 智能选择执行引擎:
|
||
* - Python + 图形代码 → Pyodide (浏览器端)
|
||
* - 其他情况 → Piston API (服务端)
|
||
*/
|
||
export async function executeCode(input: CodeExecutionInput): Promise<CodeExecutionResponse> {
|
||
const { code, language, stdin, onProgress } = input;
|
||
const startTime = Date.now();
|
||
|
||
// 检查是否应该使用 Pyodide(Python + 图形)
|
||
if (shouldUsePyodide(code, language)) {
|
||
return executePythonWithPyodide(code, onProgress, startTime);
|
||
}
|
||
|
||
// 使用 Piston API 执行
|
||
return executePythonWithPiston(code, language, stdin, startTime);
|
||
}
|
||
|
||
/**
|
||
* 使用 Pyodide 执行 Python 代码(支持 matplotlib 图形)
|
||
*/
|
||
async function executePythonWithPyodide(
|
||
code: string,
|
||
onProgress?: LoadingCallback,
|
||
startTime?: number
|
||
): Promise<CodeExecutionResponse> {
|
||
const start = startTime || Date.now();
|
||
|
||
try {
|
||
const result: PyodideExecutionResult = await executePythonInPyodide(code, onProgress);
|
||
|
||
return {
|
||
success: result.success,
|
||
output: result.output,
|
||
error: result.error,
|
||
language: 'python',
|
||
version: 'Pyodide (WebAssembly)',
|
||
images: result.images,
|
||
engine: 'pyodide',
|
||
executionTime: Date.now() - start,
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : '执行错误',
|
||
language: 'python',
|
||
engine: 'pyodide',
|
||
executionTime: Date.now() - start,
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用 Piston API 执行代码
|
||
*/
|
||
async function executePythonWithPiston(
|
||
code: string,
|
||
language: string,
|
||
stdin?: string,
|
||
startTime?: number
|
||
): Promise<CodeExecutionResponse> {
|
||
const start = startTime || Date.now();
|
||
|
||
// 获取语言映射
|
||
const langConfig = LANGUAGE_MAP[language.toLowerCase()];
|
||
if (!langConfig) {
|
||
return {
|
||
success: false,
|
||
error: `不支持的编程语言: ${language}。支持的语言: ${Object.keys(LANGUAGE_MAP).join(', ')}`,
|
||
engine: 'piston',
|
||
executionTime: Date.now() - start,
|
||
};
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(PISTON_API_URL, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
language: langConfig.language,
|
||
version: langConfig.version,
|
||
files: [
|
||
{
|
||
name: getFileName(langConfig.language),
|
||
content: code,
|
||
},
|
||
],
|
||
stdin: stdin || '',
|
||
args: [],
|
||
compile_timeout: 10000,
|
||
run_timeout: 5000,
|
||
compile_memory_limit: -1,
|
||
run_memory_limit: -1,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('Piston API error:', errorText);
|
||
return {
|
||
success: false,
|
||
error: `代码执行 API 错误: ${response.status}`,
|
||
engine: 'piston',
|
||
executionTime: Date.now() - start,
|
||
};
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// 检查编译错误
|
||
if (data.compile && data.compile.code !== 0) {
|
||
return {
|
||
success: false,
|
||
error: data.compile.stderr || data.compile.output || '编译错误',
|
||
language: langConfig.language,
|
||
version: langConfig.version,
|
||
engine: 'piston',
|
||
executionTime: Date.now() - start,
|
||
};
|
||
}
|
||
|
||
// 检查运行错误
|
||
if (data.run && data.run.code !== 0) {
|
||
return {
|
||
success: true, // 代码执行了,但有运行时错误
|
||
output: data.run.stdout || '',
|
||
error: data.run.stderr || '',
|
||
language: langConfig.language,
|
||
version: langConfig.version,
|
||
engine: 'piston',
|
||
executionTime: Date.now() - start,
|
||
};
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
output: data.run?.stdout || data.run?.output || '',
|
||
error: data.run?.stderr || '',
|
||
language: langConfig.language,
|
||
version: langConfig.version,
|
||
engine: 'piston',
|
||
executionTime: Date.now() - start,
|
||
};
|
||
} catch (error) {
|
||
console.error('Code execution error:', error);
|
||
return {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : '未知错误',
|
||
engine: 'piston',
|
||
executionTime: Date.now() - start,
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据语言获取文件名
|
||
*/
|
||
function getFileName(language: string): string {
|
||
const fileNameMap: Record<string, string> = {
|
||
python: 'main.py',
|
||
javascript: 'main.js',
|
||
typescript: 'main.ts',
|
||
java: 'Main.java',
|
||
c: 'main.c',
|
||
'c++': 'main.cpp',
|
||
go: 'main.go',
|
||
rust: 'main.rs',
|
||
ruby: 'main.rb',
|
||
php: 'main.php',
|
||
swift: 'main.swift',
|
||
kotlin: 'Main.kt',
|
||
scala: 'Main.scala',
|
||
r: 'main.r',
|
||
bash: 'main.sh',
|
||
sqlite3: 'main.sql',
|
||
lua: 'main.lua',
|
||
perl: 'main.pl',
|
||
haskell: 'Main.hs',
|
||
elixir: 'main.exs',
|
||
clojure: 'main.clj',
|
||
};
|
||
|
||
return fileNameMap[language] || 'main.txt';
|
||
}
|
||
|
||
/**
|
||
* 格式化代码执行结果为文本(完整版 - 发送给 AI)
|
||
*/
|
||
export function formatExecutionResult(response: CodeExecutionResponse): string {
|
||
if (!response.success && !response.output && !response.images?.length) {
|
||
return `❌ 执行失败: ${response.error}`;
|
||
}
|
||
|
||
let result = '';
|
||
|
||
// 执行引擎信息
|
||
if (response.engine) {
|
||
const engineName = response.engine === 'pyodide' ? 'Pyodide (浏览器)' : 'Piston (服务器)';
|
||
result += `**执行引擎**: ${engineName}\n`;
|
||
}
|
||
|
||
if (response.language && response.version) {
|
||
result += `**语言**: ${response.language} (${response.version})\n`;
|
||
}
|
||
|
||
if (response.executionTime) {
|
||
result += `**执行时间**: ${response.executionTime}ms\n`;
|
||
}
|
||
|
||
result += '\n';
|
||
|
||
if (response.output) {
|
||
result += `## 输出结果\n\`\`\`\n${response.output}\n\`\`\`\n`;
|
||
}
|
||
|
||
// 图片信息(仅标记有图片,实际渲染由前端处理)
|
||
if (response.images && response.images.length > 0) {
|
||
result += `\n## 图形输出\n已生成 ${response.images.length} 张图表\n`;
|
||
}
|
||
|
||
if (response.error) {
|
||
result += `\n## 错误信息\n\`\`\`\n${response.error}\n\`\`\`\n`;
|
||
}
|
||
|
||
return result || '执行完成,无输出';
|
||
}
|
||
|
||
/**
|
||
* 格式化代码执行结果为简短文本(显示给用户)
|
||
*/
|
||
export function formatExecutionResultShort(response: CodeExecutionResponse, language: string): string {
|
||
if (!response.success && !response.output && !response.images?.length) {
|
||
return `❌ 代码执行失败: ${response.error}`;
|
||
}
|
||
|
||
const langDisplay = response.language || language;
|
||
const engineDisplay = response.engine === 'pyodide' ? ' [Pyodide]' : '';
|
||
const timeDisplay = response.executionTime ? ` (${response.executionTime}ms)` : '';
|
||
|
||
if (response.error && !response.output && !response.images?.length) {
|
||
return `⚠️ ${langDisplay}${engineDisplay} 代码执行出错`;
|
||
}
|
||
|
||
// 计算输出行数
|
||
const outputLines = response.output?.split('\n').filter(l => l.trim()).length || 0;
|
||
const imageCount = response.images?.length || 0;
|
||
|
||
// 构建结果描述
|
||
const parts: string[] = [];
|
||
if (outputLines > 0) {
|
||
parts.push(`输出 ${outputLines} 行`);
|
||
}
|
||
if (imageCount > 0) {
|
||
parts.push(`生成 ${imageCount} 张图表`);
|
||
}
|
||
|
||
const resultDesc = parts.length > 0 ? parts.join(',') : '无输出';
|
||
|
||
return `✅ ${langDisplay}${engineDisplay} 代码执行完成${timeDisplay},${resultDesc}`;
|
||
}
|
||
|
||
/**
|
||
* 获取支持的语言列表
|
||
*/
|
||
export function getSupportedLanguages(): string[] {
|
||
return Object.keys(LANGUAGE_MAP);
|
||
}
|