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