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 工具服务
|
* 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();
|
||||||
|
|
||||||
|
// 检查是否应该使用 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()];
|
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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user