Commit 2954445d347f38820da3f93d5a00a9e1ef814459

Authored by 安浩浩
1 parent ae96ff4a

【功能新增】IoT: 新增脚本模块,支持 JavaScript 引擎解析,提供示例脚本和安全性验证功能,更新相关依赖和配置。

Showing 24 changed files with 2125 additions and 105 deletions
yudao-module-iot/pom.xml
@@ -11,6 +11,7 @@ @@ -11,6 +11,7 @@
11 <module>yudao-module-iot-api</module> 11 <module>yudao-module-iot-api</module>
12 <module>yudao-module-iot-biz</module> 12 <module>yudao-module-iot-biz</module>
13 <module>yudao-module-iot-net-components</module> 13 <module>yudao-module-iot-net-components</module>
  14 + <module>yudao-module-iot-script</module>
14 <!-- <module>yudao-module-iot-plugins</module>--> 15 <!-- <module>yudao-module-iot-plugins</module>-->
15 </modules> 16 </modules>
16 <modelVersion>4.0.0</modelVersion> 17 <modelVersion>4.0.0</modelVersion>
yudao-module-iot/yudao-module-iot-api/pom.xml
@@ -37,10 +37,10 @@ @@ -37,10 +37,10 @@
37 <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> 37 <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
38 </dependency> 38 </dependency>
39 39
40 - <dependency>  
41 - <groupId>org.pf4j</groupId> <!-- PF4J:内置插件机制 -->  
42 - <artifactId>pf4j-spring</artifactId>  
43 - </dependency> 40 + <!-- <dependency>-->
  41 + <!-- <groupId>org.pf4j</groupId> &lt;!&ndash; PF4J:内置插件机制 &ndash;&gt;-->
  42 + <!-- <artifactId>pf4j-spring</artifactId>-->
  43 + <!-- </dependency>-->
44 44
45 <!-- 参数校验 --> 45 <!-- 参数校验 -->
46 <dependency> 46 <dependency>
yudao-module-iot/yudao-module-iot-biz/pom.xml
@@ -69,13 +69,6 @@ @@ -69,13 +69,6 @@
69 <artifactId>yudao-spring-boot-starter-excel</artifactId> 69 <artifactId>yudao-spring-boot-starter-excel</artifactId>
70 </dependency> 70 </dependency>
71 71
72 - <!-- 脚本插件相关 -->  
73 - <!-- <dependency>-->  
74 - <!-- <groupId>cn.iocoder.boot</groupId>-->  
75 - <!-- <artifactId>yudao-module-iot-plugin-script</artifactId>-->  
76 - <!-- <version>${revision}</version>-->  
77 - <!-- </dependency>-->  
78 -  
79 <!-- 消息队列相关 --> 72 <!-- 消息队列相关 -->
80 <dependency> 73 <dependency>
81 <groupId>org.apache.rocketmq</groupId> 74 <groupId>org.apache.rocketmq</groupId>
@@ -93,11 +86,6 @@ @@ -93,11 +86,6 @@
93 <optional>true</optional> 86 <optional>true</optional>
94 </dependency> 87 </dependency>
95 88
96 - <!-- <dependency>-->  
97 - <!-- <groupId>org.pf4j</groupId> &lt;!&ndash; PF4J:内置插件机制 &ndash;&gt;-->  
98 - <!-- <artifactId>pf4j-spring</artifactId>-->  
99 - <!-- </dependency>-->  
100 -  
101 <!-- TODO @芋艿:bom 管理 --> 89 <!-- TODO @芋艿:bom 管理 -->
102 <dependency> 90 <dependency>
103 <groupId>org.apache.groovy</groupId> 91 <groupId>org.apache.groovy</groupId>
@@ -145,6 +133,15 @@ @@ -145,6 +133,15 @@
145 <artifactId>yudao-module-iot-net-component-emqx</artifactId> 133 <artifactId>yudao-module-iot-net-component-emqx</artifactId>
146 <version>${revision}</version> 134 <version>${revision}</version>
147 </dependency> 135 </dependency>
  136 +
  137 + <!-- 脚本相关 -->
  138 + <dependency>
  139 + <groupId>cn.iocoder.boot</groupId>
  140 + <artifactId>yudao-module-iot-script</artifactId>
  141 + <version>${revision}</version>
  142 + </dependency>
  143 +
  144 +
148 </dependencies> 145 </dependencies>
149 146
150 </project> 147 </project>
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductScriptController.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.*; 6 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.*;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO; 7 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO;
  8 +import cn.iocoder.yudao.module.iot.script.example.ProductScriptSamples;
8 import cn.iocoder.yudao.module.iot.service.product.IotProductScriptService; 9 import cn.iocoder.yudao.module.iot.service.product.IotProductScriptService;
9 import io.swagger.v3.oas.annotations.Operation; 10 import io.swagger.v3.oas.annotations.Operation;
10 import io.swagger.v3.oas.annotations.Parameter; 11 import io.swagger.v3.oas.annotations.Parameter;
@@ -28,6 +29,9 @@ public class IotProductScriptController { @@ -28,6 +29,9 @@ public class IotProductScriptController {
28 @Resource 29 @Resource
29 private IotProductScriptService productScriptService; 30 private IotProductScriptService productScriptService;
30 31
  32 + @Resource
  33 + private ProductScriptSamples scriptSamples;
  34 +
31 @PostMapping("/create") 35 @PostMapping("/create")
32 @Operation(summary = "创建产品脚本") 36 @Operation(summary = "创建产品脚本")
33 @PreAuthorize("@ss.hasPermission('iot:product-script:create')") 37 @PreAuthorize("@ss.hasPermission('iot:product-script:create')")
@@ -96,4 +100,26 @@ public class IotProductScriptController { @@ -96,4 +100,26 @@ public class IotProductScriptController {
96 productScriptService.updateProductScriptStatus(updateStatusReqVO.getId(), updateStatusReqVO.getStatus()); 100 productScriptService.updateProductScriptStatus(updateStatusReqVO.getId(), updateStatusReqVO.getStatus());
97 return success(true); 101 return success(true);
98 } 102 }
  103 +
  104 + @GetMapping("/sample")
  105 + @Operation(summary = "获取示例脚本")
  106 + @Parameter(name = "type", description = "脚本类型(1=属性解析, 2=事件解析, 3=命令编码)", required = true, example = "1")
  107 + @PreAuthorize("@ss.hasPermission('iot:product-script:query')")
  108 + public CommonResult<String> getSampleScript(@RequestParam("type") Integer type) {
  109 + String sample;
  110 + switch (type) {
  111 + case 1:
  112 + sample = scriptSamples.getPropertyParserSample();
  113 + break;
  114 + case 2:
  115 + sample = scriptSamples.getEventParserSample();
  116 + break;
  117 + case 3:
  118 + sample = scriptSamples.getCommandEncoderSample();
  119 + break;
  120 + default:
  121 + sample = "// 不支持的脚本类型";
  122 + }
  123 + return success(sample);
  124 + }
99 } 125 }
100 \ No newline at end of file 126 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductScriptServiceImpl.java
@@ -9,13 +9,18 @@ import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProduct @@ -9,13 +9,18 @@ import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProduct
9 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; 9 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
10 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO; 10 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO;
11 import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductScriptMapper; 11 import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductScriptMapper;
  12 +import cn.iocoder.yudao.module.iot.script.context.DeviceScriptContext;
  13 +import cn.iocoder.yudao.module.iot.script.service.ScriptService;
12 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 14 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
13 import jakarta.annotation.Resource; 15 import jakarta.annotation.Resource;
14 import lombok.extern.slf4j.Slf4j; 16 import lombok.extern.slf4j.Slf4j;
15 import org.springframework.stereotype.Service; 17 import org.springframework.stereotype.Service;
16 import org.springframework.validation.annotation.Validated; 18 import org.springframework.validation.annotation.Validated;
17 19
  20 +import java.time.LocalDateTime;
  21 +import java.util.HashMap;
18 import java.util.List; 22 import java.util.List;
  23 +import java.util.Map;
19 24
20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 25 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
21 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS; 26 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS;
@@ -38,8 +43,8 @@ public class IotProductScriptServiceImpl implements IotProductScriptService { @@ -38,8 +43,8 @@ public class IotProductScriptServiceImpl implements IotProductScriptService {
38 @Resource 43 @Resource
39 private IotProductService productService; 44 private IotProductService productService;
40 45
41 -// @Resource  
42 -// private ScriptService scriptService; 46 + @Resource
  47 + private ScriptService scriptService;
43 48
44 @Override 49 @Override
45 public Long createProductScript(IotProductScriptSaveReqVO createReqVO) { 50 public Long createProductScript(IotProductScriptSaveReqVO createReqVO) {
@@ -116,90 +121,103 @@ public class IotProductScriptServiceImpl implements IotProductScriptService { @@ -116,90 +121,103 @@ public class IotProductScriptServiceImpl implements IotProductScriptService {
116 121
117 @Override 122 @Override
118 public IotProductScriptTestRespVO testProductScript(IotProductScriptTestReqVO testReqVO) { 123 public IotProductScriptTestRespVO testProductScript(IotProductScriptTestReqVO testReqVO) {
119 -// long startTime = System.currentTimeMillis();  
120 -//  
121 -// try {  
122 -// // 验证产品是否存在  
123 -// validateProductExists(testReqVO.getProductId());  
124 -//  
125 -// // 根据ID获取已保存的脚本(如果有)  
126 -// IotProductScriptDO existingScript = null;  
127 -// if (testReqVO.getId() != null) {  
128 -// existingScript = getProductScript(testReqVO.getId());  
129 -// }  
130 -//  
131 -// // 创建测试上下文  
132 -// PluginScriptContext context = new PluginScriptContext();  
133 -// IotProductDO product = productService.getProduct(testReqVO.getProductId());  
134 -//  
135 -// // 设置设备上下文(使用产品信息,没有具体设备)  
136 -// context.withDeviceContext(product.getProductKey(), null);  
137 -//  
138 -// // 设置输入参数  
139 -// Map<String, Object> params = new HashMap<>();  
140 -// params.put("input", testReqVO.getTestInput());  
141 -// params.put("productKey", product.getProductKey());  
142 -// params.put("scriptType", testReqVO.getScriptType());  
143 -//  
144 -// // 根据脚本类型设置特定参数  
145 -// switch (testReqVO.getScriptType()) {  
146 -// case 1: // PROPERTY_PARSER  
147 -// params.put("method", "property");  
148 -// break;  
149 -// case 2: // EVENT_PARSER  
150 -// params.put("method", "event");  
151 -// params.put("identifier", "default");  
152 -// break;  
153 -// case 3: // COMMAND_ENCODER  
154 -// params.put("method", "command");  
155 -// break;  
156 -// default:  
157 -// // 默认不添加额外参数  
158 -// }  
159 -//  
160 -// // 添加所有参数到上下文  
161 -// for (Map.Entry<String, Object> entry : params.entrySet()) {  
162 -// context.setParameter(entry.getKey(), entry.getValue());  
163 -// }  
164 -//  
165 -// // 执行脚本  
166 -// Object result = scriptService.executeScript(  
167 -// testReqVO.getScriptLanguage(),  
168 -// testReqVO.getScriptContent(),  
169 -// context);  
170 -//  
171 -// // 更新测试结果(如果是已保存的脚本)  
172 -// if (existingScript != null) {  
173 -// IotProductScriptDO updateObj = new IotProductScriptDO();  
174 -// updateObj.setId(existingScript.getId());  
175 -// updateObj.setLastTestTime(LocalDateTime.now());  
176 -// updateObj.setLastTestResult(1); // 1表示成功  
177 -// productScriptMapper.updateById(updateObj);  
178 -// }  
179 -//  
180 -// long executionTime = System.currentTimeMillis() - startTime;  
181 -// return IotProductScriptTestRespVO.success(result, executionTime);  
182 -//  
183 -// } catch (Exception e) {  
184 -// log.error("[testProductScript][测试脚本异常]", e);  
185 -//  
186 -// // 如果是已保存的脚本,更新测试失败状态  
187 -// if (testReqVO.getId() != null) {  
188 -// try {  
189 -// IotProductScriptDO updateObj = new IotProductScriptDO();  
190 -// updateObj.setId(testReqVO.getId());  
191 -// updateObj.setLastTestTime(LocalDateTime.now());  
192 -// updateObj.setLastTestResult(0); // 0表示失败  
193 -// productScriptMapper.updateById(updateObj);  
194 -// } catch (Exception ex) {  
195 -// log.error("[testProductScript][更新脚本测试结果异常]", ex);  
196 -// }  
197 -// }  
198 -//  
199 -// long executionTime = System.currentTimeMillis() - startTime;  
200 -// return IotProductScriptTestRespVO.error(e.getMessage(), executionTime);  
201 -// }  
202 - return null; 124 + long startTime = System.currentTimeMillis();
  125 +
  126 + try {
  127 + // 验证产品是否存在
  128 + validateProductExists(testReqVO.getProductId());
  129 +
  130 + // 根据ID获取已保存的脚本(如果有)
  131 + IotProductScriptDO existingScript = null;
  132 + if (testReqVO.getId() != null) {
  133 + existingScript = getProductScript(testReqVO.getId());
  134 + }
  135 +
  136 + // 创建测试上下文
  137 + IotProductDO product = productService.getProduct(testReqVO.getProductId());
  138 + DeviceScriptContext context = new DeviceScriptContext();
  139 +
  140 + // 设置设备上下文(使用产品信息,测试时无具体设备)
  141 + context.withDeviceInfo(product.getProductKey(), null);
  142 +
  143 + // 设置输入参数
  144 + Map<String, Object> params = new HashMap<>();
  145 + params.put("input", testReqVO.getTestInput());
  146 + params.put("productKey", product.getProductKey());
  147 + params.put("scriptType", testReqVO.getScriptType());
  148 +
  149 + // 根据脚本类型设置特定参数
  150 + switch (testReqVO.getScriptType()) {
  151 + case 1: // PROPERTY_PARSER
  152 + params.put("method", "property");
  153 + // 添加一些模拟的属性数据
  154 + Map<String, Object> properties = new HashMap<>();
  155 + properties.put("temp", 25.5);
  156 + properties.put("humidity", 60);
  157 + context.withProperties(properties);
  158 + break;
  159 + case 2: // EVENT_PARSER
  160 + params.put("method", "event");
  161 + params.put("identifier", "default");
  162 + // 添加事件数据
  163 + Map<String, Object> eventParams = new HashMap<>();
  164 + eventParams.put("timestamp", System.currentTimeMillis());
  165 + params.put("eventParams", eventParams);
  166 + break;
  167 + case 3: // COMMAND_ENCODER
  168 + params.put("method", "command");
  169 + // 添加命令参数
  170 + Map<String, Object> cmdParams = new HashMap<>();
  171 + cmdParams.put("cmdName", "setValue");
  172 + cmdParams.put("cmdValue", 100);
  173 + params.put("cmdParams", cmdParams);
  174 + break;
  175 + default:
  176 + // 默认不添加额外参数
  177 + }
  178 +
  179 + // 添加所有参数到上下文
  180 + for (Map.Entry<String, Object> entry : params.entrySet()) {
  181 + context.setParameter(entry.getKey(), entry.getValue());
  182 + }
  183 +
  184 + // 执行脚本
  185 + Object result = scriptService.executeScript(
  186 + testReqVO.getScriptLanguage(),
  187 + testReqVO.getScriptContent(),
  188 + context);
  189 +
  190 + // 更新测试结果(如果是已保存的脚本)
  191 + if (existingScript != null) {
  192 + IotProductScriptDO updateObj = new IotProductScriptDO();
  193 + updateObj.setId(existingScript.getId());
  194 + updateObj.setLastTestTime(LocalDateTime.now());
  195 + updateObj.setLastTestResult(1); // 1表示成功
  196 + productScriptMapper.updateById(updateObj);
  197 + }
  198 +
  199 + long executionTime = System.currentTimeMillis() - startTime;
  200 + return IotProductScriptTestRespVO.success(result, executionTime);
  201 +
  202 + } catch (Exception e) {
  203 + log.error("[testProductScript][测试脚本异常]", e);
  204 +
  205 + // 如果是已保存的脚本,更新测试失败状态
  206 + if (testReqVO.getId() != null) {
  207 + try {
  208 + IotProductScriptDO updateObj = new IotProductScriptDO();
  209 + updateObj.setId(testReqVO.getId());
  210 + updateObj.setLastTestTime(LocalDateTime.now());
  211 + updateObj.setLastTestResult(0); // 0表示失败
  212 + productScriptMapper.updateById(updateObj);
  213 + } catch (Exception ex) {
  214 + log.error("[testProductScript][更新脚本测试结果异常]", ex);
  215 + }
  216 + }
  217 +
  218 + long executionTime = System.currentTimeMillis() - startTime;
  219 + return IotProductScriptTestRespVO.error(e.getMessage(), executionTime);
  220 + }
203 } 221 }
204 222
205 @Override 223 @Override
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; @@ -3,10 +3,8 @@ package cn.iocoder.yudao.module.iot.net.component.core.upstream;
3 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; 4 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
5 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; 5 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*;
  6 +import jakarta.annotation.Resource;
6 import lombok.extern.slf4j.Slf4j; 7 import lombok.extern.slf4j.Slf4j;
7 -  
8 -import javax.annotation.Resource;  
9 -  
10 /** 8 /**
11 * 设备数据 Upstream 上行客户端 9 * 设备数据 Upstream 上行客户端
12 * <p> 10 * <p>
yudao-module-iot/yudao-module-iot-script/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <artifactId>yudao-module-iot</artifactId>
  7 + <groupId>cn.iocoder.boot</groupId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modelVersion>4.0.0</modelVersion>
  11 + <artifactId>yudao-module-iot-script</artifactId>
  12 + <packaging>jar</packaging>
  13 +
  14 + <name>${project.artifactId}</name>
  15 + <description>IoT 脚本模块,提供 JavaScript 引擎解析等功能</description>
  16 +
  17 + <dependencies>
  18 + <!-- 引入公共模块 -->
  19 + <dependency>
  20 + <groupId>cn.iocoder.boot</groupId>
  21 + <artifactId>yudao-module-iot-api</artifactId>
  22 + <version>${revision}</version>
  23 + </dependency>
  24 +
  25 + <!-- Spring 相关依赖 -->
  26 + <dependency>
  27 + <groupId>org.springframework</groupId>
  28 + <artifactId>spring-context</artifactId>
  29 + </dependency>
  30 +
  31 + <!-- 工具类相关 -->
  32 + <dependency>
  33 + <groupId>cn.hutool</groupId>
  34 + <artifactId>hutool-all</artifactId>
  35 + </dependency>
  36 + <dependency>
  37 + <groupId>org.projectlombok</groupId>
  38 + <artifactId>lombok</artifactId>
  39 + <optional>true</optional>
  40 + </dependency>
  41 + <dependency>
  42 + <groupId>org.slf4j</groupId>
  43 + <artifactId>slf4j-api</artifactId>
  44 + </dependency>
  45 +
  46 + <!-- JavaScript 引擎 - 使用 GraalJS 替代 Nashorn -->
  47 + <dependency>
  48 + <groupId>org.graalvm.sdk</groupId>
  49 + <artifactId>graal-sdk</artifactId>
  50 + <version>22.3.0</version>
  51 + </dependency>
  52 + <dependency>
  53 + <groupId>org.graalvm.js</groupId>
  54 + <artifactId>js</artifactId>
  55 + <version>22.3.0</version>
  56 + </dependency>
  57 + <dependency>
  58 + <groupId>org.graalvm.js</groupId>
  59 + <artifactId>js-scriptengine</artifactId>
  60 + <version>22.3.0</version>
  61 + </dependency>
  62 +
  63 + <!-- 测试相关 -->
  64 + <dependency>
  65 + <groupId>org.springframework.boot</groupId>
  66 + <artifactId>spring-boot-starter-test</artifactId>
  67 + <scope>test</scope>
  68 + <exclusions>
  69 + <exclusion>
  70 + <groupId>org.junit.vintage</groupId>
  71 + <artifactId>junit-vintage-engine</artifactId>
  72 + </exclusion>
  73 + </exclusions>
  74 + </dependency>
  75 + <dependency>
  76 + <groupId>cn.iocoder.boot</groupId>
  77 + <artifactId>yudao-spring-boot-starter-test</artifactId>
  78 + <version>${revision}</version>
  79 + <scope>test</scope>
  80 + </dependency>
  81 + <dependency>
  82 + <groupId>org.mockito</groupId>
  83 + <artifactId>mockito-core</artifactId>
  84 + <scope>test</scope>
  85 + </dependency>
  86 + <dependency>
  87 + <groupId>org.mockito</groupId>
  88 + <artifactId>mockito-junit-jupiter</artifactId>
  89 + <scope>test</scope>
  90 + </dependency>
  91 + </dependencies>
  92 +
  93 +</project>
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/ScriptExample.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext;
  5 +import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
  6 +import cn.iocoder.yudao.module.iot.script.service.ScriptService;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.beans.factory.annotation.Autowired;
  9 +import org.springframework.stereotype.Component;
  10 +
  11 +import java.util.Map;
  12 +
  13 +/**
  14 + * 脚本使用示例类
  15 + */
  16 +@Slf4j
  17 +@Component
  18 +public class ScriptExample {
  19 +
  20 + @Autowired
  21 + private ScriptService scriptService;
  22 +
  23 + /**
  24 + * 执行简单的 JavaScript 脚本
  25 + *
  26 + * @return 执行结果
  27 + */
  28 + public Object executeSimpleScript() {
  29 + // 简单的脚本内容
  30 + String script = "var result = a + b; result;";
  31 +
  32 + // 创建参数
  33 + Map<String, Object> params = MapUtil.newHashMap();
  34 + params.put("a", 10);
  35 + params.put("b", 20);
  36 +
  37 + // 执行脚本
  38 + return scriptService.executeJavaScript(script, params);
  39 + }
  40 +
  41 + /**
  42 + * 执行包含函数的 JavaScript 脚本
  43 + *
  44 + * @return 执行结果
  45 + */
  46 + public Object executeScriptWithFunction() {
  47 + // 包含函数的脚本内容
  48 + String script = "function calc(x, y) { return x * y; } calc(a, b);";
  49 +
  50 + // 创建上下文
  51 + ScriptContext context = new DefaultScriptContext();
  52 + context.setParameter("a", 5);
  53 + context.setParameter("b", 6);
  54 +
  55 + // 执行脚本
  56 + return scriptService.executeJavaScript(script, context);
  57 + }
  58 +
  59 + /**
  60 + * 执行包含工具类使用的脚本
  61 + *
  62 + * @return 执行结果
  63 + */
  64 + public Object executeScriptWithUtils() {
  65 + // 使用工具类的脚本内容
  66 + String script = "var data = {name: 'test', value: 123}; utils.toJson(data);";
  67 +
  68 + // 执行脚本
  69 + return scriptService.executeJavaScript(script, MapUtil.newHashMap());
  70 + }
  71 +
  72 + /**
  73 + * 执行包含日志输出的脚本
  74 + *
  75 + * @return 执行结果
  76 + */
  77 + public Object executeScriptWithLogging() {
  78 + // 包含日志输出的脚本内容
  79 + String script = "log.info('脚本开始执行...'); " +
  80 + "var result = a + b; " +
  81 + "log.info('计算结果: ' + result); " +
  82 + "result;";
  83 +
  84 + // 创建参数
  85 + Map<String, Object> params = MapUtil.newHashMap();
  86 + params.put("a", 100);
  87 + params.put("b", 200);
  88 +
  89 + // 执行脚本
  90 + return scriptService.executeJavaScript(script, params);
  91 + }
  92 +
  93 + /**
  94 + * 演示脚本安全性验证
  95 + *
  96 + * @return 是否安全
  97 + */
  98 + public boolean validateScriptSecurity() {
  99 + // 安全的脚本
  100 + String safeScript = "var x = 10; var y = 20; x + y;";
  101 + boolean safeResult = scriptService.validateScript("js", safeScript);
  102 +
  103 + // 不安全的脚本
  104 + String unsafeScript = "java.lang.System.exit(0);";
  105 + boolean unsafeResult = scriptService.validateScript("js", unsafeScript);
  106 +
  107 + log.info("安全脚本验证结果: {}", safeResult);
  108 + log.info("不安全脚本验证结果: {}", unsafeResult);
  109 +
  110 + return safeResult && !unsafeResult;
  111 + }
  112 +}
0 \ No newline at end of file 113 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/config/ScriptConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.config;
  2 +
  3 +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory;
  4 +import org.springframework.context.annotation.Bean;
  5 +import org.springframework.context.annotation.Configuration;
  6 +import org.springframework.context.annotation.Primary;
  7 +
  8 +/**
  9 + * 脚本模块配置类
  10 + */
  11 +@Configuration
  12 +public class ScriptConfiguration {
  13 +
  14 + /**
  15 + * 创建脚本引擎工厂
  16 + *
  17 + * @return 脚本引擎工厂
  18 + */
  19 + @Bean
  20 + @Primary
  21 + public ScriptEngineFactory scriptEngineFactory() {
  22 + return new ScriptEngineFactory();
  23 + }
  24 +}
0 \ No newline at end of file 25 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DefaultScriptContext.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.context;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +
  5 +import java.util.Map;
  6 +
  7 +/**
  8 + * 默认脚本上下文实现
  9 + */
  10 +public class DefaultScriptContext implements ScriptContext {
  11 +
  12 + /**
  13 + * 上下文参数
  14 + */
  15 + private final Map<String, Object> parameters = MapUtil.newHashMap();
  16 +
  17 + /**
  18 + * 上下文函数
  19 + */
  20 + private final Map<String, Object> functions = MapUtil.newHashMap();
  21 +
  22 + @Override
  23 + public Map<String, Object> getParameters() {
  24 + return parameters;
  25 + }
  26 +
  27 + @Override
  28 + public Map<String, Object> getFunctions() {
  29 + return functions;
  30 + }
  31 +
  32 + @Override
  33 + public void setParameter(String key, Object value) {
  34 + parameters.put(key, value);
  35 + }
  36 +
  37 + @Override
  38 + public Object getParameter(String key) {
  39 + return parameters.get(key);
  40 + }
  41 +
  42 + @Override
  43 + public void registerFunction(String name, Object function) {
  44 + functions.put(name, function);
  45 + }
  46 +}
0 \ No newline at end of file 47 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DeviceScriptContext.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.context;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import lombok.Getter;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +
  8 +import java.util.Map;
  9 +
  10 +/**
  11 + * 设备脚本上下文,提供设备相关的上下文信息
  12 + */
  13 +@Slf4j
  14 +public class DeviceScriptContext extends DefaultScriptContext {
  15 +
  16 + /**
  17 + * 产品 Key
  18 + */
  19 + @Getter
  20 + private String productKey;
  21 +
  22 + /**
  23 + * 设备名称
  24 + */
  25 + @Getter
  26 + private String deviceName;
  27 +
  28 + /**
  29 + * 设备属性数据缓存
  30 + */
  31 + private Map<String, Object> properties;
  32 +
  33 + /**
  34 + * 使用产品 Key 和设备名称初始化上下文
  35 + *
  36 + * @param productKey 产品 Key
  37 + * @param deviceName 设备名称,可以为 null
  38 + * @return 当前上下文实例,用于链式调用
  39 + */
  40 + public DeviceScriptContext withDeviceInfo(String productKey, String deviceName) {
  41 + this.productKey = productKey;
  42 + this.deviceName = deviceName;
  43 +
  44 + // 添加到参数中,便于脚本访问
  45 + setParameter("productKey", productKey);
  46 + if (StrUtil.isNotEmpty(deviceName)) {
  47 + setParameter("deviceName", deviceName);
  48 + }
  49 + return this;
  50 + }
  51 +
  52 + /**
  53 + * 设置设备属性数据
  54 + *
  55 + * @param properties 属性数据
  56 + * @return 当前上下文实例,用于链式调用
  57 + */
  58 + public DeviceScriptContext withProperties(Map<String, Object> properties) {
  59 + this.properties = properties;
  60 + if (MapUtil.isNotEmpty(properties)) {
  61 + setParameter("properties", properties);
  62 + }
  63 + return this;
  64 + }
  65 +
  66 + /**
  67 + * 获取设备属性值
  68 + *
  69 + * @param key 属性标识符
  70 + * @return 属性值
  71 + */
  72 + public Object getProperty(String key) {
  73 + if (MapUtil.isEmpty(properties)) {
  74 + return null;
  75 + }
  76 + return properties.get(key);
  77 + }
  78 +
  79 + /**
  80 + * 设置设备属性值
  81 + *
  82 + * @param key 属性标识符
  83 + * @param value 属性值
  84 + */
  85 + public void setProperty(String key, Object value) {
  86 + if (this.properties == null) {
  87 + this.properties = MapUtil.newHashMap();
  88 + setParameter("properties", this.properties);
  89 + }
  90 + this.properties.put(key, value);
  91 + }
  92 +}
0 \ No newline at end of file 93 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/ScriptContext.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.context;
  2 +
  3 +import java.util.Map;
  4 +
  5 +/**
  6 + * 脚本上下文接口,定义脚本执行所需的上下文环境
  7 + */
  8 +public interface ScriptContext {
  9 +
  10 + /**
  11 + * 获取上下文参数
  12 + *
  13 + * @return 上下文参数
  14 + */
  15 + Map<String, Object> getParameters();
  16 +
  17 + /**
  18 + * 获取上下文函数
  19 + *
  20 + * @return 上下文函数
  21 + */
  22 + Map<String, Object> getFunctions();
  23 +
  24 + /**
  25 + * 设置上下文参数
  26 + *
  27 + * @param key 参数名
  28 + * @param value 参数值
  29 + */
  30 + void setParameter(String key, Object value);
  31 +
  32 + /**
  33 + * 获取上下文参数
  34 + *
  35 + * @param key 参数名
  36 + * @return 参数值
  37 + */
  38 + Object getParameter(String key);
  39 +
  40 + /**
  41 + * 注册函数
  42 + *
  43 + * @param name 函数名称
  44 + * @param function 函数对象
  45 + */
  46 + void registerFunction(String name, Object function);
  47 +}
0 \ No newline at end of file 48 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/AbstractScriptEngine.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.engine;
  2 +
  3 +import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
  4 +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +
  7 +/**
  8 + * 抽象脚本引擎,提供脚本引擎的基本框架
  9 + */
  10 +@Slf4j
  11 +public abstract class AbstractScriptEngine implements ScriptEngine {
  12 +
  13 + /**
  14 + * 脚本沙箱,用于提供安全执行环境
  15 + */
  16 + protected final ScriptSandbox sandbox;
  17 +
  18 + /**
  19 + * 构造函数
  20 + *
  21 + * @param sandbox 脚本沙箱
  22 + */
  23 + protected AbstractScriptEngine(ScriptSandbox sandbox) {
  24 + this.sandbox = sandbox;
  25 + }
  26 +
  27 + @Override
  28 + public Object execute(String script, ScriptContext context) {
  29 + try {
  30 + // 执行前验证脚本安全性
  31 + sandbox.validate(script);
  32 + // 执行脚本
  33 + return doExecute(script, context);
  34 + } catch (Exception e) {
  35 + log.error("执行脚本出错:{}", e.getMessage(), e);
  36 + throw new RuntimeException("脚本执行失败:" + e.getMessage(), e);
  37 + }
  38 + }
  39 +
  40 + /**
  41 + * 执行脚本的具体实现
  42 + *
  43 + * @param script 脚本内容
  44 + * @param context 脚本上下文
  45 + * @return 脚本执行结果
  46 + * @throws Exception 执行异常
  47 + */
  48 + protected abstract Object doExecute(String script, ScriptContext context) throws Exception;
  49 +}
0 \ No newline at end of file 50 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/JsScriptEngine.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.engine;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
  7 +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox;
  8 +import cn.iocoder.yudao.module.iot.script.util.ScriptUtils;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.graalvm.polyglot.*;
  11 +
  12 +import java.nio.file.Path;
  13 +import java.util.Map;
  14 +import java.util.concurrent.ConcurrentHashMap;
  15 +import java.util.concurrent.TimeUnit;
  16 +import java.util.concurrent.atomic.AtomicReference;
  17 +
  18 +/**
  19 + * JavaScript 脚本引擎实现,基于 GraalJS Context API
  20 + */
  21 +@Slf4j
  22 +public class JsScriptEngine extends AbstractScriptEngine implements AutoCloseable {
  23 +
  24 + /**
  25 + * JavaScript 引擎类型
  26 + */
  27 + public static final String TYPE = "js";
  28 +
  29 + /**
  30 + * 脚本语言类型
  31 + */
  32 + private static final String LANGUAGE_ID = "js";
  33 +
  34 + /**
  35 + * GraalJS 上下文
  36 + */
  37 + private final Context context;
  38 +
  39 + /**
  40 + * 脚本源代码缓存
  41 + */
  42 + private final Map<String, Source> sourceCache = new ConcurrentHashMap<>();
  43 +
  44 + /**
  45 + * 脚本缓存的最大数量
  46 + */
  47 + private static final int MAX_CACHE_SIZE = 1000;
  48 +
  49 + /**
  50 + * 构造函数
  51 + *
  52 + * @param sandbox JavaScript 沙箱
  53 + */
  54 + public JsScriptEngine(ScriptSandbox sandbox) {
  55 + super(sandbox);
  56 +
  57 + // 创建安全的主机访问配置
  58 + HostAccess hostAccess = HostAccess.newBuilder()
  59 + .allowPublicAccess(true) // 允许访问公共方法和字段
  60 + .allowArrayAccess(true) // 允许数组访问
  61 + .allowListAccess(true) // 允许 List 访问
  62 + .allowMapAccess(true) // 允许 Map 访问
  63 + .build();
  64 +
  65 + // 创建隔离的临时目录路径
  66 + Path tempDirectory = Path.of(System.getProperty("java.io.tmpdir"), "graaljs-" + IdUtil.fastSimpleUUID());
  67 +
  68 + // 初始化 GraalJS 上下文
  69 + this.context = Context.newBuilder(LANGUAGE_ID)
  70 + .allowHostAccess(hostAccess) // 使用安全的主机访问配置
  71 + .allowHostClassLookup(className -> false) // 禁止查找 Java 类
  72 + .allowIO(false) // 禁止文件 IO
  73 + .allowNativeAccess(false) // 禁止本地访问
  74 + .allowCreateThread(false) // 禁止创建线程
  75 + .allowEnvironmentAccess(org.graalvm.polyglot.EnvironmentAccess.NONE) // 禁止环境变量访问
  76 + .allowExperimentalOptions(false) // 禁止实验性选项
  77 + .option("js.ecmascript-version", "2021") // 使用最新的 ECMAScript 标准
  78 + .option("js.foreign-object-prototype", "false") // 禁用外部对象原型
  79 + .option("js.nashorn-compat", "false") // 关闭 Nashorn 兼容模式以获得更好性能
  80 + .build();
  81 + }
  82 +
  83 + @Override
  84 + protected Object doExecute(String script, ScriptContext context) throws Exception {
  85 + if (StrUtil.isBlank(script)) {
  86 + return null;
  87 + }
  88 +
  89 + try {
  90 + // 绑定上下文变量
  91 + bindContextVariables(context);
  92 +
  93 + // 从缓存获取或创建脚本源
  94 + Source source = getOrCreateSource(script);
  95 +
  96 + // 执行脚本并捕获结果,添加超时控制
  97 + Value result;
  98 + Thread executionThread = Thread.currentThread();
  99 + Thread watchdogThread = new Thread(() -> {
  100 + try {
  101 + // 等待 5 秒
  102 + TimeUnit.SECONDS.sleep(5);
  103 + // 如果执行线程还在运行,中断它
  104 + if (executionThread.isAlive()) {
  105 + log.warn("脚本执行超时,强制中断");
  106 + executionThread.interrupt();
  107 + }
  108 + } catch (InterruptedException ignored) {
  109 + // 忽略中断
  110 + }
  111 + });
  112 +
  113 + watchdogThread.setDaemon(true);
  114 + watchdogThread.start();
  115 +
  116 + try {
  117 + result = this.context.eval(source);
  118 + } finally {
  119 + watchdogThread.interrupt(); // 确保看门狗线程停止
  120 + }
  121 +
  122 + // 转换结果为 Java 对象
  123 + return convertResultToJava(result);
  124 + } catch (PolyglotException e) {
  125 + handleScriptException(e, script);
  126 + throw e;
  127 + }
  128 + }
  129 +
  130 + /**
  131 + * 绑定上下文变量
  132 + *
  133 + * @param context 脚本上下文
  134 + */
  135 + private void bindContextVariables(ScriptContext context) {
  136 + Value bindings = this.context.getBindings(LANGUAGE_ID);
  137 +
  138 + // 添加上下文参数
  139 + if (MapUtil.isNotEmpty(context.getParameters())) {
  140 + context.getParameters().forEach(bindings::putMember);
  141 + }
  142 +
  143 + // 添加上下文函数
  144 + if (MapUtil.isNotEmpty(context.getFunctions())) {
  145 + context.getFunctions().forEach(bindings::putMember);
  146 + }
  147 +
  148 + // 添加工具类
  149 + bindings.putMember("utils", ScriptUtils.getInstance());
  150 +
  151 + // 添加日志对象
  152 + bindings.putMember("log", log);
  153 +
  154 + // 添加控制台输出(限制并重定向到日志)
  155 + AtomicReference<StringBuilder> consoleBuffer = new AtomicReference<>(new StringBuilder());
  156 +
  157 + Value console = this.context.eval(LANGUAGE_ID, "({\n" +
  158 + " log: function(msg) { _consoleLog(msg, 'INFO'); },\n" +
  159 + " info: function(msg) { _consoleLog(msg, 'INFO'); },\n" +
  160 + " warn: function(msg) { _consoleLog(msg, 'WARN'); },\n" +
  161 + " error: function(msg) { _consoleLog(msg, 'ERROR'); }\n" +
  162 + "})");
  163 +
  164 + bindings.putMember("console", console);
  165 +
  166 + bindings.putMember("_consoleLog", (java.util.function.BiConsumer<String, String>) (message, level) -> {
  167 + String formattedMsg = String.valueOf(message);
  168 + switch (level) {
  169 + case "INFO":
  170 + log.info("Script console: {}", formattedMsg);
  171 + break;
  172 + case "WARN":
  173 + log.warn("Script console: {}", formattedMsg);
  174 + break;
  175 + case "ERROR":
  176 + log.error("Script console: {}", formattedMsg);
  177 + break;
  178 + default:
  179 + log.info("Script console: {}", formattedMsg);
  180 + }
  181 +
  182 + // 将输出添加到缓冲区
  183 + StringBuilder buffer = consoleBuffer.get();
  184 + if (buffer.length() > 10000) {
  185 + buffer = new StringBuilder();
  186 + consoleBuffer.set(buffer);
  187 + }
  188 + buffer.append(formattedMsg).append("\n");
  189 + });
  190 + }
  191 +
  192 + /**
  193 + * 从缓存中获取或创建脚本源
  194 + *
  195 + * @param script 脚本内容
  196 + * @return 脚本源
  197 + */
  198 + private Source getOrCreateSource(String script) {
  199 + // 如果缓存太大,清理部分缓存
  200 + if (sourceCache.size() > MAX_CACHE_SIZE) {
  201 + int itemsToRemove = (int) (MAX_CACHE_SIZE * 0.2); // 清理 20% 的缓存
  202 + sourceCache.keySet().stream()
  203 + .limit(itemsToRemove)
  204 + .toList()
  205 + .forEach(sourceCache::remove);
  206 + }
  207 +
  208 + // 使用脚本的哈希码作为缓存键
  209 + String cacheKey = String.valueOf(script.hashCode());
  210 +
  211 + return sourceCache.computeIfAbsent(cacheKey, key -> {
  212 + try {
  213 + return Source.newBuilder(LANGUAGE_ID, script, "script-" + key + ".js").cached(true).build();
  214 + } catch (Exception e) {
  215 + log.error("创建脚本源失败: {}", e.getMessage(), e);
  216 + throw new RuntimeException("创建脚本源失败: " + e.getMessage(), e);
  217 + }
  218 + });
  219 + }
  220 +
  221 + /**
  222 + * 将 GraalJS 结果转换为 Java 对象
  223 + *
  224 + * @param result GraalJS 执行结果
  225 + * @return Java 对象
  226 + */
  227 + private Object convertResultToJava(Value result) {
  228 + if (result == null || result.isNull()) {
  229 + return null;
  230 + }
  231 +
  232 + if (result.isString()) {
  233 + return result.asString();
  234 + }
  235 +
  236 + if (result.isNumber()) {
  237 + if (result.fitsInInt()) {
  238 + return result.asInt();
  239 + } else if (result.fitsInLong()) {
  240 + return result.asLong();
  241 + } else if (result.fitsInFloat()) {
  242 + return result.asFloat();
  243 + } else if (result.fitsInDouble()) {
  244 + return result.asDouble();
  245 + }
  246 + }
  247 +
  248 + if (result.isBoolean()) {
  249 + return result.asBoolean();
  250 + }
  251 +
  252 + if (result.hasArrayElements()) {
  253 + int size = (int) result.getArraySize();
  254 + Object[] array = new Object[size];
  255 + for (int i = 0; i < size; i++) {
  256 + array[i] = convertResultToJava(result.getArrayElement(i));
  257 + }
  258 + return array;
  259 + }
  260 +
  261 + if (result.hasMembers()) {
  262 + Map<String, Object> map = MapUtil.newHashMap();
  263 + for (String key : result.getMemberKeys()) {
  264 + map.put(key, convertResultToJava(result.getMember(key)));
  265 + }
  266 + return map;
  267 + }
  268 +
  269 + if (result.isHostObject()) {
  270 + return result.asHostObject();
  271 + }
  272 +
  273 + // 默认情况下尝试转换为字符串
  274 + return result.toString();
  275 + }
  276 +
  277 + /**
  278 + * 处理脚本执行异常
  279 + *
  280 + * @param e 多语言异常
  281 + * @param script 原始脚本
  282 + */
  283 + private void handleScriptException(PolyglotException e, String script) {
  284 + if (e.isCancelled()) {
  285 + log.error("脚本执行被取消,可能超出资源限制");
  286 + } else if (e.isHostException()) {
  287 + Throwable hostException = e.asHostException();
  288 + log.error("脚本执行时发生 Java 异常: {}", hostException.getMessage(), hostException);
  289 + } else if (e.isGuestException()) {
  290 + if (e.getSourceLocation() != null) {
  291 + log.error("脚本执行错误: {} 位于行 {},列 {}",
  292 + e.getMessage(),
  293 + e.getSourceLocation().getStartLine(),
  294 + e.getSourceLocation().getStartColumn());
  295 +
  296 + // 尝试显示错误位置上下文
  297 + try {
  298 + String[] lines = script.split("\n");
  299 + int lineNumber = e.getSourceLocation().getStartLine();
  300 + if (lineNumber > 0 && lineNumber <= lines.length) {
  301 + int contextStart = Math.max(1, lineNumber - 2);
  302 + int contextEnd = Math.min(lines.length, lineNumber + 2);
  303 +
  304 + StringBuilder context = new StringBuilder();
  305 + for (int i = contextStart; i <= contextEnd; i++) {
  306 + if (i == lineNumber) {
  307 + context.append("> "); // 标记错误行
  308 + } else {
  309 + context.append(" ");
  310 + }
  311 + context.append(i).append(": ").append(lines[i - 1]).append("\n");
  312 + }
  313 + log.error("脚本上下文:\n{}", context);
  314 + }
  315 + } catch (Exception ignored) {
  316 + // 忽略上下文显示失败
  317 + }
  318 + } else {
  319 + log.error("脚本执行错误: {}", e.getMessage());
  320 + }
  321 + } else {
  322 + log.error("脚本执行时发生未知错误: {}", e.getMessage(), e);
  323 + }
  324 + }
  325 +
  326 + @Override
  327 + public String getType() {
  328 + return TYPE;
  329 + }
  330 +
  331 + @Override
  332 + public void close() {
  333 + try {
  334 + // 清除脚本缓存
  335 + sourceCache.clear();
  336 +
  337 + // 关闭 GraalJS 上下文,释放资源
  338 + context.close(true);
  339 + } catch (Exception e) {
  340 + log.warn("关闭 GraalJS 引擎时发生错误: {}", e.getMessage());
  341 + }
  342 + }
  343 +}
0 \ No newline at end of file 344 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngine.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.engine;
  2 +
  3 +import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
  4 +
  5 +/**
  6 + * 脚本引擎接口,定义脚本执行的核心功能
  7 + */
  8 +public interface ScriptEngine {
  9 +
  10 + /**
  11 + * 执行脚本
  12 + *
  13 + * @param script 脚本内容
  14 + * @param context 脚本上下文
  15 + * @return 脚本执行结果
  16 + */
  17 + Object execute(String script, ScriptContext context);
  18 +
  19 + /**
  20 + * 获取脚本引擎类型
  21 + *
  22 + * @return 脚本引擎类型
  23 + */
  24 + String getType();
  25 +}
0 \ No newline at end of file 26 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngineFactory.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.engine;
  2 +
  3 +import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.beans.factory.DisposableBean;
  6 +import org.springframework.stereotype.Component;
  7 +
  8 +import java.util.Map;
  9 +import java.util.concurrent.ConcurrentHashMap;
  10 +
  11 +/**
  12 + * 脚本引擎工厂,用于创建和缓存不同类型的脚本引擎,支持资源生命周期管理
  13 + */
  14 +@Slf4j
  15 +@Component
  16 +public class ScriptEngineFactory implements DisposableBean {
  17 +
  18 + /**
  19 + * 脚本引擎缓存
  20 + */
  21 + private final Map<String, ScriptEngine> engines = new ConcurrentHashMap<>();
  22 +
  23 + /**
  24 + * 获取脚本引擎
  25 + *
  26 + * @param type 脚本类型
  27 + * @return 脚本引擎
  28 + */
  29 + public ScriptEngine getEngine(String type) {
  30 + // 从缓存中获取引擎
  31 + return engines.computeIfAbsent(type, this::createEngine);
  32 + }
  33 +
  34 + /**
  35 + * 创建脚本引擎
  36 + *
  37 + * @param type 脚本类型
  38 + * @return 脚本引擎
  39 + */
  40 + private ScriptEngine createEngine(String type) {
  41 + try {
  42 + if (JsScriptEngine.TYPE.equals(type)) {
  43 + log.info("创建 GraalJS 脚本引擎");
  44 + return new JsScriptEngine(new JsSandbox());
  45 + }
  46 +
  47 + log.warn("不支持的脚本类型: {}", type);
  48 + return null;
  49 + } catch (Exception e) {
  50 + log.error("创建脚本引擎 [{}] 失败: {}", type, e.getMessage(), e);
  51 + throw new RuntimeException("创建脚本引擎失败: " + e.getMessage(), e);
  52 + }
  53 + }
  54 +
  55 + /**
  56 + * 释放指定类型的引擎资源
  57 + *
  58 + * @param type 脚本类型
  59 + */
  60 + public void releaseEngine(String type) {
  61 + ScriptEngine engine = engines.remove(type);
  62 + if (engine instanceof AutoCloseable) {
  63 + try {
  64 + ((AutoCloseable) engine).close();
  65 + log.info("已释放脚本引擎资源: {}", type);
  66 + } catch (Exception e) {
  67 + log.warn("释放脚本引擎 [{}] 资源时发生错误: {}", type, e.getMessage());
  68 + }
  69 + }
  70 + }
  71 +
  72 + /**
  73 + * 清理所有引擎资源
  74 + */
  75 + public void releaseAllEngines() {
  76 + engines.keySet().forEach(this::releaseEngine);
  77 + log.info("已清理所有脚本引擎资源");
  78 + }
  79 +
  80 + @Override
  81 + public void destroy() {
  82 + // 应用关闭时,释放所有引擎资源
  83 + log.info("应用关闭,释放所有脚本引擎资源...");
  84 + releaseAllEngines();
  85 + }
  86 +}
0 \ No newline at end of file 87 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/GraalJsExample.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.example;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext;
  5 +import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
  6 +import cn.iocoder.yudao.module.iot.script.service.ScriptService;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.beans.factory.annotation.Autowired;
  9 +import org.springframework.stereotype.Component;
  10 +
  11 +import java.util.Map;
  12 +import java.util.concurrent.CompletableFuture;
  13 +import java.util.concurrent.ExecutionException;
  14 +import java.util.concurrent.TimeUnit;
  15 +import java.util.concurrent.TimeoutException;
  16 +
  17 +/**
  18 + * GraalJS 脚本引擎示例
  19 + * <p>
  20 + * 展示了如何使用 GraalJS 脚本引擎的各种功能
  21 + */
  22 +@Slf4j
  23 +@Component
  24 +public class GraalJsExample {
  25 +
  26 + @Autowired
  27 + private ScriptService scriptService;
  28 +
  29 + /**
  30 + * 执行简单的 JavaScript 脚本
  31 + *
  32 + * @return 执行结果
  33 + */
  34 + public Object executeSimpleScript() {
  35 + // 简单的脚本内容
  36 + String script = "var result = a + b; result;";
  37 +
  38 + // 创建参数
  39 + Map<String, Object> params = MapUtil.newHashMap();
  40 + params.put("a", 10);
  41 + params.put("b", 20);
  42 +
  43 + // 执行脚本
  44 + return scriptService.executeJavaScript(script, params);
  45 + }
  46 +
  47 + /**
  48 + * 执行现代 JavaScript 语法(ES6+)
  49 + *
  50 + * @return 执行结果
  51 + */
  52 + public Object executeModernJavaScript() {
  53 + // 使用现代 JavaScript 语法
  54 + String script = "// 使用箭头函数\n" +
  55 + "const add = (a, b) => a + b;\n" +
  56 + "\n" +
  57 + "// 使用解构赋值\n" +
  58 + "const {c, d} = params;\n" +
  59 + "\n" +
  60 + "// 使用模板字符串\n" +
  61 + "const result = `计算结果: ${add(c, d)}`;\n" +
  62 + "\n" +
  63 + "// 使用可选链操作符\n" +
  64 + "const value = params?.e?.value ?? 'default';\n" +
  65 + "\n" +
  66 + "// 返回一个对象\n" +
  67 + "({\n" +
  68 + " sum: add(c, d),\n" +
  69 + " message: result,\n" +
  70 + " defaultValue: value\n" +
  71 + "})";
  72 +
  73 + // 创建参数
  74 + Map<String, Object> params = MapUtil.newHashMap();
  75 + params.put("params", MapUtil.builder()
  76 + .put("c", 30)
  77 + .put("d", 40)
  78 + .build());
  79 +
  80 + // 执行脚本
  81 + return scriptService.executeJavaScript(script, params);
  82 + }
  83 +
  84 + /**
  85 + * 执行带错误处理的脚本
  86 + *
  87 + * @return 执行结果
  88 + */
  89 + public Object executeWithErrorHandling() {
  90 + // 包含错误处理的脚本
  91 + String script = "try {\n" +
  92 + " // 故意制造错误\n" +
  93 + " if (!nonExistentVar) {\n" +
  94 + " throw new Error('手动抛出的错误');\n" +
  95 + " }\n" +
  96 + "} catch (error) {\n" +
  97 + " console.error('捕获到错误: ' + error.message);\n" +
  98 + " return { success: false, error: error.message };\n" +
  99 + "}\n" +
  100 + "\n" +
  101 + "return { success: true, data: 'No error' };";
  102 +
  103 + // 执行脚本
  104 + return scriptService.executeJavaScript(script, MapUtil.newHashMap());
  105 + }
  106 +
  107 + /**
  108 + * 演示超时控制
  109 + *
  110 + * @return 执行结果
  111 + */
  112 + public Object executeWithTimeout() {
  113 + // 这个脚本会导致无限循环
  114 + String script = "// 无限循环\n" +
  115 + "var counter = 0;\n" +
  116 + "while(true) {\n" +
  117 + " counter++;\n" +
  118 + " if (counter % 1000000 === 0) {\n" +
  119 + " console.log('Still running: ' + counter);\n" +
  120 + " }\n" +
  121 + "}\n" +
  122 + "return counter;";
  123 +
  124 + // 使用 CompletableFuture 和超时控制
  125 + CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
  126 + try {
  127 + return scriptService.executeJavaScript(script, MapUtil.newHashMap());
  128 + } catch (Exception e) {
  129 + log.error("脚本执行失败: {}", e.getMessage());
  130 + return "执行失败: " + e.getMessage();
  131 + }
  132 + });
  133 +
  134 + try {
  135 + // 等待结果,最多 10 秒
  136 + return future.get(10, TimeUnit.SECONDS);
  137 + } catch (InterruptedException | ExecutionException e) {
  138 + return "执行异常: " + e.getMessage();
  139 + } catch (TimeoutException e) {
  140 + future.cancel(true);
  141 + return "执行超时,已强制终止";
  142 + }
  143 + }
  144 +
  145 + /**
  146 + * 演示 JSON 处理
  147 + *
  148 + * @return 执行结果
  149 + */
  150 + public Object executeJsonProcessing() {
  151 + // JSON 处理示例
  152 + String script = "// 解析传入的 JSON\n" +
  153 + "var data = JSON.parse(jsonString);\n" +
  154 + "\n" +
  155 + "// 处理 JSON 数据\n" +
  156 + "var result = {\n" +
  157 + " name: data.name.toUpperCase(),\n" +
  158 + " age: data.age + 1,\n" +
  159 + " address: data.address || 'Unknown',\n" +
  160 + " tags: [...data.tags, 'processed'],\n" +
  161 + " timestamp: Date.now()\n" +
  162 + "};\n" +
  163 + "\n" +
  164 + "// 转换回 JSON 字符串\n" +
  165 + "JSON.stringify(result);";
  166 +
  167 + // 创建上下文
  168 + ScriptContext context = new DefaultScriptContext();
  169 + context.setParameter("jsonString",
  170 + "{\"name\":\"test user\",\"age\":25,\"tags\":[\"tag1\",\"tag2\"]}");
  171 +
  172 + // 执行脚本
  173 + return scriptService.executeJavaScript(script, context);
  174 + }
  175 +
  176 + /**
  177 + * 演示数据转换
  178 + *
  179 + * @return 执行结果
  180 + */
  181 + public Object executeDataConversion() {
  182 + // 数据转换和处理示例
  183 + String script = "// 使用 utils 工具类进行数据转换\n" +
  184 + "var stringValue = utils.isEmpty(input) ? \"默认值\" : input;\n" +
  185 + "var numberValue = utils.convert(stringValue, \"number\");\n" +
  186 + "\n" +
  187 + "// 创建一个复杂数据结构\n" +
  188 + "var result = {\n" +
  189 + " original: input,\n" +
  190 + " stringValue: stringValue,\n" +
  191 + " numberValue: numberValue,\n" +
  192 + " booleanValue: Boolean(numberValue),\n" +
  193 + " isValid: utils.isNotEmpty(input)\n" +
  194 + "};\n" +
  195 + "\n" +
  196 + "// 记录处理结果\n" +
  197 + "log.info(\"处理结果: \" + utils.toJson(result));\n" +
  198 + "\n" +
  199 + "return result;";
  200 +
  201 + // 创建参数
  202 + Map<String, Object> params = MapUtil.newHashMap();
  203 + params.put("input", "42");
  204 +
  205 + // 执行脚本
  206 + return scriptService.executeJavaScript(script, params);
  207 + }
  208 +}
0 \ No newline at end of file 209 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/ProductScriptSamples.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.example;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +import org.springframework.stereotype.Component;
  5 +
  6 +/**
  7 + * 产品脚本示例类,提供各种类型的产品脚本示例代码
  8 + */
  9 +@Slf4j
  10 +@Component
  11 +public class ProductScriptSamples {
  12 +
  13 + /**
  14 + * 获取属性解析脚本示例
  15 + *
  16 + * @return 属性解析脚本示例代码
  17 + */
  18 + public String getPropertyParserSample() {
  19 + return "/**\n" +
  20 + " * 属性上报数据解析脚本示例\n" +
  21 + " * @param input 设备上报的原始数据\n" +
  22 + " * @param productKey 产品标识\n" +
  23 + " * @param method 方法类型,固定为 property\n" +
  24 + " * @param properties 当前设备的属性数据\n" +
  25 + " * @return 解析后的属性数据\n" +
  26 + " */\n" +
  27 + "function parseProperty(input, productKey) {\n" +
  28 + " // 记录日志\n" +
  29 + " console.log('开始解析属性数据: ' + input);\n" +
  30 + " \n" +
  31 + " try {\n" +
  32 + " // 假设上报的是 JSON 字符串\n" +
  33 + " var data = JSON.parse(input);\n" +
  34 + " \n" +
  35 + " // 构建属性数据结构\n" +
  36 + " var result = {\n" +
  37 + " // 属性上报的时间戳,毫秒级\n" +
  38 + " timestamp: data.timestamp || Date.now(),\n" +
  39 + " // 属性数据\n" +
  40 + " params: {}\n" +
  41 + " };\n" +
  42 + " \n" +
  43 + " // 处理属性值\n" +
  44 + " if (data.temperature) {\n" +
  45 + " result.params.temperature = parseFloat(data.temperature);\n" +
  46 + " }\n" +
  47 + " \n" +
  48 + " if (data.humidity) {\n" +
  49 + " result.params.humidity = parseFloat(data.humidity);\n" +
  50 + " }\n" +
  51 + " \n" +
  52 + " console.log('属性解析结果: ' + JSON.stringify(result));\n" +
  53 + " return result;\n" +
  54 + " } catch (error) {\n" +
  55 + " console.error('解析属性数据失败: ' + error.message);\n" +
  56 + " throw new Error('解析失败: ' + error.message);\n" +
  57 + " }\n" +
  58 + "};\n" +
  59 + "\n" +
  60 + "// 执行解析\n" +
  61 + "parseProperty(input, productKey);";
  62 + }
  63 +
  64 + /**
  65 + * 获取事件解析脚本示例
  66 + *
  67 + * @return 事件解析脚本示例代码
  68 + */
  69 + public String getEventParserSample() {
  70 + return "/**\n" +
  71 + " * 事件数据解析脚本示例\n" +
  72 + " * @param input 设备上报的原始数据\n" +
  73 + " * @param productKey 产品标识\n" +
  74 + " * @param method 方法类型,固定为 event\n" +
  75 + " * @param identifier 事件标识符\n" +
  76 + " * @return 解析后的事件数据\n" +
  77 + " */\n" +
  78 + "function parseEvent(input, productKey, identifier) {\n" +
  79 + " // 记录日志\n" +
  80 + " console.log('开始解析事件数据: ' + input);\n" +
  81 + " console.log('事件标识符: ' + identifier);\n" +
  82 + " \n" +
  83 + " try {\n" +
  84 + " // 假设上报的是 JSON 字符串\n" +
  85 + " var data = JSON.parse(input);\n" +
  86 + " \n" +
  87 + " // 构建事件数据结构\n" +
  88 + " var result = {\n" +
  89 + " // 事件标识符\n" +
  90 + " identifier: identifier || 'alert',\n" +
  91 + " // 事件上报的时间戳,毫秒级\n" +
  92 + " timestamp: data.timestamp || Date.now(),\n" +
  93 + " // 事件参数\n" +
  94 + " params: {}\n" +
  95 + " };\n" +
  96 + " \n" +
  97 + " // 根据不同事件类型处理参数\n" +
  98 + " if (result.identifier === 'alert') {\n" +
  99 + " result.params.level = data.level || 'info';\n" +
  100 + " result.params.message = data.message || '';\n" +
  101 + " } else if (result.identifier === 'error') {\n" +
  102 + " result.params.code = data.code || 0;\n" +
  103 + " result.params.message = data.message || '';\n" +
  104 + " }\n" +
  105 + " \n" +
  106 + " console.log('事件解析结果: ' + JSON.stringify(result));\n" +
  107 + " return result;\n" +
  108 + " } catch (error) {\n" +
  109 + " console.error('解析事件数据失败: ' + error.message);\n" +
  110 + " throw new Error('解析失败: ' + error.message);\n" +
  111 + " }\n" +
  112 + "};\n" +
  113 + "\n" +
  114 + "// 执行解析\n" +
  115 + "parseEvent(input, productKey, identifier);";
  116 + }
  117 +
  118 + /**
  119 + * 获取命令编码脚本示例
  120 + *
  121 + * @return 命令编码脚本示例代码
  122 + */
  123 + public String getCommandEncoderSample() {
  124 + return "/**\n" +
  125 + " * 命令数据编码脚本示例\n" +
  126 + " * @param input 平台下发的命令数据\n" +
  127 + " * @param productKey 产品标识\n" +
  128 + " * @param method 方法类型,固定为 command\n" +
  129 + " * @param cmdParams 命令参数\n" +
  130 + " * @return 编码后的命令数据\n" +
  131 + " */\n" +
  132 + "function encodeCommand(input, productKey, cmdParams) {\n" +
  133 + " // 记录日志\n" +
  134 + " console.log('开始编码命令数据: ' + input);\n" +
  135 + " console.log('命令参数: ' + JSON.stringify(cmdParams));\n" +
  136 + " \n" +
  137 + " try {\n" +
  138 + " // 输入可能是 JSON 字符串或对象\n" +
  139 + " var data = typeof input === 'string' ? JSON.parse(input) : input;\n" +
  140 + " \n" +
  141 + " // 获取命令名称和值\n" +
  142 + " var cmdName = cmdParams.cmdName || '';\n" +
  143 + " var cmdValue = cmdParams.cmdValue;\n" +
  144 + " \n" +
  145 + " // 构建设备可识别的命令格式\n" +
  146 + " var result = {\n" +
  147 + " cmd: cmdName,\n" +
  148 + " value: cmdValue,\n" +
  149 + " timestamp: Date.now()\n" +
  150 + " };\n" +
  151 + " \n" +
  152 + " // 根据不同命令类型构建参数\n" +
  153 + " if (cmdName === 'setValue') {\n" +
  154 + " // 无需额外处理\n" +
  155 + " } else if (cmdName === 'control') {\n" +
  156 + " result.mode = data.mode || 'auto';\n" +
  157 + " result.action = data.action || 'start';\n" +
  158 + " }\n" +
  159 + " \n" +
  160 + " // 转换为设备能识别的格式(此处以 JSON 字符串为例)\n" +
  161 + " var encodedResult = JSON.stringify(result);\n" +
  162 + " \n" +
  163 + " console.log('命令编码结果: ' + encodedResult);\n" +
  164 + " return encodedResult;\n" +
  165 + " } catch (error) {\n" +
  166 + " console.error('编码命令数据失败: ' + error.message);\n" +
  167 + " throw new Error('编码失败: ' + error.message);\n" +
  168 + " }\n" +
  169 + "};\n" +
  170 + "\n" +
  171 + "// 执行编码\n" +
  172 + "encodeCommand(input, productKey, cmdParams);";
  173 + }
  174 +}
0 \ No newline at end of file 175 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/package-info.java 0 → 100644
  1 +/**
  2 + * IoT 脚本模块,提供脚本引擎、执行环境和沙箱功能,支持 JavaScript 脚本的执行
  3 + */
  4 +package cn.iocoder.yudao.module.iot.script;
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/JsSandbox.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.sandbox;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +
  7 +import java.util.ArrayList;
  8 +import java.util.Arrays;
  9 +import java.util.List;
  10 +import java.util.regex.Matcher;
  11 +import java.util.regex.Pattern;
  12 +
  13 +/**
  14 + * JavaScript 沙箱实现,提供脚本安全性验证
  15 + */
  16 +@Slf4j
  17 +public class JsSandbox implements ScriptSandbox {
  18 +
  19 + /**
  20 + * JavaScript 沙箱类型
  21 + */
  22 + public static final String TYPE = "js";
  23 +
  24 + /**
  25 + * 不安全的关键字列表
  26 + */
  27 + private final List<String> unsafeKeywords = new ArrayList<>();
  28 +
  29 + /**
  30 + * 可能导致高资源消耗的关键字
  31 + */
  32 + private final List<String> highResourceKeywords = new ArrayList<>();
  33 +
  34 + /**
  35 + * 不安全的包/类访问模式
  36 + */
  37 + private final List<Pattern> unsafePatterns = new ArrayList<>();
  38 +
  39 + /**
  40 + * 递归或循环嵌套深度检测模式
  41 + */
  42 + private final List<Pattern> recursionPatterns = new ArrayList<>();
  43 +
  44 + /**
  45 + * 允许的脚本最大长度(字节)
  46 + */
  47 + private static final int MAX_SCRIPT_LENGTH = 100 * 1024; // 100KB
  48 +
  49 + /**
  50 + * 脚本安全验证超时时间(毫秒)
  51 + */
  52 + private static final long VALIDATION_TIMEOUT = 1000; // 1秒
  53 +
  54 + /**
  55 + * 构造函数,初始化不安全的关键字和模式
  56 + */
  57 + public JsSandbox() {
  58 + // 初始化 Java 相关的不安全关键字
  59 + Arrays.asList(
  60 + "java.lang.System",
  61 + "java.io",
  62 + "java.net",
  63 + "java.nio",
  64 + "java.security",
  65 + "java.rmi",
  66 + "java.lang.reflect",
  67 + "java.sql",
  68 + "javax.sql",
  69 + "javax.naming",
  70 + "javax.script",
  71 + "javax.tools",
  72 + "org.omg",
  73 + "org.graalvm.polyglot",
  74 + "sun.",
  75 + "javafx.",
  76 + "Packages.",
  77 + "com.sun.",
  78 + "com.oracle.").forEach(unsafeKeywords::add);
  79 +
  80 + // GraalJS 特有的不安全关键字
  81 + Arrays.asList(
  82 + "Polyglot.import",
  83 + "Polyglot.eval",
  84 + "Java.type",
  85 + "allowHostAccess",
  86 + "allowNativeAccess",
  87 + "allowIO",
  88 + "allowHostClassLoading",
  89 + "allowAllAccess",
  90 + "allowExperimentalOptions",
  91 + "Context.Builder",
  92 + "Context.create",
  93 + "Context.getCurrent",
  94 + "Context.newBuilder",
  95 + "__proto__",
  96 + "__defineGetter__",
  97 + "__defineSetter__",
  98 + "__lookupGetter__",
  99 + "__lookupSetter__",
  100 + "__noSuchMethod__",
  101 + "constructor.constructor",
  102 + "Object.constructor").forEach(unsafeKeywords::add);
  103 +
  104 + // 可能导致高资源消耗的关键字
  105 + Arrays.asList(
  106 + "while(true)",
  107 + "for(;;)",
  108 + "do{",
  109 + "BigInt",
  110 + "Promise.all",
  111 + "setTimeout",
  112 + "setInterval",
  113 + "new Array(",
  114 + "Array(",
  115 + "new ArrayBuffer(",
  116 + ".repeat(",
  117 + ".forEach(",
  118 + ".map(",
  119 + ".reduce(").forEach(highResourceKeywords::add);
  120 +
  121 + // 初始化不安全的模式
  122 + // 系统访问和进程执行
  123 + unsafePatterns.add(Pattern.compile("java\\.lang\\.Runtime"));
  124 + unsafePatterns.add(Pattern.compile("java\\.lang\\.ProcessBuilder"));
  125 + unsafePatterns.add(Pattern.compile("java\\.lang\\.reflect"));
  126 +
  127 + // 特殊对象和操作
  128 + unsafePatterns.add(Pattern.compile("Packages"));
  129 + unsafePatterns.add(Pattern.compile("JavaImporter"));
  130 + unsafePatterns.add(Pattern.compile("load\\s*\\("));
  131 + unsafePatterns.add(Pattern.compile("loadWithNewGlobal\\s*\\("));
  132 + unsafePatterns.add(Pattern.compile("exit\\s*\\("));
  133 + unsafePatterns.add(Pattern.compile("quit\\s*\\("));
  134 + unsafePatterns.add(Pattern.compile("eval\\s*\\("));
  135 +
  136 + // GraalJS 特有的不安全模式
  137 + unsafePatterns.add(Pattern.compile("Polyglot\\."));
  138 + unsafePatterns.add(Pattern.compile("Java\\.type\\s*\\("));
  139 + unsafePatterns.add(Pattern.compile("Context\\."));
  140 + unsafePatterns.add(Pattern.compile("Engine\\."));
  141 +
  142 + // 原型污染检测
  143 + unsafePatterns.add(Pattern.compile("(?:Object|Array|String|Number|Boolean|Function|RegExp|Date)\\.prototype"));
  144 + unsafePatterns.add(Pattern.compile("\\['constructor'\\]"));
  145 + unsafePatterns.add(Pattern.compile("\\[\"constructor\"\\]"));
  146 + unsafePatterns.add(Pattern.compile("\\['__proto__'\\]"));
  147 + unsafePatterns.add(Pattern.compile("\\[\"__proto__\"\\]"));
  148 +
  149 + // 检测可能导致无限递归或循环的模式
  150 + recursionPatterns.add(Pattern.compile("for\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*for\\s*\\(")); // 嵌套循环
  151 + recursionPatterns.add(Pattern.compile("while\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*while\\s*\\(")); // 嵌套 while
  152 + recursionPatterns.add(Pattern.compile("function\\s+[a-zA-Z0-9_$]+\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*\\1\\s*\\(")); // 递归函数调用
  153 + }
  154 +
  155 + @Override
  156 + public boolean validate(String script) {
  157 + if (StrUtil.isBlank(script)) {
  158 + return true;
  159 + }
  160 +
  161 + // 检查脚本长度
  162 + if (script.length() > MAX_SCRIPT_LENGTH) {
  163 + log.warn("脚本长度超过限制: {} > {}", script.length(), MAX_SCRIPT_LENGTH);
  164 + return false;
  165 + }
  166 +
  167 + // 使用超时机制进行验证
  168 + final boolean[] result = {true};
  169 + Thread validationThread = new Thread(() -> {
  170 + // 检查不安全的关键字
  171 + if (containsUnsafeKeywords(script)) {
  172 + result[0] = false;
  173 + return;
  174 + }
  175 +
  176 + // 检查不安全的模式
  177 + if (matchesUnsafePatterns(script)) {
  178 + result[0] = false;
  179 + return;
  180 + }
  181 +
  182 + // 检查可能导致高资源消耗的构造
  183 + if (containsHighResourcePatterns(script)) {
  184 + log.warn("脚本包含可能导致高资源消耗的构造,需要注意");
  185 + // 不直接拒绝,而是记录警告
  186 + }
  187 +
  188 + // 分析脚本复杂度
  189 + analyzeScriptComplexity(script);
  190 + });
  191 +
  192 + validationThread.start();
  193 + try {
  194 + validationThread.join(VALIDATION_TIMEOUT);
  195 + if (validationThread.isAlive()) {
  196 + validationThread.interrupt();
  197 + log.warn("脚本安全验证超时");
  198 + return false;
  199 + }
  200 + } catch (InterruptedException e) {
  201 + log.warn("脚本安全验证被中断");
  202 + return false;
  203 + }
  204 +
  205 + return result[0];
  206 + }
  207 +
  208 + @Override
  209 + public String getType() {
  210 + return TYPE;
  211 + }
  212 +
  213 + /**
  214 + * 检查脚本是否包含不安全的关键字
  215 + *
  216 + * @param script 脚本内容
  217 + * @return 是否包含不安全的关键字
  218 + */
  219 + private boolean containsUnsafeKeywords(String script) {
  220 + if (CollUtil.isEmpty(unsafeKeywords)) {
  221 + return false;
  222 + }
  223 +
  224 + for (String keyword : unsafeKeywords) {
  225 + if (script.contains(keyword)) {
  226 + log.warn("脚本包含不安全的关键字: {}", keyword);
  227 + return true;
  228 + }
  229 + }
  230 + return false;
  231 + }
  232 +
  233 + /**
  234 + * 检查脚本是否匹配不安全的模式
  235 + *
  236 + * @param script 脚本内容
  237 + * @return 是否匹配不安全的模式
  238 + */
  239 + private boolean matchesUnsafePatterns(String script) {
  240 + if (CollUtil.isEmpty(unsafePatterns)) {
  241 + return false;
  242 + }
  243 +
  244 + for (Pattern pattern : unsafePatterns) {
  245 + Matcher matcher = pattern.matcher(script);
  246 + if (matcher.find()) {
  247 + log.warn("脚本匹配到不安全的模式: {}", pattern.pattern());
  248 + return true;
  249 + }
  250 + }
  251 + return false;
  252 + }
  253 +
  254 + /**
  255 + * 检查脚本是否包含可能导致高资源消耗的模式
  256 + *
  257 + * @param script 脚本内容
  258 + * @return 是否包含高资源消耗模式
  259 + */
  260 + private boolean containsHighResourcePatterns(String script) {
  261 + if (CollUtil.isEmpty(highResourceKeywords)) {
  262 + return false;
  263 + }
  264 +
  265 + boolean result = false;
  266 + for (String pattern : highResourceKeywords) {
  267 + if (script.contains(pattern)) {
  268 + log.warn("脚本包含高资源消耗模式: {}", pattern);
  269 + result = true;
  270 + }
  271 + }
  272 +
  273 + // 还要检查递归或嵌套循环模式
  274 + for (Pattern pattern : recursionPatterns) {
  275 + Matcher matcher = pattern.matcher(script);
  276 + if (matcher.find()) {
  277 + log.warn("脚本包含嵌套循环或递归调用: {}", pattern.pattern());
  278 + result = true;
  279 + }
  280 + }
  281 +
  282 + return result;
  283 + }
  284 +
  285 + /**
  286 + * 分析脚本复杂度
  287 + *
  288 + * @param script 脚本内容
  289 + */
  290 + private void analyzeScriptComplexity(String script) {
  291 + // 计算循环和条件语句的数量
  292 + int forCount = countOccurrences(script, "for(");
  293 + forCount += countOccurrences(script, "for (");
  294 +
  295 + int whileCount = countOccurrences(script, "while(");
  296 + whileCount += countOccurrences(script, "while (");
  297 +
  298 + int doWhileCount = countOccurrences(script, "do{");
  299 + doWhileCount += countOccurrences(script, "do {");
  300 +
  301 + int funcCount = countOccurrences(script, "function");
  302 +
  303 + // 记录复杂度评估
  304 + if (forCount + whileCount + doWhileCount > 10) {
  305 + log.warn("脚本循环结构过多: for={}, while={}, do-while={}", forCount, whileCount, doWhileCount);
  306 + }
  307 +
  308 + if (funcCount > 20) {
  309 + log.warn("脚本函数定义过多: {}", funcCount);
  310 + }
  311 + }
  312 +
  313 + /**
  314 + * 计算字符串出现次数
  315 + *
  316 + * @param source 源字符串
  317 + * @param substring 子字符串
  318 + * @return 出现次数
  319 + */
  320 + private int countOccurrences(String source, String substring) {
  321 + int count = 0;
  322 + int index = 0;
  323 + while ((index = source.indexOf(substring, index)) != -1) {
  324 + count++;
  325 + index += substring.length();
  326 + }
  327 + return count;
  328 + }
  329 +}
0 \ No newline at end of file 330 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/ScriptSandbox.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.sandbox;
  2 +
  3 +/**
  4 + * 脚本沙箱接口,提供脚本安全性验证
  5 + */
  6 +public interface ScriptSandbox {
  7 +
  8 + /**
  9 + * 验证脚本内容是否安全
  10 + *
  11 + * @param script 脚本内容
  12 + * @return 脚本是否安全
  13 + */
  14 + boolean validate(String script);
  15 +
  16 + /**
  17 + * 获取沙箱类型
  18 + *
  19 + * @return 沙箱类型
  20 + */
  21 + String getType();
  22 +}
0 \ No newline at end of file 23 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.service;
  2 +
  3 +import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
  4 +
  5 +import java.util.Map;
  6 +
  7 +/**
  8 + * 脚本服务接口,定义脚本执行的核心功能
  9 + */
  10 +public interface ScriptService {
  11 +
  12 + /**
  13 + * 执行脚本
  14 + *
  15 + * @param scriptType 脚本类型(如 js、groovy 等)
  16 + * @param script 脚本内容
  17 + * @param context 脚本上下文
  18 + * @return 脚本执行结果
  19 + */
  20 + Object executeScript(String scriptType, String script, ScriptContext context);
  21 +
  22 + /**
  23 + * 执行脚本
  24 + *
  25 + * @param scriptType 脚本类型(如 js、groovy 等)
  26 + * @param script 脚本内容
  27 + * @param params 脚本参数
  28 + * @return 脚本执行结果
  29 + */
  30 + Object executeScript(String scriptType, String script, Map<String, Object> params);
  31 +
  32 + /**
  33 + * 执行 JavaScript 脚本
  34 + *
  35 + * @param script 脚本内容
  36 + * @param context 脚本上下文
  37 + * @return 脚本执行结果
  38 + */
  39 + Object executeJavaScript(String script, ScriptContext context);
  40 +
  41 + /**
  42 + * 执行 JavaScript 脚本
  43 + *
  44 + * @param script 脚本内容
  45 + * @param params 脚本参数
  46 + * @return 脚本执行结果
  47 + */
  48 + Object executeJavaScript(String script, Map<String, Object> params);
  49 +
  50 + /**
  51 + * 验证脚本内容是否安全
  52 + *
  53 + * @param scriptType 脚本类型
  54 + * @param script 脚本内容
  55 + * @return 脚本是否安全
  56 + */
  57 + boolean validateScript(String scriptType, String script);
  58 +}
0 \ No newline at end of file 59 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.service;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext;
  6 +import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
  7 +import cn.iocoder.yudao.module.iot.script.engine.JsScriptEngine;
  8 +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngine;
  9 +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory;
  10 +import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox;
  11 +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.springframework.beans.factory.annotation.Autowired;
  14 +import org.springframework.stereotype.Service;
  15 +
  16 +import java.util.Map;
  17 +
  18 +/**
  19 + * 脚本服务实现类
  20 + */
  21 +@Slf4j
  22 +@Service
  23 +public class ScriptServiceImpl implements ScriptService {
  24 +
  25 + @Autowired
  26 + private ScriptEngineFactory engineFactory;
  27 +
  28 + @Override
  29 + public Object executeScript(String scriptType, String script, ScriptContext context) {
  30 + if (StrUtil.isBlank(scriptType) || StrUtil.isBlank(script)) {
  31 + return null;
  32 + }
  33 +
  34 + ScriptEngine engine = engineFactory.getEngine(scriptType);
  35 + if (engine == null) {
  36 + log.error("找不到脚本引擎: {}", scriptType);
  37 + throw new RuntimeException("不支持的脚本类型: " + scriptType);
  38 + }
  39 +
  40 + try {
  41 + return engine.execute(script, context);
  42 + } catch (Exception e) {
  43 + log.error("执行脚本失败: {}", e.getMessage(), e);
  44 + throw new RuntimeException("执行脚本失败: " + e.getMessage(), e);
  45 + }
  46 + }
  47 +
  48 + @Override
  49 + public Object executeScript(String scriptType, String script, Map<String, Object> params) {
  50 + ScriptContext context = createContext(params);
  51 + return executeScript(scriptType, script, context);
  52 + }
  53 +
  54 + @Override
  55 + public Object executeJavaScript(String script, ScriptContext context) {
  56 + return executeScript(JsScriptEngine.TYPE, script, context);
  57 + }
  58 +
  59 + @Override
  60 + public Object executeJavaScript(String script, Map<String, Object> params) {
  61 + return executeScript(JsScriptEngine.TYPE, script, params);
  62 + }
  63 +
  64 + @Override
  65 + public boolean validateScript(String scriptType, String script) {
  66 + if (StrUtil.isBlank(scriptType) || StrUtil.isBlank(script)) {
  67 + return true;
  68 + }
  69 +
  70 + ScriptSandbox sandbox = getSandbox(scriptType);
  71 + if (sandbox == null) {
  72 + log.warn("找不到对应的脚本沙箱: {}", scriptType);
  73 + return false;
  74 + }
  75 +
  76 + try {
  77 + return sandbox.validate(script);
  78 + } catch (Exception e) {
  79 + log.error("验证脚本安全性失败: {}", e.getMessage(), e);
  80 + return false;
  81 + }
  82 + }
  83 +
  84 + /**
  85 + * 根据脚本类型获取对应的沙箱实现
  86 + *
  87 + * @param scriptType 脚本类型
  88 + * @return 沙箱实现
  89 + */
  90 + private ScriptSandbox getSandbox(String scriptType) {
  91 + if (JsScriptEngine.TYPE.equals(scriptType)) {
  92 + return new JsSandbox();
  93 + }
  94 + return null;
  95 + }
  96 +
  97 + /**
  98 + * 根据参数创建脚本上下文
  99 + *
  100 + * @param params 参数
  101 + * @return 脚本上下文
  102 + */
  103 + private ScriptContext createContext(Map<String, Object> params) {
  104 + ScriptContext context = new DefaultScriptContext();
  105 + if (MapUtil.isNotEmpty(params)) {
  106 + params.forEach(context::setParameter);
  107 + }
  108 + return context;
  109 + }
  110 +}
0 \ No newline at end of file 111 \ No newline at end of file
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/util/ScriptUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.script.util;
  2 +
  3 +import cn.hutool.core.convert.Convert;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.hutool.json.JSONUtil;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +import java.util.Map;
  10 +
  11 +/**
  12 + * 脚本工具类,提供给脚本执行环境使用的工具方法
  13 + */
  14 +@Slf4j
  15 +public class ScriptUtils {
  16 +
  17 + /**
  18 + * 单例实例
  19 + */
  20 + private static final ScriptUtils INSTANCE = new ScriptUtils();
  21 +
  22 + /**
  23 + * 获取单例实例
  24 + *
  25 + * @return 工具类实例
  26 + */
  27 + public static ScriptUtils getInstance() {
  28 + return INSTANCE;
  29 + }
  30 +
  31 + private ScriptUtils() {
  32 + // 私有构造函数
  33 + }
  34 +
  35 + /**
  36 + * 字符串是否为空
  37 + *
  38 + * @param str 字符串
  39 + * @return 是否为空
  40 + */
  41 + public boolean isEmpty(String str) {
  42 + return StrUtil.isEmpty(str);
  43 + }
  44 +
  45 + /**
  46 + * 字符串是否不为空
  47 + *
  48 + * @param str 字符串
  49 + * @return 是否不为空
  50 + */
  51 + public boolean isNotEmpty(String str) {
  52 + return StrUtil.isNotEmpty(str);
  53 + }
  54 +
  55 + /**
  56 + * 将对象转为 JSON 字符串
  57 + *
  58 + * @param obj 对象
  59 + * @return JSON 字符串
  60 + */
  61 + public String toJson(Object obj) {
  62 + return JSONUtil.toJsonStr(obj);
  63 + }
  64 +
  65 + /**
  66 + * 将 JSON 字符串转为 Map
  67 + *
  68 + * @param json JSON 字符串
  69 + * @return Map 对象
  70 + */
  71 + public Map<String, Object> parseJson(String json) {
  72 + if (StrUtil.isEmpty(json)) {
  73 + return MapUtil.newHashMap();
  74 + }
  75 + try {
  76 + return JSONUtil.toBean(json, Map.class);
  77 + } catch (Exception e) {
  78 + log.error("JSON 解析失败: {}", json, e);
  79 + return MapUtil.newHashMap();
  80 + }
  81 + }
  82 +
  83 + /**
  84 + * 类型转换
  85 + *
  86 + * @param value 值
  87 + * @param type 目标类型
  88 + * @param <T> 泛型
  89 + * @return 转换后的值
  90 + */
  91 + public <T> T convert(Object value, Class<T> type) {
  92 + return Convert.convert(type, value);
  93 + }
  94 +
  95 + /**
  96 + * 从 Map 中获取值
  97 + *
  98 + * @param map Map 对象
  99 + * @param key 键
  100 + * @return 值
  101 + */
  102 + public Object get(Map<String, Object> map, String key) {
  103 + return MapUtil.get(map, key, Object.class);
  104 + }
  105 +
  106 + /**
  107 + * 从 Map 中获取字符串
  108 + *
  109 + * @param map Map 对象
  110 + * @param key 键
  111 + * @return 字符串值
  112 + */
  113 + public String getString(Map<String, Object> map, String key) {
  114 + return MapUtil.getStr(map, key);
  115 + }
  116 +
  117 + /**
  118 + * 从 Map 中获取整数
  119 + *
  120 + * @param map Map 对象
  121 + * @param key 键
  122 + * @return 整数值
  123 + */
  124 + public Integer getInt(Map<String, Object> map, String key) {
  125 + return MapUtil.getInt(map, key);
  126 + }
  127 +
  128 + /**
  129 + * 从 Map 中获取布尔值
  130 + *
  131 + * @param map Map 对象
  132 + * @param key 键
  133 + * @return 布尔值
  134 + */
  135 + public Boolean getBool(Map<String, Object> map, String key) {
  136 + return MapUtil.getBool(map, key);
  137 + }
  138 +
  139 + /**
  140 + * 从 Map 中获取双精度浮点数
  141 + *
  142 + * @param map Map 对象
  143 + * @param key 键
  144 + * @return 双精度浮点数值
  145 + */
  146 + public Double getDouble(Map<String, Object> map, String key) {
  147 + return MapUtil.getDouble(map, key);
  148 + }
  149 +
  150 + /**
  151 + * 获取当前时间戳(毫秒)
  152 + *
  153 + * @return 时间戳
  154 + */
  155 + public long currentTimeMillis() {
  156 + return System.currentTimeMillis();
  157 + }
  158 +}
0 \ No newline at end of file 159 \ No newline at end of file