feat(工具): 实现混合代码执行架构

codeExecution.ts:
- 实现 Pyodide + Piston 混合执行架构
- Python 图形代码使用 Pyodide 在浏览器端执行
- 其他代码使用 Piston API 在服务端执行
- 响应增加 images、engine、executionTime 字段

executor.ts:
- 集成代码分析工具判断执行方式
- 支持返回 requiresPyodide 标记浏览器端执行需求
- 传递图片数据到执行结果
This commit is contained in:
gaoziman 2025-12-19 20:18:58 +08:00
parent ef45e14534
commit 68ba9b3204
2 changed files with 166 additions and 15 deletions

View File

@ -1,13 +1,27 @@
/** /**
* Code Execution * Code Execution
* 使 Piston API * Pyodide ( Python) + Piston API ()
*
* - Python + Pyodide ( matplotlib )
* - / Piston API
*
* Piston API 文档: https://github.com/engineer-man/piston * Piston API 文档: https://github.com/engineer-man/piston
* Pyodide 文档: https://pyodide.org/
*/ */
import {
shouldUsePyodide,
executePythonInPyodide,
type LoadingCallback,
type PyodideExecutionResult,
} from './pyodideRunner';
export interface CodeExecutionInput { export interface CodeExecutionInput {
code: string; code: string;
language: string; language: string;
stdin?: string; stdin?: string;
/** Pyodide 加载进度回调(可选) */
onProgress?: LoadingCallback;
} }
export interface CodeExecutionResponse { export interface CodeExecutionResponse {
@ -16,6 +30,12 @@ export interface CodeExecutionResponse {
error?: string; error?: string;
language?: string; language?: string;
version?: string; version?: string;
/** Base64 编码的图片数组matplotlib 输出) */
images?: string[];
/** 执行引擎: 'pyodide' | 'piston' */
engine?: 'pyodide' | 'piston';
/** 执行时间 (ms) */
executionTime?: number;
} }
// Piston API 支持的语言映射 // Piston API 支持的语言映射
@ -53,9 +73,67 @@ const PISTON_API_URL = 'https://emkc.org/api/v2/piston/execute';
/** /**
* *
*
* - Python + Pyodide ()
* - Piston API ()
*/ */
export async function executeCode(input: CodeExecutionInput): Promise<CodeExecutionResponse> { export async function executeCode(input: CodeExecutionInput): Promise<CodeExecutionResponse> {
const { code, language, stdin } = input; 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()]; const langConfig = LANGUAGE_MAP[language.toLowerCase()];
@ -63,6 +141,8 @@ export async function executeCode(input: CodeExecutionInput): Promise<CodeExecut
return { return {
success: false, success: false,
error: `不支持的编程语言: ${language}。支持的语言: ${Object.keys(LANGUAGE_MAP).join(', ')}`, error: `不支持的编程语言: ${language}。支持的语言: ${Object.keys(LANGUAGE_MAP).join(', ')}`,
engine: 'piston',
executionTime: Date.now() - start,
}; };
} }
@ -96,6 +176,8 @@ export async function executeCode(input: CodeExecutionInput): Promise<CodeExecut
return { return {
success: false, success: false,
error: `代码执行 API 错误: ${response.status}`, error: `代码执行 API 错误: ${response.status}`,
engine: 'piston',
executionTime: Date.now() - start,
}; };
} }
@ -108,6 +190,8 @@ export async function executeCode(input: CodeExecutionInput): Promise<CodeExecut
error: data.compile.stderr || data.compile.output || '编译错误', error: data.compile.stderr || data.compile.output || '编译错误',
language: langConfig.language, language: langConfig.language,
version: langConfig.version, version: langConfig.version,
engine: 'piston',
executionTime: Date.now() - start,
}; };
} }
@ -119,6 +203,8 @@ export async function executeCode(input: CodeExecutionInput): Promise<CodeExecut
error: data.run.stderr || '', error: data.run.stderr || '',
language: langConfig.language, language: langConfig.language,
version: langConfig.version, version: langConfig.version,
engine: 'piston',
executionTime: Date.now() - start,
}; };
} }
@ -128,12 +214,16 @@ export async function executeCode(input: CodeExecutionInput): Promise<CodeExecut
error: data.run?.stderr || '', error: data.run?.stderr || '',
language: langConfig.language, language: langConfig.language,
version: langConfig.version, version: langConfig.version,
engine: 'piston',
executionTime: Date.now() - start,
}; };
} catch (error) { } catch (error) {
console.error('Code execution error:', error); console.error('Code execution error:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : '未知错误', error: error instanceof Error ? error.message : '未知错误',
engine: 'piston',
executionTime: Date.now() - start,
}; };
} }
} }
@ -173,20 +263,37 @@ function getFileName(language: string): string {
* - AI * - AI
*/ */
export function formatExecutionResult(response: CodeExecutionResponse): string { export function formatExecutionResult(response: CodeExecutionResponse): string {
if (!response.success && !response.output) { if (!response.success && !response.output && !response.images?.length) {
return `❌ 执行失败: ${response.error}`; return `❌ 执行失败: ${response.error}`;
} }
let result = ''; let result = '';
if (response.language && response.version) { // 执行引擎信息
result += `**语言**: ${response.language} (v${response.version})\n\n`; 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) { if (response.output) {
result += `## 输出结果\n\`\`\`\n${response.output}\n\`\`\`\n`; result += `## 输出结果\n\`\`\`\n${response.output}\n\`\`\`\n`;
} }
// 图片信息(仅标记有图片,实际渲染由前端处理)
if (response.images && response.images.length > 0) {
result += `\n## 图形输出\n已生成 ${response.images.length} 张图表\n`;
}
if (response.error) { if (response.error) {
result += `\n## 错误信息\n\`\`\`\n${response.error}\n\`\`\`\n`; result += `\n## 错误信息\n\`\`\`\n${response.error}\n\`\`\`\n`;
} }
@ -198,21 +305,34 @@ export function formatExecutionResult(response: CodeExecutionResponse): string {
* *
*/ */
export function formatExecutionResultShort(response: CodeExecutionResponse, language: string): string { export function formatExecutionResultShort(response: CodeExecutionResponse, language: string): string {
if (!response.success && !response.output) { if (!response.success && !response.output && !response.images?.length) {
return `❌ 代码执行失败: ${response.error}`; return `❌ 代码执行失败: ${response.error}`;
} }
const langDisplay = response.language || language; const langDisplay = response.language || language;
const versionDisplay = response.version ? ` (v${response.version})` : ''; const engineDisplay = response.engine === 'pyodide' ? ' [Pyodide]' : '';
const timeDisplay = response.executionTime ? ` (${response.executionTime}ms)` : '';
if (response.error && !response.output) { if (response.error && !response.output && !response.images?.length) {
return `⚠️ ${langDisplay}${versionDisplay} 代码执行出错`; return `⚠️ ${langDisplay}${engineDisplay} 代码执行出错`;
} }
// 计算输出行数 // 计算输出行数
const outputLines = response.output?.split('\n').length || 0; const outputLines = response.output?.split('\n').filter(l => l.trim()).length || 0;
const imageCount = response.images?.length || 0;
return `${langDisplay}${versionDisplay} 代码执行完成,输出 ${outputLines}`; // 构建结果描述
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}`;
} }
/** /**

View File

@ -24,6 +24,10 @@ import {
type WebFetchInput, type WebFetchInput,
type WebFetchResponse, type WebFetchResponse,
} from './webFetch'; } from './webFetch';
import { shouldUsePyodide, analyzeCode, type LoadingCallback } from './codeAnalyzer';
// 导出代码分析函数供外部使用
export { shouldUsePyodide, analyzeCode, type LoadingCallback } from './codeAnalyzer';
export interface ToolExecutionResult { export interface ToolExecutionResult {
success: boolean; success: boolean;
@ -33,17 +37,27 @@ export interface ToolExecutionResult {
displayResult: string; displayResult: string;
/** 原始数据 */ /** 原始数据 */
rawData?: unknown; rawData?: unknown;
/** Base64 编码的图片数组(代码执行时可能产生) */
images?: string[];
/** 是否需要浏览器端 Pyodide 执行 */
requiresPyodide?: boolean;
/** 代码内容(当 requiresPyodide 为 true 时) */
code?: string;
/** 语言(当 requiresPyodide 为 true 时) */
language?: string;
} }
/** /**
* *
* @param toolName * @param toolName
* @param input * @param input
* @param onProgress Pyodide
* @returns * @returns
*/ */
export async function executeTool( export async function executeTool(
toolName: string, toolName: string,
input: Record<string, unknown> input: Record<string, unknown>,
onProgress?: LoadingCallback
): Promise<ToolExecutionResult> { ): Promise<ToolExecutionResult> {
try { try {
switch (toolName) { switch (toolName) {
@ -61,10 +75,26 @@ export async function executeTool(
case 'code_execution': { case 'code_execution': {
const language = String(input.language || 'python'); const language = String(input.language || 'python');
const code = String(input.code || '');
// 检测是否需要浏览器端 Pyodide 执行Python + 图形代码)
if (shouldUsePyodide(code, language)) {
return {
success: true,
fullResult: '需要在浏览器端执行 Python 图形代码',
displayResult: '检测到图形绑制代码,正在准备浏览器端执行...',
requiresPyodide: true,
code,
language,
};
}
// 使用 Piston API 执行(服务端)
const codeInput: CodeExecutionInput = { const codeInput: CodeExecutionInput = {
code: String(input.code || ''), code,
language, language,
stdin: input.stdin ? String(input.stdin) : undefined, stdin: input.stdin ? String(input.stdin) : undefined,
onProgress, // 传递 Pyodide 加载进度回调
}; };
const response: CodeExecutionResponse = await executeCode(codeInput); const response: CodeExecutionResponse = await executeCode(codeInput);
return { return {
@ -72,6 +102,7 @@ export async function executeTool(
fullResult: formatExecutionResult(response), fullResult: formatExecutionResult(response),
displayResult: formatExecutionResultShort(response, language), displayResult: formatExecutionResultShort(response, language),
rawData: response, rawData: response,
images: response.images, // 传递图片数据
}; };
} }
@ -92,7 +123,7 @@ export async function executeTool(
return { return {
success: false, success: false,
fullResult: `未知的工具: ${toolName}`, fullResult: `未知的工具: ${toolName}`,
displayResult: `未知的工具: ${toolName}`, displayResult: `未知的工具: ${toolName}`,
}; };
} }
} catch (error) { } catch (error) {
@ -101,7 +132,7 @@ export async function executeTool(
return { return {
success: false, success: false,
fullResult: `工具执行错误: ${errorMsg}`, fullResult: `工具执行错误: ${errorMsg}`,
displayResult: `工具执行错误: ${errorMsg}`, displayResult: `工具执行错误: ${errorMsg}`,
}; };
} }
} }