using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using TeamAAS.FlowEditor.Execution; // ResultRegistry using TeamAAS.FlowEditor.Plugins; // BasePlugin using Plugins.Script.Models; namespace Plugins.Script.Core { /// /// 完整 C# 代码的 headless 编译 / 缓存 / 入口定位 / 实例化注入(无 UI 依赖,可在工作线程执行)。 /// 用户代码是一个完整插件类(约定入口:继承 BasePlugin<ScriptToolModel> 的类,含 PluginRun/DeclareOutputs)。 /// 编译用 CodeForge.CodeForgeEngine.CompileCode(整段代码)(切勿用 Compile/CompileAndRun,那编译的是编辑器文本), /// 加载后定位入口类型并 Activator 实例化,注入 Model/Context/Registry,使其行为与普通插件一致。 /// 按代码全文缓存入口 Type(编译一次、多次运行,避免每次 Assembly.Load 泄漏);引用集变化时清空缓存重编。 /// internal static class ScriptCompiler { /// 约定的入口类名(优先匹配;找不到则取任一 BasePlugin<ScriptToolModel> 子类)。 public const string EntryClassName = "MyScript"; private static readonly ConcurrentDictionary _typeCache = new ConcurrentDictionary(); private static readonly ConcurrentDictionary> _errorCache = new ConcurrentDictionary>(); private static readonly HashSet _addedRefPaths = new HashSet(StringComparer.OrdinalIgnoreCase); private static CodeForge.CodeForgeEngine _engine; private static readonly object _engineGate = new object(); /// 仅查缓存(不触发编译):命中返回入口 Type。 public static bool TryGetEntry(string code, out Type entry) { entry = null; if (string.IsNullOrWhiteSpace(code)) return false; return _typeCache.TryGetValue(code, out entry); } /// 用缓存好的入口 Type 快速创建脚本实例(绝不编译)。未命中缓存返回 null。 public static BasePlugin TryCreateCached(string code, ScriptToolModel model, TeamAAS.Global.IPluginContext context, ResultRegistry registry) { Type entry; if (!TryGetEntry(code, out entry)) return null; try { var inst = (BasePlugin)Activator.CreateInstance(entry); inst.GetModel = model; inst.Context = context; inst.Registry = registry; return inst; } catch { return null; } } /// /// 后台预热引擎(首次创建 CodeForge 引擎 + 全量基础引用 + 用户引用同步)。 /// 这一步在 UI 线程上做会卡顿数秒——务必只在后台线程调用(节点创建时 Task.Run 触发)。 /// public static void WarmupEngine() { lock (_engineGate) { var engine = GetEngine(); SyncUserReferences(engine); } } /// /// 编译整段代码并定位入口类型(命中缓存则跳过编译)。成功返回入口 Type;失败返回 null 并给出 errors。 /// public static Type CompileEntry(string code, out List errors, out List warnings) { errors = new List(); warnings = new List(); if (string.IsNullOrWhiteSpace(code)) { errors.Add("脚本代码为空"); return null; } Type cached; if (_typeCache.TryGetValue(code, out cached)) return cached; List cachedErr; if (_errorCache.TryGetValue(code, out cachedErr)) { errors = cachedErr; return null; } CodeForge.CodeCompileResult compile; lock (_engineGate) { var engine = GetEngine(); if (SyncUserReferences(engine)) { _typeCache.Clear(); _errorCache.Clear(); } compile = engine.CompileCode(code); } if (compile == null || !compile.Success || compile.AssemblyBytes == null) { errors = (compile != null && compile.Errors != null) ? new List(compile.Errors) : new List { "编译失败(无详情)" }; _errorCache[code] = errors; return null; } if (compile.Warnings != null) warnings = new List(compile.Warnings); var asm = Assembly.Load(compile.AssemblyBytes); var entry = FindEntryType(asm); if (entry == null) { errors = new List { "编译成功但未找到入口类:请保留 public class MyScript : BasePlugin { public override NodeRunStatus PluginRun(...) {...} }" }; _errorCache[code] = errors; return null; } _typeCache[code] = entry; return entry; } /// /// 编译(命中缓存则跳过)并创建已注入 Model/Context/Registry 的脚本插件实例;失败返回 null 并给出 errors。 /// 返回的实例即普通 BasePlugin,可直接调用其 PluginRun / DeclareOutputs / InitPlugin。 /// public static BasePlugin CreateScript( string code, ScriptToolModel model, TeamAAS.Global.IPluginContext context, ResultRegistry registry, out List errors, out List warnings) { var type = CompileEntry(code, out errors, out warnings); if (type == null) return null; try { var inst = (BasePlugin)Activator.CreateInstance(type); inst.GetModel = model; inst.Context = context; inst.Registry = registry; return inst; } catch (Exception ex) { errors = new List { "创建脚本实例失败:" + ex.Message }; return null; } } /// 定位入口类型:继承 BasePlugin<ScriptToolModel> 的非抽象类;优先名为 MyScript 的。 private static Type FindEntryType(Assembly asm) { Type[] types; try { types = asm.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t != null).ToArray(); } var baseType = typeof(BasePlugin); Type first = null; foreach (var t in types) { if (t == null || t.IsAbstract || t == baseType) continue; if (!baseType.IsAssignableFrom(t)) continue; if (t.Name == EntryClassName) return t; if (first == null) first = t; } return first; } private static CodeForge.CodeForgeEngine GetEngine() { if (_engine == null) { _engine = new CodeForge.CodeForgeEngine(); AddBaseReferences(_engine); } return _engine; } /// 基础引用:本插件(ScriptToolModel)+ 所有已加载的 TeamAAS.* 程序集(跟普通插件一样)。 private static void AddBaseReferences(CodeForge.CodeForgeEngine engine) { AddRef(engine, typeof(ScriptToolModel).Assembly); foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { try { if (asm.IsDynamic) continue; var name = asm.GetName().Name ?? ""; if (!name.StartsWith("TeamAAS", StringComparison.OrdinalIgnoreCase)) continue; AddRef(engine, asm); } catch { /* 忽略单个引用失败 */ } } } /// /// 用户 DLL 归档目录候选(都基于 exe 目录):根目录 References\ 是 CodeForge.InstallAndReferenceDll /// 的归档约定处;Runtime\References\ 是历史版本 PostBuild 把 References 整体搬入 Runtime 后的遗留位置。 /// 两处都增量扫描,保证旧文件也能被编译环境引用。 /// private static IEnumerable UserReferenceDirs() { yield return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "References"); yield return Path.Combine(TeamAAS.PathHelper.RuntimeDir, "References"); } /// 扫描用户 DLL 归档目录,增量加入引擎;有新增返回 true。 private static bool SyncUserReferences(CodeForge.CodeForgeEngine engine) { bool changed = false; foreach (var dir in UserReferenceDirs()) { try { if (!Directory.Exists(dir)) continue; foreach (var dll in Directory.GetFiles(dir, "*.dll")) { if (_addedRefPaths.Contains(dll)) continue; try { engine.AddReferenceFromFile(dll); _addedRefPaths.Add(dll); changed = true; } catch { /* 忽略坏 DLL */ } } } catch { /* 忽略扫描失败 */ } } return changed; } private static void AddRef(CodeForge.CodeForgeEngine engine, Assembly asm) { try { if (asm == null || asm.IsDynamic) return; var loc = asm.Location; if (string.IsNullOrEmpty(loc) || !File.Exists(loc)) return; if (_addedRefPaths.Contains(loc)) return; engine.AddReferenceFromFile(loc); _addedRefPaths.Add(loc); } catch { /* 忽略单个引用失败 */ } } } }