Browse Source

Web Worker 多线程脚本,可停止

master
修宁 5 months ago
parent
commit
548052c0e7
  1. 11
      pnpm-lock.yaml
  2. 20
      src/core/script/LCCScript.ts
  3. 249
      src/core/script/ModelManager.ts
  4. 8
      src/core/script/ModelScript.ts
  5. 62
      src/editor/widgets/script/ScriptView.vue
  6. 246
      src/runtime/ScriptRunner.ts
  7. 130
      src/runtime/ToolProxyManager.ts
  8. 11
      src/types/LCC.d.ts
  9. 22
      src/types/Model.d.ts

11
pnpm-lock.yaml

@ -615,67 +615,56 @@ packages:
resolution: {integrity: sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.41.0':
resolution: {integrity: sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.41.0':
resolution: {integrity: sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.41.0':
resolution: {integrity: sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.41.0':
resolution: {integrity: sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.41.0':
resolution: {integrity: sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.41.0':
resolution: {integrity: sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.41.0':
resolution: {integrity: sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.41.0':
resolution: {integrity: sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.41.0':
resolution: {integrity: sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.41.0':
resolution: {integrity: sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.41.0':
resolution: {integrity: sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==}

20
src/core/script/LCCScript.ts

@ -6,6 +6,26 @@ import { Request } from '@ease-forge/shared'
* LCC API
*/
export default class LCCScript implements LCC {
log(from: string, message: string, ...args: any[]): void {
// 插入时分秒 HH:mm:ss.sss
const now = new Date().toISOString().replace('T', ' ').replace('Z', '').split(' ')[1]
// 打成彩色
// console.log(now + ' [LCC-' + from + '] ' + message, ...args)
console.log(`%c${now} [${from}] ${message}`, 'color: #00f', ...args)
}
/**
* - 使
*/
async sleep(timeOfMs: number = 1000): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve() // 确保调用 resolve
}, timeOfMs)
})
}
async getAllProjects(): Promise<ServerResponse<LccProjectVo[]>> {
return Request.request.post('/api/workbench/LccController@getAllProjects', {})
}

249
src/core/script/ModelManager.ts

@ -1,5 +1,5 @@
import Viewport from '@/core/engine/Viewport.ts'
import { executeTypeScript } from '@/core/script/ModelScript.ts'
import { compileTypeScript, executeTypeScript } from '@/core/script/ModelScript.ts'
import * as THREE from 'three'
import { getMatrixFromTf } from '@/core/ModelUtils.ts'
import type { Object3DLike } from '@/types/ModelTypes.ts'
@ -7,6 +7,8 @@ import TaskManager from '../manager/TaskManager.ts'
import Cl2Entity from '@/modules/amr/ptr/cl2/Cl2Entity.ts'
import ClxEntity from '@/modules/amr/ptr/clx/ClxEntity.ts'
import { getRenderer } from '@/core/manager/ModuleManager.ts'
import { markRaw } from 'vue'
import ToolProxyManager from '@/runtime/ToolProxyManager.ts'
export default class ModelManager implements IControls, Model {
private viewport: Viewport
@ -177,4 +179,249 @@ this.viewport.stateManager.update(({ getEntity, putEntity, addEntity }) => {
system.showInfoDialog(content)
}
}
// 脚本与 Worker 的映射,用于快速查找
private scriptWorkers = new WeakMap<CustomScript, Worker>()
private toolProxyManager = new ToolProxyManager()
/**
* 线
*/
async runScript(script: CustomScript) {
// 检查脚本是否已在运行
if (script.isRunning) {
console.warn(`脚本 "${script.name}" 已在运行中`)
return
}
try {
// 创建 Blob URL 作为 Worker 执行内容
const jsCode = await compileTypeScript(script.content)
const wrappedContent = `
// Worker 端工具代理
// 创建代理对象
const createToolProxy = (port) => {
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);
// 发送调用请求
port.postMessage({
type: 'call',
tool: toolName,
method: methodName,
callId,
args
});
// 监听响应
const responseHandler = (event) => {
const { data } = event;
if (data.callId === callId) {
console.log('[Worker] 收到响应: ' + toolName + '.' + methodName, data.type);
port.removeEventListener('message', responseHandler);
if (data.type === 'result') {
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;
reject(err);
}
}
};
port.addEventListener('message', responseHandler);
});
};
}
});
}
});
};
// 创建代理对象
const tools = new Proxy({}, toolProxyHandler);
self.onmessage = function(__initMessage) {
if (__initMessage.data.type === 'init') {
// 设置通信端口
self.port = __initMessage.data.port;
self.port.onmessage = function(event) {
const { type, callId, result, error } = event.data;
if (!self.pendingCalls || !self.pendingCalls[callId]) return;
const { resolve, reject } = self.pendingCalls[callId];
delete self.pendingCalls[callId];
if (type === 'result') {
resolve(result);
} else if (type === 'error') {
const err = new Error(error.message);
err.name = error.name;
err.stack = error.stack;
reject(err);
}
};
self.port.start();
// 解构代理工具
const { LCC, RCS } = tools;
try {
(async function() {
// ---------------------custom script start---------------------[
${jsCode}
// ]---------------------custom script end---------------------
})().then(() => {
self.postMessage({ type: 'completed' })
}).catch(error => {
// 捕获异步错误
self.postMessage({
type: 'error',
error: {
name: error.name,
message: error.message,
stack: error.stack
}
})
})
} catch (error) {
// 捕获脚本错误
self.postMessage({
type: 'error',
error: {
name: error.name,
message: error.message,
stack: error.stack
}
})
} finally {
// 确保关闭 worker
self.close()
}
}
}
`
// console.log(wrappedContent)
const blob = new Blob([wrappedContent], { type: 'application/javascript' })
const blobUrl = URL.createObjectURL(blob)
const worker = new Worker(blobUrl)
// 创建代理并初始化 Worker
const port = this.toolProxyManager.createToolProxyForWorker(worker)
// 存储 Worker 引用并更新状态
this.scriptWorkers.set(script, worker)
script.worker = markRaw(worker)
script.isRunning = true
URL.revokeObjectURL(blobUrl) // 清理不再需要的 Blob URL
// 设置 Worker 事件监听器
worker.onmessage = (e) => this.handleWorkerMessage(script, e)
worker.onerror = (e) => this.handleWorkerError(script, e)
worker.onmessageerror = (e) => this.handleMessageError(script, e)
console.log(`[ScriptRunner] 创建 Worker 并启动脚本 "${script.name}"`)
worker.postMessage({
type: 'init',
port: port
}, [port])
console.log(`[ScriptRunner] 初始化消息已发送给 Worker`)
} catch (error) {
console.error(`启动脚本 "${script.name}" 失败:`, error)
script.isRunning = false
}
}
/**
*
*/
stopScript(script: CustomScript) {
this.cleanupScript(script, '手动停止')
}
/**
* Worker
*/
private handleWorkerMessage(script: CustomScript, event: MessageEvent): void {
const { data } = event
if (data?.type === 'call') {
// 处理来自 Worker 的工具调用请求
this.toolProxyManager.handleWorkerCall(script.worker, event)
return
}
if (data?.type === 'completed') {
// 脚本自然完成
console.log(`脚本 "${script.name}" 执行完成`)
this.cleanupScript(script, '执行完成')
return
}
if (data?.type === 'error') {
// 脚本内部错误
const error = new Error(data.error.message)
error.name = data.error.name
error.stack = data.error.stack
console.error(`[${script.name}] 执行错误:`, error)
this.cleanupScript(script, '执行错误')
return
}
// 常规消息处理
console.log(`[${script.name}] 收到消息:`, data)
debugger
}
/**
* Worker
*/
private handleWorkerError(script: CustomScript, event: ErrorEvent): void {
console.error(`[${script.name}] 发生错误:`, event.message, `@${event.filename}:${event.lineno}`)
this.cleanupScript(script, '运行时错误')
}
/**
*
*/
private handleMessageError(script: CustomScript, event: MessageEvent): void {
console.error(`[${script.name}] 消息解析错误:`, event)
this.cleanupScript(script, '消息错误')
}
/**
*
*/
private cleanupScript(script: CustomScript, reason: string): void {
if (!script.isRunning || !script.worker) return
try {
// 清理代理管理器中的资源
this.toolProxyManager.cleanupWorker(script.worker)
// 终止 Worker
script.worker.terminate()
this.scriptWorkers.delete(script)
script.worker = undefined
script.isRunning = false
console.log(`脚本 "${script.name}" 已停止 (原因: ${reason})`)
} catch (error) {
console.error(`清理脚本 "${script.name}" 资源失败:`, error)
}
}
}

8
src/core/script/ModelScript.ts

@ -6,7 +6,7 @@ import LCCDts from '@/types/LCC.d.ts?raw'
import * as ts from 'typescript'
import { createDefaultMapFromCDN, createSystem, createVirtualCompilerHost } from '@typescript/vfs'
export async function executeTypeScript(code: string, contextExtend: any = {}) {
export async function compileTypeScript(code: string) {
const fsMap = await createDefaultMapFromCDN(
{ target: ts.ScriptTarget.ESNext },
'latest',
@ -31,7 +31,11 @@ export async function executeTypeScript(code: string, contextExtend: any = {}) {
program.emit()
// 4. 获取编译后的JS代码
const jsCode = fsMap.get('input.js') || ''
return fsMap.get('input.js') || ''
}
export async function executeTypeScript(code: string, contextExtend: any = {}) {
const jsCode = await compileTypeScript(code)
// 5. 安全执行(使用沙箱)
return await executeInSandbox(jsCode, contextExtend)

62
src/editor/widgets/script/ScriptView.vue

@ -18,9 +18,12 @@
</el-button>
<el-divider direction="vertical" />
<el-button :icon="renderIcon('antd PlayCircleOutlined')" link type="primary"
:disabled="scriptList.length === 0 || !scriptList[scriptIndex]"
:loading="scriptIsRunning"
@click="runScript">运行
v-if="scriptList[scriptIndex] && !scriptList[scriptIndex].isRunning"
@click="()=>runScript(scriptList[scriptIndex])">运行脚本
</el-button>
<el-button :icon="renderIcon('antd StopOutlined')" link type="danger"
v-if="scriptList[scriptIndex] && scriptList[scriptIndex].isRunning"
@click="()=>stopScript(scriptList[scriptIndex])">停止运行
</el-button>
<el-button :icon="renderIcon('fa EyeDropper')" link
:disabled="scriptList.length === 0 || !scriptList[scriptIndex]"
@ -32,6 +35,9 @@
<el-radio-button v-for="(item, index) in scriptList" :key="index" :class="'script_'+index"
:label="item.name" :value="index" v-menus="scriptMenu" />
</el-radio-group>
<el-button link v-if="scriptList[scriptIndex]"
@click="()=>localRun(scriptList[scriptIndex])">LocalRun
</el-button>
</el-row>
<span class="close" @click="closeMe">
<component :is="renderIcon('element Close')"></component>
@ -49,6 +55,7 @@ import YvSrcEditor from '@/components/YvSrcEditor.vue'
import IWidgets from '../IWidgets.js'
import CodeDropper from '@/core/manager/CodeDropper.js'
import { escByKeyboard } from '@/core/ModelUtils.js'
import scriptRunner from '@/runtime/ScriptRunner.js'
export default {
name: 'ScriptView',
@ -56,12 +63,22 @@ export default {
YvSrcEditor
},
mixins: [IWidgets],
mounted() {
window['ScriptView'] = this
},
unmounted() {
window['ScriptView'] = null
},
data() {
const me = this
return {
scriptIsRunning: false,
scriptIndex: 0,
/**
* 脚本列表
* @type {Array<CustomScript>}
*/
scriptList: [],
searchKeyword: '',
scriptMenu: [
@ -151,15 +168,27 @@ export default {
system.clearLoading()
}
},
runScript() {
this.scriptIsRunning = true
this.viewport.modelManager.executestring(this.currentScript)
.catch(err => {
system.msg('脚本执行失败: ' + err.message, 'error')
})
/**
* 运行脚本
* @type {(script: CustomScript) => void}
*/
runScript(script) {
system.showLoading()
scriptRunner.runScript(script)
.finally(() => {
this.scriptIsRunning = false
system.clearLoading()
})
// this.viewport.modelManager.runScript(script)
},
localRun(script) {
this.viewport.modelManager.executestring(script.content)
},
/**
* 停止脚本
* @type {(script: CustomScript) => void}
*/
stopScript(script) {
this.viewport.modelManager.stopScript(script)
},
setCodeDropper() {
CodeDropper.start(this.viewport, (item) => {
@ -182,7 +211,12 @@ export default {
},
saveScriptsToLocal() {
localforage.setItem('-yvan-lcc-scriptlist-', JSON.stringify({
scriptList: this.scriptList,
scriptList: _.map(this.scriptList, item => {
return {
name: item.name,
content: item.content
}
}),
scriptIndex: this.scriptIndex
})).catch(err => {
@ -211,7 +245,8 @@ export default {
addScript() {
this.scriptList.push({
name: 'Script' + (this.scriptList.length + 1),
content: ''
content: '',
isRunning: false
})
this.scriptIndex = this.scriptList.length - 1
},
@ -235,7 +270,8 @@ export default {
if (!this.scriptList[this.scriptIndex]) {
this.scriptList[this.scriptIndex] = {
name: 'Script' + (this.scriptList.length + 1),
content: ''
content: '',
isRunning: false
}
}
this.scriptList[this.scriptIndex].content = value

246
src/runtime/ScriptRunner.ts

@ -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

130
src/runtime/ToolProxyManager.ts

@ -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)
}
}
}

11
src/types/LCC.d.ts

@ -2,6 +2,17 @@
* LCC API
*/
declare interface LCC {
/**
*
*/
log(from: string, message: string, ...args: any[]): void
/**
*
*/
sleep(timeOfMs: number = 1000): Promise<void>
/**
*
*/

22
src/types/Model.d.ts

@ -386,3 +386,25 @@ interface Cl2If extends EntityIf, Carry, Walk, ForkArm, LiftingArm {
*/
interface ClxIf extends EntityIf, Carry, Walk, ForkArm, LiftingArm {
}
interface CustomScript {
/**
*
*/
name: string
/**
*
*/
content: string
/**
*
*/
isRunning: boolean
/**
* Worker
*/
worker?: Worker;
}

Loading…
Cancel
Save