feat(工具): 实现混合代码执行架构
codeExecution.ts: - 实现 Pyodide + Piston 混合执行架构 - Python 图形代码使用 Pyodide 在浏览器端执行 - 其他代码使用 Piston API 在服务端执行 - 响应增加 images、engine、executionTime 字段 executor.ts: - 集成代码分析工具判断执行方式 - 支持返回 requiresPyodide 标记浏览器端执行需求 - 传递图片数据到执行结果
This commit is contained in:
parent
ef45e14534
commit
68ba9b3204
@ -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<CodeExecutionResponse> {
|
||||
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<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()];
|
||||
@ -63,6 +141,8 @@ export async function executeCode(input: CodeExecutionInput): Promise<CodeExecut
|
||||
return {
|
||||
success: false,
|
||||
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 {
|
||||
success: false,
|
||||
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 || '编译错误',
|
||||
language: langConfig.language,
|
||||
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 || '',
|
||||
language: langConfig.language,
|
||||
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 || '',
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -173,20 +263,37 @@ function getFileName(language: string): string {
|
||||
* 格式化代码执行结果为文本(完整版 - 发送给 AI)
|
||||
*/
|
||||
export function formatExecutionResult(response: CodeExecutionResponse): string {
|
||||
if (!response.success && !response.output) {
|
||||
if (!response.success && !response.output && !response.images?.length) {
|
||||
return `❌ 执行失败: ${response.error}`;
|
||||
}
|
||||
|
||||
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) {
|
||||
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`;
|
||||
}
|
||||
@ -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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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<string, unknown>
|
||||
input: Record<string, unknown>,
|
||||
onProgress?: LoadingCallback
|
||||
): Promise<ToolExecutionResult> {
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user