claude-code-cchui/src/lib/code-runner/engines/sandbox.ts
gaoziman 192cd175da feat(代码运行): 添加多语言代码执行引擎
- 实现 sandbox 引擎支持 JavaScript/TypeScript 执行
- 实现 pyodide 引擎支持 Python 浏览器端执行
- 实现 remote 引擎支持 Java/Go/C/C++/Rust 等远程执行
- 添加语言配置和入口点检测逻辑
- 支持执行状态回调和加载进度显示
2025-12-21 03:20:16 +08:00

225 lines
6.7 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.

/**
* JavaScript/TypeScript 沙箱执行引擎
* 在隔离的 iframe 中安全执行代码
*/
import type { IExecutionEngine, ExecutionResult } from '../types';
// 执行超时时间(毫秒)
const EXECUTION_TIMEOUT = 10000;
// 最大输出长度
const MAX_OUTPUT_LENGTH = 50000;
export class SandboxEngine implements IExecutionEngine {
private iframe: HTMLIFrameElement | null = null;
private abortController: AbortController | null = null;
supports(language: string): boolean {
const lang = language.toLowerCase();
return ['javascript', 'js', 'typescript', 'ts'].includes(lang);
}
async execute(code: string, language: string): Promise<ExecutionResult> {
const startTime = performance.now();
const lang = language.toLowerCase();
try {
// TypeScript 需要先编译
let executableCode = code;
if (lang === 'typescript' || lang === 'ts') {
executableCode = this.transpileTypeScript(code);
}
// 在沙箱中执行
const output = await this.executeInSandbox(executableCode);
const executionTime = Math.round(performance.now() - startTime);
return {
success: true,
output: output.slice(0, MAX_OUTPUT_LENGTH),
executionTime,
engine: 'sandbox',
};
} catch (error) {
const executionTime = Math.round(performance.now() - startTime);
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
executionTime,
engine: 'sandbox',
};
}
}
stop(): void {
if (this.abortController) {
this.abortController.abort();
}
this.cleanup();
}
private transpileTypeScript(code: string): string {
// 简单的 TypeScript 转换(移除类型注解)
// 实际项目中可以使用 @babel/standalone 或 typescript
return code
// 移除类型注解
.replace(/:\s*\w+(\[\])?(\s*[=,)])/g, '$2')
// 移除接口定义
.replace(/interface\s+\w+\s*\{[^}]*\}/g, '')
// 移除类型别名
.replace(/type\s+\w+\s*=\s*[^;]+;/g, '')
// 移除泛型
.replace(/<[^>]+>/g, '')
// 移除 as 类型断言
.replace(/\s+as\s+\w+/g, '')
// 移除 ! 非空断言
.replace(/!\./g, '.')
// 移除可选链前的类型
.replace(/\?\./g, '?.');
}
private executeInSandbox(code: string): Promise<string> {
return new Promise((resolve, reject) => {
this.abortController = new AbortController();
const outputs: string[] = [];
// 创建隔离的 iframe
this.iframe = document.createElement('iframe');
this.iframe.style.display = 'none';
this.iframe.sandbox.add('allow-scripts');
document.body.appendChild(this.iframe);
const iframeWindow = this.iframe.contentWindow;
if (!iframeWindow) {
this.cleanup();
reject(new Error('无法创建执行沙箱'));
return;
}
// 设置超时
const timeoutId = setTimeout(() => {
this.cleanup();
reject(new Error(`执行超时(${EXECUTION_TIMEOUT / 1000}秒)`));
}, EXECUTION_TIMEOUT);
// 监听消息
const messageHandler = (event: MessageEvent) => {
if (event.source !== iframeWindow) return;
const { type, data } = event.data;
if (type === 'console') {
outputs.push(data);
} else if (type === 'done') {
clearTimeout(timeoutId);
window.removeEventListener('message', messageHandler);
this.cleanup();
resolve(outputs.join('\n'));
} else if (type === 'error') {
clearTimeout(timeoutId);
window.removeEventListener('message', messageHandler);
this.cleanup();
reject(new Error(data));
}
};
window.addEventListener('message', messageHandler);
// 中止处理
this.abortController.signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
window.removeEventListener('message', messageHandler);
this.cleanup();
reject(new Error('执行已取消'));
});
// 在 iframe 中执行代码
const sandboxCode = `
(function() {
const outputs = [];
// 重写 console 方法
const originalConsole = console;
const customConsole = {
log: (...args) => {
const msg = args.map(a => {
if (typeof a === 'object') {
try {
return JSON.stringify(a, null, 2);
} catch {
return String(a);
}
}
return String(a);
}).join(' ');
parent.postMessage({ type: 'console', data: msg }, '*');
},
error: (...args) => customConsole.log('[Error]', ...args),
warn: (...args) => customConsole.log('[Warn]', ...args),
info: (...args) => customConsole.log(...args),
debug: (...args) => customConsole.log('[Debug]', ...args),
table: (data) => customConsole.log(JSON.stringify(data, null, 2)),
clear: () => {},
dir: (obj) => customConsole.log(obj),
time: () => {},
timeEnd: () => {},
group: () => {},
groupEnd: () => {},
};
// 替换全局 console
window.console = customConsole;
// 限制危险 API
delete window.fetch;
delete window.XMLHttpRequest;
delete window.WebSocket;
delete window.localStorage;
delete window.sessionStorage;
delete window.indexedDB;
delete window.open;
delete window.close;
delete window.alert;
delete window.confirm;
delete window.prompt;
try {
// 执行用户代码
const result = eval(${JSON.stringify(code)});
// 如果有返回值且不是 undefined输出它
if (result !== undefined) {
customConsole.log(result);
}
parent.postMessage({ type: 'done' }, '*');
} catch (error) {
parent.postMessage({ type: 'error', data: error.message || String(error) }, '*');
}
})();
`;
// 写入并执行
const iframeDoc = this.iframe.contentDocument;
if (iframeDoc) {
iframeDoc.open();
iframeDoc.write(`<script>${sandboxCode}</script>`);
iframeDoc.close();
}
});
}
private cleanup(): void {
if (this.iframe && this.iframe.parentNode) {
this.iframe.parentNode.removeChild(this.iframe);
}
this.iframe = null;
this.abortController = null;
}
}
// 导出单例
export const sandboxEngine = new SandboxEngine();