From 19bac12c9bcd00747b7853ac7ed6beac7fa49d55 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Sun, 21 Dec 2025 16:51:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E4=BB=A3=E7=A0=81=E6=89=A7=E8=A1=8C):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B2=99=E7=AE=B1=E5=BC=95=E6=93=8E=E9=98=BB?= =?UTF-8?q?=E5=A1=9E=E5=92=8C=E8=B6=85=E6=97=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 alert/confirm/prompt 提供 mock 实现,避免执行阻塞 - 为 DOM API 提供 fallback,防止元素不存在时报错 - 优化 TypeScript 转译,完善类型注解移除逻辑 - 添加 allow-same-origin 确保 postMessage 正常工作 - 改进危险 API 限制,提供友好错误提示 --- src/lib/code-runner/engines/sandbox.ts | 180 ++++++++++++++++++++----- 1 file changed, 150 insertions(+), 30 deletions(-) diff --git a/src/lib/code-runner/engines/sandbox.ts b/src/lib/code-runner/engines/sandbox.ts index 857437e..5432b43 100644 --- a/src/lib/code-runner/engines/sandbox.ts +++ b/src/lib/code-runner/engines/sandbox.ts @@ -61,23 +61,71 @@ export class SandboxEngine implements IExecutionEngine { } 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, '?.'); + // TypeScript 转 JavaScript(移除类型注解和 TS 特有语法) + let result = code; + + // 1. 移除接口定义(包括多行和 extends) + result = result.replace(/\binterface\s+\w+(?:\s+extends\s+[\w,\s]+)?\s*\{[\s\S]*?\}/g, ''); + + // 2. 移除类型别名 + result = result.replace(/\btype\s+\w+(?:<[^>]*>)?\s*=\s*[^;]+;/g, ''); + + // 3. 移除 enum 定义 + result = result.replace(/\benum\s+\w+\s*\{[\s\S]*?\}/g, ''); + + // 4. 移除泛型参数(循环处理嵌套泛型) + let prev = ''; + while (prev !== result) { + prev = result; + result = result.replace(/<[^<>]+>/g, ''); + } + + // 5. 移除访问修饰符(private, public, protected, readonly) + result = result.replace(/\b(private|public|protected)\s+/g, ''); + result = result.replace(/\breadonly\s+/g, ''); + + // 6. 移除函数/方法返回类型注解 + // ): string { -> ) { + // ): string => -> ) => + // ): Promise { -> ) { (泛型已在步骤4移除) + result = result.replace(/\)\s*:\s*[\w\[\]|&\s]+\s*(\{|=>)/g, ') $1'); + + // 7. 移除函数参数类型注解(循环处理多参数) + // (person: Person) -> (person) + // (name: string, age: number) -> (name, age) + // (value?: string) -> (value) + for (let i = 0; i < 20; i++) { + const before = result; + result = result.replace(/(\(|,)\s*(\w+)\s*\??\s*:\s*[\w\[\]|&\s]+([,)])/g, '$1$2$3'); + if (before === result) break; + } + + // 8. 移除类属性/变量类型注解 + // greeting: string; -> greeting; + // count: number = 0; -> count = 0; + result = result.replace(/(\w+)\s*:\s*[\w\[\]|&\s]+\s*([;=])/g, '$1 $2'); + + // 9. 移除 as 类型断言 + result = result.replace(/\s+as\s+[\w\[\]]+/g, ''); + + // 10. 移除 ! 非空断言 + result = result.replace(/!([.;,)\s])/g, '$1'); + result = result.replace(/!\./g, '.'); + + // 11. 移除 declare 关键字 + result = result.replace(/\bdeclare\s+/g, ''); + + // 12. 移除 abstract 关键字 + result = result.replace(/\babstract\s+/g, ''); + + // 13. 移除 implements 子句 + result = result.replace(/\s+implements\s+[\w,\s]+(?=\s*\{)/g, ''); + + // 14. 清理多余的空行 + result = result.replace(/^\s*[\r\n]/gm, '\n'); + result = result.replace(/\n{3,}/g, '\n\n'); + + return result.trim(); } private executeInSandbox(code: string): Promise { @@ -88,7 +136,10 @@ export class SandboxEngine implements IExecutionEngine { // 创建隔离的 iframe this.iframe = document.createElement('iframe'); this.iframe.style.display = 'none'; + // 需要 allow-same-origin 才能让 postMessage 正常工作 + // allow-scripts 允许执行脚本 this.iframe.sandbox.add('allow-scripts'); + this.iframe.sandbox.add('allow-same-origin'); document.body.appendChild(this.iframe); const iframeWindow = this.iframe.contentWindow; @@ -172,18 +223,87 @@ export class SandboxEngine implements IExecutionEngine { // 替换全局 console window.console = customConsole; - // 限制危险 API - delete window.fetch; - delete window.XMLHttpRequest; - delete window.WebSocket; - delete window.localStorage; - delete window.sessionStorage; + // 提供浏览器 API 的 mock 实现(避免阻塞和错误) + window.alert = (msg) => { + customConsole.log('[Alert]', msg); + }; + window.confirm = (msg) => { + customConsole.log('[Confirm]', msg); + return true; // 默认返回 true + }; + window.prompt = (msg, defaultValue) => { + customConsole.log('[Prompt]', msg); + return defaultValue || '用户输入'; // 返回默认值或模拟值 + }; + + // Mock document API + const mockElement = { + innerHTML: '', + innerText: '', + textContent: '', + value: '', + style: {}, + classList: { + add: () => {}, + remove: () => {}, + toggle: () => {}, + contains: () => false, + }, + appendChild: () => mockElement, + removeChild: () => mockElement, + addEventListener: () => {}, + removeEventListener: () => {}, + setAttribute: () => {}, + getAttribute: () => null, + querySelector: () => null, + querySelectorAll: () => [], + }; + + // 保存原始 document 方法 + const originalGetElementById = document.getElementById.bind(document); + const originalQuerySelector = document.querySelector.bind(document); + const originalQuerySelectorAll = document.querySelectorAll.bind(document); + const originalCreateElement = document.createElement.bind(document); + + // 重写 document 方法,提供 fallback + document.getElementById = (id) => { + const el = originalGetElementById(id); + if (!el) { + customConsole.log('[DOM] getElementById("' + id + '") 返回 null,使用 mock 元素'); + return mockElement; + } + return el; + }; + + document.querySelector = (selector) => { + const el = originalQuerySelector(selector); + if (!el) { + return mockElement; + } + return el; + }; + + document.querySelectorAll = (selector) => { + return originalQuerySelectorAll(selector) || []; + }; + + document.createElement = (tag) => { + try { + return originalCreateElement(tag); + } catch { + return mockElement; + } + }; + + // 限制危险的网络和存储 API + window.fetch = () => Promise.reject(new Error('fetch 在沙箱中不可用')); + window.XMLHttpRequest = undefined; + window.WebSocket = undefined; + window.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} }; + window.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} }; delete window.indexedDB; - delete window.open; - delete window.close; - delete window.alert; - delete window.confirm; - delete window.prompt; + window.open = () => { customConsole.log('[Blocked] window.open 在沙箱中不可用'); return null; }; + window.close = () => { customConsole.log('[Blocked] window.close 在沙箱中不可用'); }; try { // 执行用户代码 @@ -201,11 +321,11 @@ export class SandboxEngine implements IExecutionEngine { })(); `; - // 写入并执行 + // 写入并执行(先写入 body 确保 document.body 存在) const iframeDoc = this.iframe.contentDocument; if (iframeDoc) { iframeDoc.open(); - iframeDoc.write(``); + iframeDoc.write(``); iframeDoc.close(); } });