claude-code-cchui/src/services/tools/codeExecution.ts
gaoziman 68ba9b3204 feat(工具): 实现混合代码执行架构
codeExecution.ts:
- 实现 Pyodide + Piston 混合执行架构
- Python 图形代码使用 Pyodide 在浏览器端执行
- 其他代码使用 Piston API 在服务端执行
- 响应增加 images、engine、executionTime 字段

executor.ts:
- 集成代码分析工具判断执行方式
- 支持返回 requiresPyodide 标记浏览器端执行需求
- 传递图片数据到执行结果
2025-12-19 20:18:58 +08:00

344 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
// 检查是否应该使用 PyodidePython + 图形)
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);
}