From 68ba9b3204286985b44d2005cfc2062014c46077 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Fri, 19 Dec 2025 20:18:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=B7=A5=E5=85=B7):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=B7=B7=E5=90=88=E4=BB=A3=E7=A0=81=E6=89=A7=E8=A1=8C=E6=9E=B6?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit codeExecution.ts: - 实现 Pyodide + Piston 混合执行架构 - Python 图形代码使用 Pyodide 在浏览器端执行 - 其他代码使用 Piston API 在服务端执行 - 响应增加 images、engine、executionTime 字段 executor.ts: - 集成代码分析工具判断执行方式 - 支持返回 requiresPyodide 标记浏览器端执行需求 - 传递图片数据到执行结果 --- src/services/tools/codeExecution.ts | 142 +++++++++++++++++++++++++--- src/services/tools/executor.ts | 39 +++++++- 2 files changed, 166 insertions(+), 15 deletions(-) diff --git a/src/services/tools/codeExecution.ts b/src/services/tools/codeExecution.ts index 20c3488..fed774c 100644 --- a/src/services/tools/codeExecution.ts +++ b/src/services/tools/codeExecution.ts @@ -1,13 +1,27 @@ /** * Code Execution 工具服务 - * 使用 Piston API 实现代码执行 + * 混合架构: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 { @@ -16,6 +30,12 @@ export interface CodeExecutionResponse { error?: string; language?: string; version?: string; + /** Base64 编码的图片数组(matplotlib 输出) */ + images?: string[]; + /** 执行引擎: 'pyodide' | 'piston' */ + engine?: 'pyodide' | 'piston'; + /** 执行时间 (ms) */ + executionTime?: number; } // 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 { - const { code, language, stdin } = input; + 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 { + 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 { + const start = startTime || Date.now(); // 获取语言映射 const langConfig = LANGUAGE_MAP[language.toLowerCase()]; @@ -63,6 +141,8 @@ export async function executeCode(input: CodeExecutionInput): Promise 0) { + result += `\n## 图形输出\n已生成 ${response.images.length} 张图表\n`; + } + if (response.error) { 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 { - if (!response.success && !response.output) { + if (!response.success && !response.output && !response.images?.length) { return `❌ 代码执行失败: ${response.error}`; } 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) { - return `⚠️ ${langDisplay}${versionDisplay} 代码执行出错`; + if (response.error && !response.output && !response.images?.length) { + 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}`; } /** diff --git a/src/services/tools/executor.ts b/src/services/tools/executor.ts index 0332020..4d65cff 100644 --- a/src/services/tools/executor.ts +++ b/src/services/tools/executor.ts @@ -24,6 +24,10 @@ import { type WebFetchInput, type WebFetchResponse, } from './webFetch'; +import { shouldUsePyodide, analyzeCode, type LoadingCallback } from './codeAnalyzer'; + +// 导出代码分析函数供外部使用 +export { shouldUsePyodide, analyzeCode, type LoadingCallback } from './codeAnalyzer'; export interface ToolExecutionResult { success: boolean; @@ -33,17 +37,27 @@ export interface ToolExecutionResult { displayResult: string; /** 原始数据 */ rawData?: unknown; + /** Base64 编码的图片数组(代码执行时可能产生) */ + images?: string[]; + /** 是否需要浏览器端 Pyodide 执行 */ + requiresPyodide?: boolean; + /** 代码内容(当 requiresPyodide 为 true 时) */ + code?: string; + /** 语言(当 requiresPyodide 为 true 时) */ + language?: string; } /** * 执行工具 * @param toolName 工具名称 * @param input 工具输入参数 + * @param onProgress Pyodide 加载进度回调(可选) * @returns 执行结果(包含完整版和简短版) */ export async function executeTool( toolName: string, - input: Record + input: Record, + onProgress?: LoadingCallback ): Promise { try { switch (toolName) { @@ -61,10 +75,26 @@ export async function executeTool( case 'code_execution': { 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 = { - code: String(input.code || ''), + code, language, stdin: input.stdin ? String(input.stdin) : undefined, + onProgress, // 传递 Pyodide 加载进度回调 }; const response: CodeExecutionResponse = await executeCode(codeInput); return { @@ -72,6 +102,7 @@ export async function executeTool( fullResult: formatExecutionResult(response), displayResult: formatExecutionResultShort(response, language), rawData: response, + images: response.images, // 传递图片数据 }; } @@ -92,7 +123,7 @@ export async function executeTool( return { success: false, fullResult: `未知的工具: ${toolName}`, - displayResult: `❌ 未知的工具: ${toolName}`, + displayResult: `未知的工具: ${toolName}`, }; } } catch (error) { @@ -101,7 +132,7 @@ export async function executeTool( return { success: false, fullResult: `工具执行错误: ${errorMsg}`, - displayResult: `❌ 工具执行错误: ${errorMsg}`, + displayResult: `工具执行错误: ${errorMsg}`, }; } }