9 changed files with 732 additions and 27 deletions
@ -0,0 +1,246 @@ |
|||||
|
import ToolProxyManager from '@/runtime/ToolProxyManager.ts' |
||||
|
import { compileTypeScript } from '@/core/script/ModelScript.ts' |
||||
|
import { markRaw } from 'vue' |
||||
|
|
||||
|
class ScriptRunner { |
||||
|
private scriptWorkers = new WeakMap<CustomScript, Worker>() |
||||
|
private toolProxyManager = new ToolProxyManager() |
||||
|
|
||||
|
async runScript(script: CustomScript) { |
||||
|
if (script.isRunning) { |
||||
|
console.warn(`脚本 "${script.name}" 已在运行中`) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const jsCode = await compileTypeScript(script.content) |
||||
|
|
||||
|
// 使用更安全的 Worker 包装代码
|
||||
|
const wrappedContent = ` |
||||
|
const pendingCalls = new Map(); |
||||
|
|
||||
|
// 端口消息处理函数
|
||||
|
const handlePortMessage = (event) => { |
||||
|
const { data } = event; |
||||
|
|
||||
|
// 只处理有 callId 的消息
|
||||
|
if (!data.callId) return; |
||||
|
|
||||
|
const resolver = pendingCalls.get(data.callId); |
||||
|
if (!resolver) { |
||||
|
console.warn(\`[Worker] 收到未知 callId 的响应: \${data.callId}\`);
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
pendingCalls.delete(data.callId); |
||||
|
|
||||
|
if (data.type === 'result') { |
||||
|
resolver.resolve(data.result); |
||||
|
} else if (data.type === 'error') { |
||||
|
const err = new Error(data.error.message); |
||||
|
err.name = data.error.name; |
||||
|
err.stack = data.error.stack; |
||||
|
resolver.reject(err); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 创建代理对象
|
||||
|
const createToolProxy = (port) => { |
||||
|
port.addEventListener('message', handlePortMessage); |
||||
|
|
||||
|
return new Proxy({}, { |
||||
|
get(_, toolName) { |
||||
|
return new Proxy({}, { |
||||
|
get(_, methodName) { |
||||
|
return (...args) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const callId = Math.random().toString(36).substr(2, 10); |
||||
|
|
||||
|
// 保存解析器
|
||||
|
pendingCalls.set(callId, { resolve, reject }); |
||||
|
|
||||
|
// 发送调用请求
|
||||
|
port.postMessage({ |
||||
|
type: 'call', tool: toolName, method: methodName, callId, args |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
self.onmessage = async (initMessage) => { |
||||
|
if (initMessage.data.type === 'init') { |
||||
|
// 设置通信端口
|
||||
|
const port = initMessage.data.port; |
||||
|
port.start(); |
||||
|
|
||||
|
// 创建工具代理
|
||||
|
const tools = createToolProxy(port); |
||||
|
const { LCC, RCS } = tools; |
||||
|
|
||||
|
try { |
||||
|
// 添加全局错误处理
|
||||
|
self.addEventListener('error', (event) => { |
||||
|
console.error('Worker 全局错误:', event.error); |
||||
|
self.postMessage({ |
||||
|
type: 'error', |
||||
|
error: { |
||||
|
name: event.error.name, |
||||
|
message: event.error.message, |
||||
|
stack: event.error.stack |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
self.addEventListener('unhandledrejection', (event) => { |
||||
|
console.error('未处理的 Promise 拒绝:', event.reason); |
||||
|
self.postMessage({ |
||||
|
type: 'error', |
||||
|
error: { |
||||
|
name: 'UnhandledRejection', |
||||
|
message: event.reason.message, |
||||
|
stack: event.reason.stack |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// 执行用户脚本
|
||||
|
(async () => { |
||||
|
// ---------------------custom script start---------------------
|
||||
|
${jsCode} |
||||
|
// ---------------------custom script end---------------------
|
||||
|
})().then(() => { |
||||
|
self.postMessage({ type: 'completed' }); |
||||
|
}).catch(error => { |
||||
|
console.error('脚本执行出错:', error); |
||||
|
self.postMessage({ |
||||
|
type: 'error', |
||||
|
error: { |
||||
|
name: error.name, |
||||
|
message: error.message, |
||||
|
stack: error.stack |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
console.error('脚本同步错误:', error); |
||||
|
self.postMessage({ |
||||
|
type: 'error', |
||||
|
error: { |
||||
|
name: error.name, |
||||
|
message: error.message, |
||||
|
stack: error.stack |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
` |
||||
|
|
||||
|
const blob = new Blob([wrappedContent], { type: 'application/javascript' }) |
||||
|
const blobUrl = URL.createObjectURL(blob) |
||||
|
const worker = new Worker(blobUrl) |
||||
|
worker['name'] = script.name // 设置 Worker 名称,便于调试
|
||||
|
|
||||
|
// 创建代理并初始化 Worker
|
||||
|
const port = this.toolProxyManager.createToolProxyForWorker(worker) |
||||
|
|
||||
|
// 存储 Worker 引用
|
||||
|
this.scriptWorkers.set(script, worker) |
||||
|
script.worker = markRaw(worker) |
||||
|
script.isRunning = true |
||||
|
URL.revokeObjectURL(blobUrl) |
||||
|
|
||||
|
// 设置 Worker 事件监听器
|
||||
|
worker.onmessage = (e) => this.handleWorkerMessage(script, e) |
||||
|
worker.onerror = (e) => this.handleWorkerError(script, e) |
||||
|
worker.onmessageerror = (e) => this.handleMessageError(script, e) |
||||
|
|
||||
|
// 发送初始化消息(仅传递可转移的端口对象)
|
||||
|
worker.postMessage({ |
||||
|
type: 'init', |
||||
|
port: port |
||||
|
}, [port]) // 将端口作为可转移对象传递
|
||||
|
|
||||
|
console.log(`脚本 "${script.name}" 已启动`) |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error(`启动脚本 "${script.name}" 失败:`, error) |
||||
|
script.isRunning = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 停止脚本 |
||||
|
*/ |
||||
|
stopScript(script: CustomScript) { |
||||
|
const worker = this.scriptWorkers.get(script) |
||||
|
if (!worker || !script.isRunning) { |
||||
|
console.warn(`脚本 "${script.name}" 未运行`) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// 清理代理管理器中的资源
|
||||
|
this.toolProxyManager.cleanupWorker(worker) |
||||
|
|
||||
|
// 终止 Worker
|
||||
|
worker.terminate() |
||||
|
this.scriptWorkers.delete(script) |
||||
|
script.worker = undefined |
||||
|
script.isRunning = false |
||||
|
|
||||
|
console.log(`脚本 "${script.name}" 已停止`) |
||||
|
} catch (error) { |
||||
|
console.error(`停止脚本 "${script.name}" 时出错:`, error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理来自 Worker 的消息 |
||||
|
*/ |
||||
|
private handleWorkerMessage(script: CustomScript, event: MessageEvent): void { |
||||
|
const { data } = event |
||||
|
|
||||
|
switch (data?.type) { |
||||
|
case 'completed': |
||||
|
console.log(`脚本 "${script.name}" 执行完成`) |
||||
|
this.stopScript(script) |
||||
|
break |
||||
|
|
||||
|
case 'error': |
||||
|
const error = new Error(data.error.message) |
||||
|
error.name = data.error.name |
||||
|
error.stack = data.error.stack |
||||
|
console.error(`[${script.name}] 执行错误:`, error) |
||||
|
this.stopScript(script) |
||||
|
break |
||||
|
|
||||
|
default: |
||||
|
console.log(`[${script.name}] 收到消息:`, data) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理 Worker 错误 |
||||
|
*/ |
||||
|
private handleWorkerError(script: CustomScript, event: ErrorEvent): void { |
||||
|
console.error(`[${script.name}] 运行时错误:`, event.message, `@${event.filename}:${event.lineno}`) |
||||
|
this.stopScript(script) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理消息错误 |
||||
|
*/ |
||||
|
private handleMessageError(script: CustomScript, event: MessageEvent): void { |
||||
|
console.error(`[${script.name}] 消息解析错误:`, event) |
||||
|
this.stopScript(script) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const scriptRunner = new ScriptRunner() |
||||
|
window['ScriptRunner'] = scriptRunner |
||||
|
export default scriptRunner |
||||
@ -0,0 +1,130 @@ |
|||||
|
// 工具类代理管理器
|
||||
|
import LCCScript from '@/core/script/LCCScript.ts' |
||||
|
|
||||
|
export default class ToolProxyManager { |
||||
|
// 存储所有活跃的 MessageChannel 端口
|
||||
|
private activePorts = new WeakMap<Worker, MessagePort>() |
||||
|
|
||||
|
/** |
||||
|
* 为 Worker 创建工具代理 |
||||
|
*/ |
||||
|
createToolProxyForWorker(worker: Worker): MessagePort { |
||||
|
// 创建 MessageChannel 用于通信
|
||||
|
const channel = new MessageChannel() |
||||
|
|
||||
|
// 存储 port2 用于后续通信
|
||||
|
this.activePorts.set(worker, channel.port2) |
||||
|
|
||||
|
// 设置 port2 的消息监听器
|
||||
|
channel.port2.onmessage = (event) => { |
||||
|
this.handleWorkerCall(worker, event) |
||||
|
} |
||||
|
|
||||
|
channel.port2.start() |
||||
|
|
||||
|
// 返回 port1 供 Worker 使用
|
||||
|
return channel.port1 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理来自 Worker 的调用请求 |
||||
|
*/ |
||||
|
handleWorkerCall(worker: Worker, event: MessageEvent): void { |
||||
|
const { data } = event |
||||
|
if (data?.type !== 'call') return |
||||
|
|
||||
|
const { tool, method, callId, args } = data |
||||
|
const port = this.activePorts.get(worker) |
||||
|
if (!port) { |
||||
|
console.error(`[ToolProxy] 找不到 worker 的端口`, worker) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 获取主线程中的工具实例
|
||||
|
const toolInstance = (window as any)[tool] |
||||
|
if (!toolInstance) { |
||||
|
console.error(`[ToolProxy] 工具未找到: ${tool}`) |
||||
|
port.postMessage({ |
||||
|
type: 'error', |
||||
|
tool, |
||||
|
method, |
||||
|
callId, |
||||
|
error: { |
||||
|
name: 'ToolNotFound', |
||||
|
message: `${tool} is not available` |
||||
|
} |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (typeof toolInstance[method] !== 'function') { |
||||
|
console.error(`[ToolProxy] 方法未找到: ${tool}.${method}`) |
||||
|
port.postMessage({ |
||||
|
type: 'error', tool, method, callId, |
||||
|
error: { |
||||
|
name: 'MethodNotFound', |
||||
|
message: `${tool}.${method} is not a function` |
||||
|
} |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// 执行工具方法
|
||||
|
let result |
||||
|
if (toolInstance instanceof LCCScript && method === 'log') { |
||||
|
// 特殊处理 LCCScript 的 log 方法
|
||||
|
result = toolInstance[method]('Script-' + worker['name'], ...args) |
||||
|
} else { |
||||
|
result = toolInstance[method](...args) |
||||
|
} |
||||
|
|
||||
|
// 处理异步函数
|
||||
|
if (result instanceof Promise) { |
||||
|
result.then( |
||||
|
resolved => { |
||||
|
port.postMessage({ |
||||
|
type: 'result', tool, method, callId, result: resolved |
||||
|
}) |
||||
|
}, |
||||
|
error => { |
||||
|
port.postMessage({ |
||||
|
type: 'error', tool, method, callId, |
||||
|
error: { |
||||
|
name: error.name || 'PromiseRejection', |
||||
|
message: error.message, |
||||
|
stack: error.stack |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
// 处理同步函数
|
||||
|
else { |
||||
|
port.postMessage({ type: 'result', tool, method, callId, result }) |
||||
|
} |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error(`[ToolProxy] 执行错误: ${tool}.${method}`, error) |
||||
|
port.postMessage({ |
||||
|
type: 'error', tool, method, callId, |
||||
|
error: { |
||||
|
name: error.name || 'ExecutionError', |
||||
|
message: error.message, |
||||
|
stack: error.stack |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清理 Worker 资源 |
||||
|
*/ |
||||
|
cleanupWorker(worker: Worker) { |
||||
|
const port = this.activePorts.get(worker) |
||||
|
if (port) { |
||||
|
port.close() |
||||
|
this.activePorts.delete(worker) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue