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