ScriptCompiler.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Reflection;
  7. using TeamAAS.FlowEditor.Execution; // ResultRegistry
  8. using TeamAAS.FlowEditor.Plugins; // BasePlugin
  9. using Plugins.Script.Models;
  10. namespace Plugins.Script.Core
  11. {
  12. /// <summary>
  13. /// 完整 C# 代码的 headless 编译 / 缓存 / 入口定位 / 实例化注入(无 UI 依赖,可在工作线程执行)。
  14. /// 用户代码是一个完整插件类(约定入口:继承 <c>BasePlugin&lt;ScriptToolModel&gt;</c> 的类,含 PluginRun/DeclareOutputs)。
  15. /// 编译用 CodeForge.CodeForgeEngine.CompileCode(整段代码)(切勿用 Compile/CompileAndRun,那编译的是编辑器文本),
  16. /// 加载后定位入口类型并 Activator 实例化,注入 Model/Context/Registry,使其行为与普通插件一致。
  17. /// 按代码全文缓存入口 Type(编译一次、多次运行,避免每次 Assembly.Load 泄漏);引用集变化时清空缓存重编。
  18. /// </summary>
  19. internal static class ScriptCompiler
  20. {
  21. /// <summary>约定的入口类名(优先匹配;找不到则取任一 BasePlugin&lt;ScriptToolModel&gt; 子类)。</summary>
  22. public const string EntryClassName = "MyScript";
  23. private static readonly ConcurrentDictionary<string, Type> _typeCache = new ConcurrentDictionary<string, Type>();
  24. private static readonly ConcurrentDictionary<string, List<string>> _errorCache = new ConcurrentDictionary<string, List<string>>();
  25. private static readonly HashSet<string> _addedRefPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  26. private static CodeForge.CodeForgeEngine _engine;
  27. private static readonly object _engineGate = new object();
  28. /// <summary>仅查缓存(不触发编译):命中返回入口 Type。</summary>
  29. public static bool TryGetEntry(string code, out Type entry)
  30. {
  31. entry = null;
  32. if (string.IsNullOrWhiteSpace(code)) return false;
  33. return _typeCache.TryGetValue(code, out entry);
  34. }
  35. /// <summary>用缓存好的入口 Type 快速创建脚本实例(绝不编译)。未命中缓存返回 null。</summary>
  36. public static BasePlugin<ScriptToolModel> TryCreateCached(string code, ScriptToolModel model,
  37. TeamAAS.Global.IPluginContext context, ResultRegistry registry)
  38. {
  39. Type entry;
  40. if (!TryGetEntry(code, out entry)) return null;
  41. try
  42. {
  43. var inst = (BasePlugin<ScriptToolModel>)Activator.CreateInstance(entry);
  44. inst.GetModel = model;
  45. inst.Context = context;
  46. inst.Registry = registry;
  47. return inst;
  48. }
  49. catch { return null; }
  50. }
  51. /// <summary>
  52. /// 后台预热引擎(首次创建 CodeForge 引擎 + 全量基础引用 + 用户引用同步)。
  53. /// 这一步在 UI 线程上做会卡顿数秒——务必只在后台线程调用(节点创建时 Task.Run 触发)。
  54. /// </summary>
  55. public static void WarmupEngine()
  56. {
  57. lock (_engineGate)
  58. {
  59. var engine = GetEngine();
  60. SyncUserReferences(engine);
  61. }
  62. }
  63. /// <summary>
  64. /// 编译整段代码并定位入口类型(命中缓存则跳过编译)。成功返回入口 Type;失败返回 null 并给出 errors。
  65. /// </summary>
  66. public static Type CompileEntry(string code, out List<string> errors, out List<string> warnings)
  67. {
  68. errors = new List<string>();
  69. warnings = new List<string>();
  70. if (string.IsNullOrWhiteSpace(code)) { errors.Add("脚本代码为空"); return null; }
  71. Type cached;
  72. if (_typeCache.TryGetValue(code, out cached)) return cached;
  73. List<string> cachedErr;
  74. if (_errorCache.TryGetValue(code, out cachedErr)) { errors = cachedErr; return null; }
  75. CodeForge.CodeCompileResult compile;
  76. lock (_engineGate)
  77. {
  78. var engine = GetEngine();
  79. if (SyncUserReferences(engine)) { _typeCache.Clear(); _errorCache.Clear(); }
  80. compile = engine.CompileCode(code);
  81. }
  82. if (compile == null || !compile.Success || compile.AssemblyBytes == null)
  83. {
  84. errors = (compile != null && compile.Errors != null)
  85. ? new List<string>(compile.Errors) : new List<string> { "编译失败(无详情)" };
  86. _errorCache[code] = errors;
  87. return null;
  88. }
  89. if (compile.Warnings != null) warnings = new List<string>(compile.Warnings);
  90. var asm = Assembly.Load(compile.AssemblyBytes);
  91. var entry = FindEntryType(asm);
  92. if (entry == null)
  93. {
  94. errors = new List<string>
  95. {
  96. "编译成功但未找到入口类:请保留 public class MyScript : BasePlugin<ScriptToolModel> { public override NodeRunStatus PluginRun(...) {...} }"
  97. };
  98. _errorCache[code] = errors;
  99. return null;
  100. }
  101. _typeCache[code] = entry;
  102. return entry;
  103. }
  104. /// <summary>
  105. /// 编译(命中缓存则跳过)并创建已注入 Model/Context/Registry 的脚本插件实例;失败返回 null 并给出 errors。
  106. /// 返回的实例即普通 BasePlugin,可直接调用其 PluginRun / DeclareOutputs / InitPlugin。
  107. /// </summary>
  108. public static BasePlugin<ScriptToolModel> CreateScript(
  109. string code, ScriptToolModel model,
  110. TeamAAS.Global.IPluginContext context, ResultRegistry registry,
  111. out List<string> errors, out List<string> warnings)
  112. {
  113. var type = CompileEntry(code, out errors, out warnings);
  114. if (type == null) return null;
  115. try
  116. {
  117. var inst = (BasePlugin<ScriptToolModel>)Activator.CreateInstance(type);
  118. inst.GetModel = model;
  119. inst.Context = context;
  120. inst.Registry = registry;
  121. return inst;
  122. }
  123. catch (Exception ex)
  124. {
  125. errors = new List<string> { "创建脚本实例失败:" + ex.Message };
  126. return null;
  127. }
  128. }
  129. /// <summary>定位入口类型:继承 BasePlugin&lt;ScriptToolModel&gt; 的非抽象类;优先名为 MyScript 的。</summary>
  130. private static Type FindEntryType(Assembly asm)
  131. {
  132. Type[] types;
  133. try { types = asm.GetTypes(); }
  134. catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t != null).ToArray(); }
  135. var baseType = typeof(BasePlugin<ScriptToolModel>);
  136. Type first = null;
  137. foreach (var t in types)
  138. {
  139. if (t == null || t.IsAbstract || t == baseType) continue;
  140. if (!baseType.IsAssignableFrom(t)) continue;
  141. if (t.Name == EntryClassName) return t;
  142. if (first == null) first = t;
  143. }
  144. return first;
  145. }
  146. private static CodeForge.CodeForgeEngine GetEngine()
  147. {
  148. if (_engine == null)
  149. {
  150. _engine = new CodeForge.CodeForgeEngine();
  151. AddBaseReferences(_engine);
  152. }
  153. return _engine;
  154. }
  155. /// <summary>基础引用:本插件(ScriptToolModel)+ 所有已加载的 TeamAAS.* 程序集(跟普通插件一样)。</summary>
  156. private static void AddBaseReferences(CodeForge.CodeForgeEngine engine)
  157. {
  158. AddRef(engine, typeof(ScriptToolModel).Assembly);
  159. foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
  160. {
  161. try
  162. {
  163. if (asm.IsDynamic) continue;
  164. var name = asm.GetName().Name ?? "";
  165. if (!name.StartsWith("TeamAAS", StringComparison.OrdinalIgnoreCase)) continue;
  166. AddRef(engine, asm);
  167. }
  168. catch { /* 忽略单个引用失败 */ }
  169. }
  170. }
  171. /// <summary>
  172. /// 用户 DLL 归档目录候选(都基于 exe 目录):根目录 References\ 是 CodeForge.InstallAndReferenceDll
  173. /// 的归档约定处;Runtime\References\ 是历史版本 PostBuild 把 References 整体搬入 Runtime 后的遗留位置。
  174. /// 两处都增量扫描,保证旧文件也能被编译环境引用。
  175. /// </summary>
  176. private static IEnumerable<string> UserReferenceDirs()
  177. {
  178. yield return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "References");
  179. yield return Path.Combine(TeamAAS.PathHelper.RuntimeDir, "References");
  180. }
  181. /// <summary>扫描用户 DLL 归档目录,增量加入引擎;有新增返回 true。</summary>
  182. private static bool SyncUserReferences(CodeForge.CodeForgeEngine engine)
  183. {
  184. bool changed = false;
  185. foreach (var dir in UserReferenceDirs())
  186. {
  187. try
  188. {
  189. if (!Directory.Exists(dir)) continue;
  190. foreach (var dll in Directory.GetFiles(dir, "*.dll"))
  191. {
  192. if (_addedRefPaths.Contains(dll)) continue;
  193. try { engine.AddReferenceFromFile(dll); _addedRefPaths.Add(dll); changed = true; }
  194. catch { /* 忽略坏 DLL */ }
  195. }
  196. }
  197. catch { /* 忽略扫描失败 */ }
  198. }
  199. return changed;
  200. }
  201. private static void AddRef(CodeForge.CodeForgeEngine engine, Assembly asm)
  202. {
  203. try
  204. {
  205. if (asm == null || asm.IsDynamic) return;
  206. var loc = asm.Location;
  207. if (string.IsNullOrEmpty(loc) || !File.Exists(loc)) return;
  208. if (_addedRefPaths.Contains(loc)) return;
  209. engine.AddReferenceFromFile(loc);
  210. _addedRefPaths.Add(loc);
  211. }
  212. catch { /* 忽略单个引用失败 */ }
  213. }
  214. }
  215. }