001/** 002 * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.gnu.org/licenses/lgpl-3.0.txt 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package dev.tinyflow.core.util; 017 018import dev.tinyflow.core.chain.Chain; 019import dev.tinyflow.core.util.graalvm.JsInteropUtils; 020import org.graalvm.polyglot.Context; 021import org.graalvm.polyglot.HostAccess; 022import org.graalvm.polyglot.Value; 023 024import java.util.HashMap; 025import java.util.Map; 026 027public class JsConditionUtil { 028 029 // 使用 Context.Builder 构建上下文,线程安全 030 private static final Context.Builder CONTEXT_BUILDER = Context.newBuilder("js") 031 .option("engine.WarnInterpreterOnly", "false") 032 .allowHostAccess(HostAccess.ALL) // 允许访问 Java 对象的方法和字段 033 .allowHostClassLookup(className -> false) // 禁止动态加载任意 Java 类 034 .option("js.ecmascript-version", "2021"); // 使用较新的 ECMAScript 版本 035 036 /** 037 * 执行 JavaScript 表达式并返回 boolean 结果 038 * 039 * @param code JS 表达式(应返回布尔或可转换为布尔的值) 040 * @param chain Chain 上下文对象 041 * @param initMap 初始变量映射 042 * @return true 表示满足条件,继续执行;false 表示跳过 043 */ 044 public static boolean eval(String code, Chain chain, Map<String, Object> initMap) { 045 try (Context context = CONTEXT_BUILDER.build()) { 046 Map<String, Object> _result = new HashMap<>(); 047 Value bindings = context.getBindings("js"); 048 049 // 合并上下文变量 050 Map<String, Object> contextVariables = collectContextVariables(chain, initMap); 051 contextVariables.forEach((key, value) -> { 052 bindings.putMember(key, JsInteropUtils.wrapJavaValueForJS(context, value)); 053 }); 054 055 bindings.putMember("_result", _result); 056 String resultCode = "_result.value = " + code; 057 058 context.eval("js", resultCode); 059 Object value = _result.get("value"); 060 return toBoolean(value); 061 } catch (Exception e) { 062 throw new JsCodeException("JavaScript exec failed: " + e.getMessage() 063 + "\ncode:" 064 + "\n```\n" 065 + "\n" + code 066 + "\n```\n" 067 , e); 068 } 069 } 070 071 072 public static long evalLong(String code, Chain chain, Map<String, Object> initMap) { 073 try (Context context = CONTEXT_BUILDER.build()) { 074 Map<String, Object> _result = new HashMap<>(); 075 Value bindings = context.getBindings("js"); 076 077 // 合并上下文变量 078 Map<String, Object> contextVariables = collectContextVariables(chain, initMap); 079 contextVariables.forEach((key, value) -> { 080 bindings.putMember(key, JsInteropUtils.wrapJavaValueForJS(context, value)); 081 }); 082 083 bindings.putMember("_result", _result); 084 String resultCode = "_result.value = " + code; 085 086 context.eval("js", resultCode); 087 Object value = _result.get("value"); 088 return toLong(value); 089 } catch (Exception e) { 090 throw new JsCodeException("JavaScript exec failed: " + e.getMessage() 091 + "\ncode:" 092 + "\n```\n" 093 + "\n" + code 094 + "\n```\n" 095 , e); 096 } 097 } 098 099 100 /** 101 * 将任意对象安全转换为 long 类型 102 */ 103 private static long toLong(Object value) { 104 if (value == null) { 105 return 0L; 106 } 107 108 if (value instanceof Number) { 109 return ((Number) value).longValue(); 110 } 111 112 if (value instanceof String) { 113 String str = ((String) value).trim(); 114 if (str.isEmpty()) { 115 return 0L; 116 } 117 try { 118 // 支持整数和浮点字符串(如 "123", "45.67") 119 return Double.valueOf(str).longValue(); 120 } catch (NumberFormatException e) { 121 throw new RuntimeException("无法将字符串 \"" + str + "\" 转换为 long", e); 122 } 123 } 124 125 if (value instanceof Value) { 126 Value v = (Value) value; 127 if (v.isNumber()) { 128 return v.asLong(); // GraalVM 的 asLong() 会自动处理 double/integer 129 } else if (v.isString()) { 130 return toLong(v.asString()); 131 } else if (v.isNull()) { 132 return 0L; 133 } else { 134 throw new RuntimeException("无法将 JS 值 " + v + " 转换为 long"); 135 } 136 } 137 138 // 兜底:尝试 toString 后解析 139 try { 140 String str = value.toString().trim(); 141 return str.isEmpty() ? 0L : Double.valueOf(str).longValue(); 142 } catch (Exception e) { 143 throw new RuntimeException("无法将对象 " + value + " 转换为 long", e); 144 } 145 } 146 147 /** 148 * 收集上下文中的变量 149 */ 150 private static Map<String, Object> collectContextVariables(Chain chain, Map<String, Object> initMap) { 151 Map<String, Object> variables = new HashMap<>(); 152 153 // 添加 Chain Memory 中的变量(去掉前缀) 154 chain.getState().getMemory().forEach((key, value) -> { 155 int dotIndex = key.indexOf("."); 156 String varName = (dotIndex >= 0) ? key.substring(dotIndex + 1) : key; 157 variables.put(varName, value); 158 }); 159 160 // 添加 _chain 和 initMap 变量 161 if (initMap != null) { 162 variables.putAll(initMap); 163// initMap.forEach((s, o) -> variables.put(s, o != null ? o : "")); 164 } 165 166 return variables; 167 } 168 169 /** 170 * 将任意对象转换为布尔值 171 */ 172 private static boolean toBoolean(Object value) { 173 if (value == null) { 174 return false; 175 } 176 if (value instanceof Boolean) { 177 return (Boolean) value; 178 } 179 if (value instanceof Number) { 180 return ((Number) value).intValue() != 0; 181 } 182 if (value instanceof String) { 183 String str = ((String) value).trim().toLowerCase(); 184 return !str.isEmpty() && !"0".equals(str) && !"false".equals(str); 185 } 186 if (value instanceof Value) { 187 Value v = (Value) value; 188 if (v.isBoolean()) { 189 return v.asBoolean(); 190 } else if (v.isNumber()) { 191 return v.asDouble() != 0; 192 } else if (v.isString()) { 193 String str = v.asString().trim().toLowerCase(); 194 return !str.isEmpty() && !"0".equals(str) && !"false".equals(str); 195 } else { 196 return !v.isNull(); 197 } 198 } 199 return true; 200 } 201 202}