entry : params.entrySet()) {
+ context.setParameter(entry.getKey(), entry.getValue());
+ }
+
+ // 执行脚本
+ Object result = scriptService.executeScript(
+ testReqVO.getScriptLanguage(),
+ testReqVO.getScriptContent(),
+ context);
+
+ // 更新测试结果(如果是已保存的脚本)
+ if (existingScript != null) {
+ IotProductScriptDO updateObj = new IotProductScriptDO();
+ updateObj.setId(existingScript.getId());
+ updateObj.setLastTestTime(LocalDateTime.now());
+ updateObj.setLastTestResult(1); // 1表示成功
+ productScriptMapper.updateById(updateObj);
+ }
+
+ long executionTime = System.currentTimeMillis() - startTime;
+ return IotProductScriptTestRespVO.success(result, executionTime);
+
+ } catch (Exception e) {
+ log.error("[testProductScript][测试脚本异常]", e);
+
+ // 如果是已保存的脚本,更新测试失败状态
+ if (testReqVO.getId() != null) {
+ try {
+ IotProductScriptDO updateObj = new IotProductScriptDO();
+ updateObj.setId(testReqVO.getId());
+ updateObj.setLastTestTime(LocalDateTime.now());
+ updateObj.setLastTestResult(0); // 0表示失败
+ productScriptMapper.updateById(updateObj);
+ } catch (Exception ex) {
+ log.error("[testProductScript][更新脚本测试结果异常]", ex);
+ }
+ }
+
+ long executionTime = System.currentTimeMillis() - startTime;
+ return IotProductScriptTestRespVO.error(e.getMessage(), executionTime);
+ }
}
@Override
diff --git a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java
index efd6cc0943..6364f5c72d 100644
--- a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java
+++ b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java
@@ -3,10 +3,8 @@ package cn.iocoder.yudao.module.iot.net.component.core.upstream;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*;
+import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
-
-import javax.annotation.Resource;
-
/**
* 设备数据 Upstream 上行客户端
*
diff --git a/yudao-module-iot/yudao-module-iot-script/pom.xml b/yudao-module-iot/yudao-module-iot-script/pom.xml
new file mode 100644
index 0000000000..8b46914a2d
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/pom.xml
@@ -0,0 +1,93 @@
+
+
+
+ yudao-module-iot
+ cn.iocoder.boot
+ ${revision}
+
+ 4.0.0
+ yudao-module-iot-script
+ jar
+
+ ${project.artifactId}
+ IoT 脚本模块,提供 JavaScript 引擎解析等功能
+
+
+
+
+ cn.iocoder.boot
+ yudao-module-iot-api
+ ${revision}
+
+
+
+
+ org.springframework
+ spring-context
+
+
+
+
+ cn.hutool
+ hutool-all
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.graalvm.sdk
+ graal-sdk
+ 22.3.0
+
+
+ org.graalvm.js
+ js
+ 22.3.0
+
+
+ org.graalvm.js
+ js-scriptengine
+ 22.3.0
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-test
+ ${revision}
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+
+
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/ScriptExample.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/ScriptExample.java
new file mode 100644
index 0000000000..7a90251836
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/ScriptExample.java
@@ -0,0 +1,112 @@
+package cn.iocoder.yudao.module.iot.script;
+
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext;
+import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
+import cn.iocoder.yudao.module.iot.script.service.ScriptService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 脚本使用示例类
+ */
+@Slf4j
+@Component
+public class ScriptExample {
+
+ @Autowired
+ private ScriptService scriptService;
+
+ /**
+ * 执行简单的 JavaScript 脚本
+ *
+ * @return 执行结果
+ */
+ public Object executeSimpleScript() {
+ // 简单的脚本内容
+ String script = "var result = a + b; result;";
+
+ // 创建参数
+ Map params = MapUtil.newHashMap();
+ params.put("a", 10);
+ params.put("b", 20);
+
+ // 执行脚本
+ return scriptService.executeJavaScript(script, params);
+ }
+
+ /**
+ * 执行包含函数的 JavaScript 脚本
+ *
+ * @return 执行结果
+ */
+ public Object executeScriptWithFunction() {
+ // 包含函数的脚本内容
+ String script = "function calc(x, y) { return x * y; } calc(a, b);";
+
+ // 创建上下文
+ ScriptContext context = new DefaultScriptContext();
+ context.setParameter("a", 5);
+ context.setParameter("b", 6);
+
+ // 执行脚本
+ return scriptService.executeJavaScript(script, context);
+ }
+
+ /**
+ * 执行包含工具类使用的脚本
+ *
+ * @return 执行结果
+ */
+ public Object executeScriptWithUtils() {
+ // 使用工具类的脚本内容
+ String script = "var data = {name: 'test', value: 123}; utils.toJson(data);";
+
+ // 执行脚本
+ return scriptService.executeJavaScript(script, MapUtil.newHashMap());
+ }
+
+ /**
+ * 执行包含日志输出的脚本
+ *
+ * @return 执行结果
+ */
+ public Object executeScriptWithLogging() {
+ // 包含日志输出的脚本内容
+ String script = "log.info('脚本开始执行...'); " +
+ "var result = a + b; " +
+ "log.info('计算结果: ' + result); " +
+ "result;";
+
+ // 创建参数
+ Map params = MapUtil.newHashMap();
+ params.put("a", 100);
+ params.put("b", 200);
+
+ // 执行脚本
+ return scriptService.executeJavaScript(script, params);
+ }
+
+ /**
+ * 演示脚本安全性验证
+ *
+ * @return 是否安全
+ */
+ public boolean validateScriptSecurity() {
+ // 安全的脚本
+ String safeScript = "var x = 10; var y = 20; x + y;";
+ boolean safeResult = scriptService.validateScript("js", safeScript);
+
+ // 不安全的脚本
+ String unsafeScript = "java.lang.System.exit(0);";
+ boolean unsafeResult = scriptService.validateScript("js", unsafeScript);
+
+ log.info("安全脚本验证结果: {}", safeResult);
+ log.info("不安全脚本验证结果: {}", unsafeResult);
+
+ return safeResult && !unsafeResult;
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/config/ScriptConfiguration.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/config/ScriptConfiguration.java
new file mode 100644
index 0000000000..8339b217f2
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/config/ScriptConfiguration.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.iot.script.config;
+
+import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+/**
+ * 脚本模块配置类
+ */
+@Configuration
+public class ScriptConfiguration {
+
+ /**
+ * 创建脚本引擎工厂
+ *
+ * @return 脚本引擎工厂
+ */
+ @Bean
+ @Primary
+ public ScriptEngineFactory scriptEngineFactory() {
+ return new ScriptEngineFactory();
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DefaultScriptContext.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DefaultScriptContext.java
new file mode 100644
index 0000000000..a75a354307
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DefaultScriptContext.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.iot.script.context;
+
+import cn.hutool.core.map.MapUtil;
+
+import java.util.Map;
+
+/**
+ * 默认脚本上下文实现
+ */
+public class DefaultScriptContext implements ScriptContext {
+
+ /**
+ * 上下文参数
+ */
+ private final Map parameters = MapUtil.newHashMap();
+
+ /**
+ * 上下文函数
+ */
+ private final Map functions = MapUtil.newHashMap();
+
+ @Override
+ public Map getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public Map getFunctions() {
+ return functions;
+ }
+
+ @Override
+ public void setParameter(String key, Object value) {
+ parameters.put(key, value);
+ }
+
+ @Override
+ public Object getParameter(String key) {
+ return parameters.get(key);
+ }
+
+ @Override
+ public void registerFunction(String name, Object function) {
+ functions.put(name, function);
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DeviceScriptContext.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DeviceScriptContext.java
new file mode 100644
index 0000000000..1518736b55
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DeviceScriptContext.java
@@ -0,0 +1,92 @@
+package cn.iocoder.yudao.module.iot.script.context;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+
+/**
+ * 设备脚本上下文,提供设备相关的上下文信息
+ */
+@Slf4j
+public class DeviceScriptContext extends DefaultScriptContext {
+
+ /**
+ * 产品 Key
+ */
+ @Getter
+ private String productKey;
+
+ /**
+ * 设备名称
+ */
+ @Getter
+ private String deviceName;
+
+ /**
+ * 设备属性数据缓存
+ */
+ private Map properties;
+
+ /**
+ * 使用产品 Key 和设备名称初始化上下文
+ *
+ * @param productKey 产品 Key
+ * @param deviceName 设备名称,可以为 null
+ * @return 当前上下文实例,用于链式调用
+ */
+ public DeviceScriptContext withDeviceInfo(String productKey, String deviceName) {
+ this.productKey = productKey;
+ this.deviceName = deviceName;
+
+ // 添加到参数中,便于脚本访问
+ setParameter("productKey", productKey);
+ if (StrUtil.isNotEmpty(deviceName)) {
+ setParameter("deviceName", deviceName);
+ }
+ return this;
+ }
+
+ /**
+ * 设置设备属性数据
+ *
+ * @param properties 属性数据
+ * @return 当前上下文实例,用于链式调用
+ */
+ public DeviceScriptContext withProperties(Map properties) {
+ this.properties = properties;
+ if (MapUtil.isNotEmpty(properties)) {
+ setParameter("properties", properties);
+ }
+ return this;
+ }
+
+ /**
+ * 获取设备属性值
+ *
+ * @param key 属性标识符
+ * @return 属性值
+ */
+ public Object getProperty(String key) {
+ if (MapUtil.isEmpty(properties)) {
+ return null;
+ }
+ return properties.get(key);
+ }
+
+ /**
+ * 设置设备属性值
+ *
+ * @param key 属性标识符
+ * @param value 属性值
+ */
+ public void setProperty(String key, Object value) {
+ if (this.properties == null) {
+ this.properties = MapUtil.newHashMap();
+ setParameter("properties", this.properties);
+ }
+ this.properties.put(key, value);
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/ScriptContext.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/ScriptContext.java
new file mode 100644
index 0000000000..d18644e822
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/ScriptContext.java
@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.iot.script.context;
+
+import java.util.Map;
+
+/**
+ * 脚本上下文接口,定义脚本执行所需的上下文环境
+ */
+public interface ScriptContext {
+
+ /**
+ * 获取上下文参数
+ *
+ * @return 上下文参数
+ */
+ Map getParameters();
+
+ /**
+ * 获取上下文函数
+ *
+ * @return 上下文函数
+ */
+ Map getFunctions();
+
+ /**
+ * 设置上下文参数
+ *
+ * @param key 参数名
+ * @param value 参数值
+ */
+ void setParameter(String key, Object value);
+
+ /**
+ * 获取上下文参数
+ *
+ * @param key 参数名
+ * @return 参数值
+ */
+ Object getParameter(String key);
+
+ /**
+ * 注册函数
+ *
+ * @param name 函数名称
+ * @param function 函数对象
+ */
+ void registerFunction(String name, Object function);
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/AbstractScriptEngine.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/AbstractScriptEngine.java
new file mode 100644
index 0000000000..b69aced139
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/AbstractScriptEngine.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.iot.script.engine;
+
+import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
+import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 抽象脚本引擎,提供脚本引擎的基本框架
+ */
+@Slf4j
+public abstract class AbstractScriptEngine implements ScriptEngine {
+
+ /**
+ * 脚本沙箱,用于提供安全执行环境
+ */
+ protected final ScriptSandbox sandbox;
+
+ /**
+ * 构造函数
+ *
+ * @param sandbox 脚本沙箱
+ */
+ protected AbstractScriptEngine(ScriptSandbox sandbox) {
+ this.sandbox = sandbox;
+ }
+
+ @Override
+ public Object execute(String script, ScriptContext context) {
+ try {
+ // 执行前验证脚本安全性
+ sandbox.validate(script);
+ // 执行脚本
+ return doExecute(script, context);
+ } catch (Exception e) {
+ log.error("执行脚本出错:{}", e.getMessage(), e);
+ throw new RuntimeException("脚本执行失败:" + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 执行脚本的具体实现
+ *
+ * @param script 脚本内容
+ * @param context 脚本上下文
+ * @return 脚本执行结果
+ * @throws Exception 执行异常
+ */
+ protected abstract Object doExecute(String script, ScriptContext context) throws Exception;
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/JsScriptEngine.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/JsScriptEngine.java
new file mode 100644
index 0000000000..222c56eb5a
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/JsScriptEngine.java
@@ -0,0 +1,343 @@
+package cn.iocoder.yudao.module.iot.script.engine;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
+import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox;
+import cn.iocoder.yudao.module.iot.script.util.ScriptUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.graalvm.polyglot.*;
+
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * JavaScript 脚本引擎实现,基于 GraalJS Context API
+ */
+@Slf4j
+public class JsScriptEngine extends AbstractScriptEngine implements AutoCloseable {
+
+ /**
+ * JavaScript 引擎类型
+ */
+ public static final String TYPE = "js";
+
+ /**
+ * 脚本语言类型
+ */
+ private static final String LANGUAGE_ID = "js";
+
+ /**
+ * GraalJS 上下文
+ */
+ private final Context context;
+
+ /**
+ * 脚本源代码缓存
+ */
+ private final Map sourceCache = new ConcurrentHashMap<>();
+
+ /**
+ * 脚本缓存的最大数量
+ */
+ private static final int MAX_CACHE_SIZE = 1000;
+
+ /**
+ * 构造函数
+ *
+ * @param sandbox JavaScript 沙箱
+ */
+ public JsScriptEngine(ScriptSandbox sandbox) {
+ super(sandbox);
+
+ // 创建安全的主机访问配置
+ HostAccess hostAccess = HostAccess.newBuilder()
+ .allowPublicAccess(true) // 允许访问公共方法和字段
+ .allowArrayAccess(true) // 允许数组访问
+ .allowListAccess(true) // 允许 List 访问
+ .allowMapAccess(true) // 允许 Map 访问
+ .build();
+
+ // 创建隔离的临时目录路径
+ Path tempDirectory = Path.of(System.getProperty("java.io.tmpdir"), "graaljs-" + IdUtil.fastSimpleUUID());
+
+ // 初始化 GraalJS 上下文
+ this.context = Context.newBuilder(LANGUAGE_ID)
+ .allowHostAccess(hostAccess) // 使用安全的主机访问配置
+ .allowHostClassLookup(className -> false) // 禁止查找 Java 类
+ .allowIO(false) // 禁止文件 IO
+ .allowNativeAccess(false) // 禁止本地访问
+ .allowCreateThread(false) // 禁止创建线程
+ .allowEnvironmentAccess(org.graalvm.polyglot.EnvironmentAccess.NONE) // 禁止环境变量访问
+ .allowExperimentalOptions(false) // 禁止实验性选项
+ .option("js.ecmascript-version", "2021") // 使用最新的 ECMAScript 标准
+ .option("js.foreign-object-prototype", "false") // 禁用外部对象原型
+ .option("js.nashorn-compat", "false") // 关闭 Nashorn 兼容模式以获得更好性能
+ .build();
+ }
+
+ @Override
+ protected Object doExecute(String script, ScriptContext context) throws Exception {
+ if (StrUtil.isBlank(script)) {
+ return null;
+ }
+
+ try {
+ // 绑定上下文变量
+ bindContextVariables(context);
+
+ // 从缓存获取或创建脚本源
+ Source source = getOrCreateSource(script);
+
+ // 执行脚本并捕获结果,添加超时控制
+ Value result;
+ Thread executionThread = Thread.currentThread();
+ Thread watchdogThread = new Thread(() -> {
+ try {
+ // 等待 5 秒
+ TimeUnit.SECONDS.sleep(5);
+ // 如果执行线程还在运行,中断它
+ if (executionThread.isAlive()) {
+ log.warn("脚本执行超时,强制中断");
+ executionThread.interrupt();
+ }
+ } catch (InterruptedException ignored) {
+ // 忽略中断
+ }
+ });
+
+ watchdogThread.setDaemon(true);
+ watchdogThread.start();
+
+ try {
+ result = this.context.eval(source);
+ } finally {
+ watchdogThread.interrupt(); // 确保看门狗线程停止
+ }
+
+ // 转换结果为 Java 对象
+ return convertResultToJava(result);
+ } catch (PolyglotException e) {
+ handleScriptException(e, script);
+ throw e;
+ }
+ }
+
+ /**
+ * 绑定上下文变量
+ *
+ * @param context 脚本上下文
+ */
+ private void bindContextVariables(ScriptContext context) {
+ Value bindings = this.context.getBindings(LANGUAGE_ID);
+
+ // 添加上下文参数
+ if (MapUtil.isNotEmpty(context.getParameters())) {
+ context.getParameters().forEach(bindings::putMember);
+ }
+
+ // 添加上下文函数
+ if (MapUtil.isNotEmpty(context.getFunctions())) {
+ context.getFunctions().forEach(bindings::putMember);
+ }
+
+ // 添加工具类
+ bindings.putMember("utils", ScriptUtils.getInstance());
+
+ // 添加日志对象
+ bindings.putMember("log", log);
+
+ // 添加控制台输出(限制并重定向到日志)
+ AtomicReference consoleBuffer = new AtomicReference<>(new StringBuilder());
+
+ Value console = this.context.eval(LANGUAGE_ID, "({\n" +
+ " log: function(msg) { _consoleLog(msg, 'INFO'); },\n" +
+ " info: function(msg) { _consoleLog(msg, 'INFO'); },\n" +
+ " warn: function(msg) { _consoleLog(msg, 'WARN'); },\n" +
+ " error: function(msg) { _consoleLog(msg, 'ERROR'); }\n" +
+ "})");
+
+ bindings.putMember("console", console);
+
+ bindings.putMember("_consoleLog", (java.util.function.BiConsumer) (message, level) -> {
+ String formattedMsg = String.valueOf(message);
+ switch (level) {
+ case "INFO":
+ log.info("Script console: {}", formattedMsg);
+ break;
+ case "WARN":
+ log.warn("Script console: {}", formattedMsg);
+ break;
+ case "ERROR":
+ log.error("Script console: {}", formattedMsg);
+ break;
+ default:
+ log.info("Script console: {}", formattedMsg);
+ }
+
+ // 将输出添加到缓冲区
+ StringBuilder buffer = consoleBuffer.get();
+ if (buffer.length() > 10000) {
+ buffer = new StringBuilder();
+ consoleBuffer.set(buffer);
+ }
+ buffer.append(formattedMsg).append("\n");
+ });
+ }
+
+ /**
+ * 从缓存中获取或创建脚本源
+ *
+ * @param script 脚本内容
+ * @return 脚本源
+ */
+ private Source getOrCreateSource(String script) {
+ // 如果缓存太大,清理部分缓存
+ if (sourceCache.size() > MAX_CACHE_SIZE) {
+ int itemsToRemove = (int) (MAX_CACHE_SIZE * 0.2); // 清理 20% 的缓存
+ sourceCache.keySet().stream()
+ .limit(itemsToRemove)
+ .toList()
+ .forEach(sourceCache::remove);
+ }
+
+ // 使用脚本的哈希码作为缓存键
+ String cacheKey = String.valueOf(script.hashCode());
+
+ return sourceCache.computeIfAbsent(cacheKey, key -> {
+ try {
+ return Source.newBuilder(LANGUAGE_ID, script, "script-" + key + ".js").cached(true).build();
+ } catch (Exception e) {
+ log.error("创建脚本源失败: {}", e.getMessage(), e);
+ throw new RuntimeException("创建脚本源失败: " + e.getMessage(), e);
+ }
+ });
+ }
+
+ /**
+ * 将 GraalJS 结果转换为 Java 对象
+ *
+ * @param result GraalJS 执行结果
+ * @return Java 对象
+ */
+ private Object convertResultToJava(Value result) {
+ if (result == null || result.isNull()) {
+ return null;
+ }
+
+ if (result.isString()) {
+ return result.asString();
+ }
+
+ if (result.isNumber()) {
+ if (result.fitsInInt()) {
+ return result.asInt();
+ } else if (result.fitsInLong()) {
+ return result.asLong();
+ } else if (result.fitsInFloat()) {
+ return result.asFloat();
+ } else if (result.fitsInDouble()) {
+ return result.asDouble();
+ }
+ }
+
+ if (result.isBoolean()) {
+ return result.asBoolean();
+ }
+
+ if (result.hasArrayElements()) {
+ int size = (int) result.getArraySize();
+ Object[] array = new Object[size];
+ for (int i = 0; i < size; i++) {
+ array[i] = convertResultToJava(result.getArrayElement(i));
+ }
+ return array;
+ }
+
+ if (result.hasMembers()) {
+ Map map = MapUtil.newHashMap();
+ for (String key : result.getMemberKeys()) {
+ map.put(key, convertResultToJava(result.getMember(key)));
+ }
+ return map;
+ }
+
+ if (result.isHostObject()) {
+ return result.asHostObject();
+ }
+
+ // 默认情况下尝试转换为字符串
+ return result.toString();
+ }
+
+ /**
+ * 处理脚本执行异常
+ *
+ * @param e 多语言异常
+ * @param script 原始脚本
+ */
+ private void handleScriptException(PolyglotException e, String script) {
+ if (e.isCancelled()) {
+ log.error("脚本执行被取消,可能超出资源限制");
+ } else if (e.isHostException()) {
+ Throwable hostException = e.asHostException();
+ log.error("脚本执行时发生 Java 异常: {}", hostException.getMessage(), hostException);
+ } else if (e.isGuestException()) {
+ if (e.getSourceLocation() != null) {
+ log.error("脚本执行错误: {} 位于行 {},列 {}",
+ e.getMessage(),
+ e.getSourceLocation().getStartLine(),
+ e.getSourceLocation().getStartColumn());
+
+ // 尝试显示错误位置上下文
+ try {
+ String[] lines = script.split("\n");
+ int lineNumber = e.getSourceLocation().getStartLine();
+ if (lineNumber > 0 && lineNumber <= lines.length) {
+ int contextStart = Math.max(1, lineNumber - 2);
+ int contextEnd = Math.min(lines.length, lineNumber + 2);
+
+ StringBuilder context = new StringBuilder();
+ for (int i = contextStart; i <= contextEnd; i++) {
+ if (i == lineNumber) {
+ context.append("> "); // 标记错误行
+ } else {
+ context.append(" ");
+ }
+ context.append(i).append(": ").append(lines[i - 1]).append("\n");
+ }
+ log.error("脚本上下文:\n{}", context);
+ }
+ } catch (Exception ignored) {
+ // 忽略上下文显示失败
+ }
+ } else {
+ log.error("脚本执行错误: {}", e.getMessage());
+ }
+ } else {
+ log.error("脚本执行时发生未知错误: {}", e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+
+ @Override
+ public void close() {
+ try {
+ // 清除脚本缓存
+ sourceCache.clear();
+
+ // 关闭 GraalJS 上下文,释放资源
+ context.close(true);
+ } catch (Exception e) {
+ log.warn("关闭 GraalJS 引擎时发生错误: {}", e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngine.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngine.java
new file mode 100644
index 0000000000..7786aea4d5
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngine.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.iot.script.engine;
+
+import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
+
+/**
+ * 脚本引擎接口,定义脚本执行的核心功能
+ */
+public interface ScriptEngine {
+
+ /**
+ * 执行脚本
+ *
+ * @param script 脚本内容
+ * @param context 脚本上下文
+ * @return 脚本执行结果
+ */
+ Object execute(String script, ScriptContext context);
+
+ /**
+ * 获取脚本引擎类型
+ *
+ * @return 脚本引擎类型
+ */
+ String getType();
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngineFactory.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngineFactory.java
new file mode 100644
index 0000000000..25cdc85c7c
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngineFactory.java
@@ -0,0 +1,86 @@
+package cn.iocoder.yudao.module.iot.script.engine;
+
+import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 脚本引擎工厂,用于创建和缓存不同类型的脚本引擎,支持资源生命周期管理
+ */
+@Slf4j
+@Component
+public class ScriptEngineFactory implements DisposableBean {
+
+ /**
+ * 脚本引擎缓存
+ */
+ private final Map engines = new ConcurrentHashMap<>();
+
+ /**
+ * 获取脚本引擎
+ *
+ * @param type 脚本类型
+ * @return 脚本引擎
+ */
+ public ScriptEngine getEngine(String type) {
+ // 从缓存中获取引擎
+ return engines.computeIfAbsent(type, this::createEngine);
+ }
+
+ /**
+ * 创建脚本引擎
+ *
+ * @param type 脚本类型
+ * @return 脚本引擎
+ */
+ private ScriptEngine createEngine(String type) {
+ try {
+ if (JsScriptEngine.TYPE.equals(type)) {
+ log.info("创建 GraalJS 脚本引擎");
+ return new JsScriptEngine(new JsSandbox());
+ }
+
+ log.warn("不支持的脚本类型: {}", type);
+ return null;
+ } catch (Exception e) {
+ log.error("创建脚本引擎 [{}] 失败: {}", type, e.getMessage(), e);
+ throw new RuntimeException("创建脚本引擎失败: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 释放指定类型的引擎资源
+ *
+ * @param type 脚本类型
+ */
+ public void releaseEngine(String type) {
+ ScriptEngine engine = engines.remove(type);
+ if (engine instanceof AutoCloseable) {
+ try {
+ ((AutoCloseable) engine).close();
+ log.info("已释放脚本引擎资源: {}", type);
+ } catch (Exception e) {
+ log.warn("释放脚本引擎 [{}] 资源时发生错误: {}", type, e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * 清理所有引擎资源
+ */
+ public void releaseAllEngines() {
+ engines.keySet().forEach(this::releaseEngine);
+ log.info("已清理所有脚本引擎资源");
+ }
+
+ @Override
+ public void destroy() {
+ // 应用关闭时,释放所有引擎资源
+ log.info("应用关闭,释放所有脚本引擎资源...");
+ releaseAllEngines();
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/GraalJsExample.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/GraalJsExample.java
new file mode 100644
index 0000000000..445b4410be
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/GraalJsExample.java
@@ -0,0 +1,208 @@
+package cn.iocoder.yudao.module.iot.script.example;
+
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext;
+import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
+import cn.iocoder.yudao.module.iot.script.service.ScriptService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * GraalJS 脚本引擎示例
+ *
+ * 展示了如何使用 GraalJS 脚本引擎的各种功能
+ */
+@Slf4j
+@Component
+public class GraalJsExample {
+
+ @Autowired
+ private ScriptService scriptService;
+
+ /**
+ * 执行简单的 JavaScript 脚本
+ *
+ * @return 执行结果
+ */
+ public Object executeSimpleScript() {
+ // 简单的脚本内容
+ String script = "var result = a + b; result;";
+
+ // 创建参数
+ Map params = MapUtil.newHashMap();
+ params.put("a", 10);
+ params.put("b", 20);
+
+ // 执行脚本
+ return scriptService.executeJavaScript(script, params);
+ }
+
+ /**
+ * 执行现代 JavaScript 语法(ES6+)
+ *
+ * @return 执行结果
+ */
+ public Object executeModernJavaScript() {
+ // 使用现代 JavaScript 语法
+ String script = "// 使用箭头函数\n" +
+ "const add = (a, b) => a + b;\n" +
+ "\n" +
+ "// 使用解构赋值\n" +
+ "const {c, d} = params;\n" +
+ "\n" +
+ "// 使用模板字符串\n" +
+ "const result = `计算结果: ${add(c, d)}`;\n" +
+ "\n" +
+ "// 使用可选链操作符\n" +
+ "const value = params?.e?.value ?? 'default';\n" +
+ "\n" +
+ "// 返回一个对象\n" +
+ "({\n" +
+ " sum: add(c, d),\n" +
+ " message: result,\n" +
+ " defaultValue: value\n" +
+ "})";
+
+ // 创建参数
+ Map params = MapUtil.newHashMap();
+ params.put("params", MapUtil.builder()
+ .put("c", 30)
+ .put("d", 40)
+ .build());
+
+ // 执行脚本
+ return scriptService.executeJavaScript(script, params);
+ }
+
+ /**
+ * 执行带错误处理的脚本
+ *
+ * @return 执行结果
+ */
+ public Object executeWithErrorHandling() {
+ // 包含错误处理的脚本
+ String script = "try {\n" +
+ " // 故意制造错误\n" +
+ " if (!nonExistentVar) {\n" +
+ " throw new Error('手动抛出的错误');\n" +
+ " }\n" +
+ "} catch (error) {\n" +
+ " console.error('捕获到错误: ' + error.message);\n" +
+ " return { success: false, error: error.message };\n" +
+ "}\n" +
+ "\n" +
+ "return { success: true, data: 'No error' };";
+
+ // 执行脚本
+ return scriptService.executeJavaScript(script, MapUtil.newHashMap());
+ }
+
+ /**
+ * 演示超时控制
+ *
+ * @return 执行结果
+ */
+ public Object executeWithTimeout() {
+ // 这个脚本会导致无限循环
+ String script = "// 无限循环\n" +
+ "var counter = 0;\n" +
+ "while(true) {\n" +
+ " counter++;\n" +
+ " if (counter % 1000000 === 0) {\n" +
+ " console.log('Still running: ' + counter);\n" +
+ " }\n" +
+ "}\n" +
+ "return counter;";
+
+ // 使用 CompletableFuture 和超时控制
+ CompletableFuture