f35b04181b7932bb5a4858c713e9ef5c3cdfd168fb38a8aa27d1ba42a1f577de.source 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.IO;
  5. using System.Threading;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Media;
  9. using CodeForge;
  10. using TeamAAS.FlowEditor.Models;
  11. using TeamAAS.FlowEditor.Plugins;
  12. using Plugins.Script.Models;
  13. namespace Plugins.Script.Views
  14. {
  15. /// <summary>错误/警告列表项(ListBox 绑定)。</summary>
  16. public class DiagnosticItem
  17. {
  18. public string SeverityText { get; set; }
  19. public Brush SeverityBrush { get; set; }
  20. public string Display { get; set; }
  21. public int Line { get; set; }
  22. public int Column { get; set; }
  23. public bool IsWarning { get; set; }
  24. }
  25. /// <summary>
  26. /// C# 脚本节点的自定义编辑视图(承载在 PluginViewDialog,DataContext 由宿主注入为 ScriptToolModel)。
  27. /// 中部是 CodeForge 完整代码编辑器 <see cref="CodeEditorControl"/>:自由写命名空间/多个类/方法,
  28. /// 智能补全、编译诊断波浪线、代码配色、鸟瞰图全部可用;顶部「引用」按钮打开程序集引用管理(添加/移除 DLL)。
  29. /// 用户代码是一个完整插件类(继承 BasePlugin&lt;ScriptToolModel&gt;,PluginRun/DeclareOutputs 预写好)。
  30. /// - IPluginViewSave:确定时把代码回写模型。
  31. /// </summary>
  32. public partial class ScriptToolView : UserControl, IPluginViewSave, IPluginViewTip
  33. {
  34. static ScriptToolView()
  35. {
  36. // XAML 里用到 CodeForge 控件,按 CodeForge 说明书在解析前确保其内嵌依赖解析器已注册(幂等)
  37. try { CodeForgeRuntime.Initialize(); } catch { /* 模块初始化器通常已自动注册 */ }
  38. }
  39. private readonly ObservableCollection<DiagnosticItem> _diagnostics = new ObservableCollection<DiagnosticItem>();
  40. private ScriptToolModel _model;
  41. private ScriptToolModel Model { get { return _model; } }
  42. private bool _initialized;
  43. private bool _refsAdded;
  44. public ScriptToolView()
  45. {
  46. InitializeComponent();
  47. DataContextChanged += (s, e) => _model = e.NewValue as ScriptToolModel;
  48. ErrorListBox.ItemsSource = _diagnostics;
  49. Editor.DiagnosticMessage += (s, msg) => Dispatcher.BeginInvoke(new Action(() => AppendOutput(msg)));
  50. Editor.CompileRequested += (s, e) => Dispatcher.BeginInvoke(new Action(CompileCode));
  51. Loaded += OnLoaded;
  52. Unloaded += OnUnloaded;
  53. }
  54. private void OnLoaded(object sender, RoutedEventArgs e)
  55. {
  56. if (_model != null)
  57. {
  58. TxtTitle.Text = string.IsNullOrWhiteSpace(_model.NodeName) ? "C#脚本" : _model.NodeName;
  59. Editor.Code = string.IsNullOrEmpty(_model.Code) ? ScriptToolModel.DefaultCode : _model.Code;
  60. }
  61. else
  62. {
  63. Editor.Code = ScriptToolModel.DefaultCode;
  64. }
  65. if (!_refsAdded)
  66. {
  67. // 批量添加引用:BeginBatch 作用域内挂起 ReferencesChanged,Dispose 时只触发一次。
  68. // 否则每加一个引用都会重启一次 Roslyn 预热(N 个引用=N 次并发预热),打开脚本编辑器会卡顿数秒。
  69. using (Editor.Engine.References.BeginBatch())
  70. AddPlatformReferences();
  71. _refsAdded = true;
  72. }
  73. try { var _ = Editor.WarmupRoslynAsync(); } catch { }
  74. _initialized = true;
  75. RefreshErrorCount();
  76. AppendOutput("就绪 · 顶部「引用」=程序集引用管理 · 编译(F5)/运行 · 入口=继承 BasePlugin<ScriptToolModel> 的类(默认 MyScript)");
  77. }
  78. private void OnUnloaded(object sender, RoutedEventArgs e)
  79. {
  80. try { Editor.Dispose(); } catch { }
  81. }
  82. /// <summary>给编辑器补全/编译环境加入平台程序集引用(TeamAAS 全家桶 + 本插件 + 用户归档的 References\)。</summary>
  83. private void AddPlatformReferences()
  84. {
  85. try { Editor.AddReference(typeof(ScriptToolModel).Assembly); } catch { }
  86. var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  87. foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
  88. {
  89. try
  90. {
  91. if (asm.IsDynamic) continue;
  92. var name = asm.GetName().Name ?? "";
  93. if (!name.StartsWith("TeamAAS", StringComparison.OrdinalIgnoreCase)) continue;
  94. var loc = asm.Location;
  95. if (string.IsNullOrEmpty(loc) || !File.Exists(loc)) continue;
  96. if (!seen.Add(loc)) continue;
  97. Editor.AddReferenceFromFile(loc);
  98. }
  99. catch { /* 忽略单个引用失败 */ }
  100. }
  101. // 用户 DLL 归档目录:根目录 References\(CodeForge 归档约定处)+ Runtime\References\(历史 PostBuild 搬移遗留位置)
  102. var refDirs = new[]
  103. {
  104. Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "References"),
  105. Path.Combine(TeamAAS.PathHelper.RuntimeDir, "References"),
  106. };
  107. foreach (var dir in refDirs)
  108. {
  109. try
  110. {
  111. if (!Directory.Exists(dir)) continue;
  112. foreach (var dll in Directory.GetFiles(dir, "*.dll"))
  113. {
  114. try { Editor.AddReferenceFromFile(dll); } catch { }
  115. }
  116. }
  117. catch { }
  118. }
  119. }
  120. #region 工具栏事件
  121. private void BtnRefs_Click(object sender, RoutedEventArgs e)
  122. {
  123. var owner = Window.GetWindow(this);
  124. var dlg = new ScriptReferenceDialog(Editor, owner);
  125. if (owner != null)
  126. {
  127. dlg.Owner = owner;
  128. dlg.WindowStartupLocation = WindowStartupLocation.CenterOwner;
  129. }
  130. dlg.ShowDialog();
  131. }
  132. private void BtnCompile_Click(object sender, RoutedEventArgs e) { CompileCode(); }
  133. private void BtnRun_Click(object sender, RoutedEventArgs e) { RunScript(); }
  134. private void BtnClearOutput_Click(object sender, RoutedEventArgs e) { OutputTextBox.Text = ""; }
  135. #endregion
  136. #region 编译 / 运行
  137. private void CompileCode()
  138. {
  139. ClearDiagnostics();
  140. AppendOutput("======== 编译 " + DateTime.Now.ToString("HH:mm:ss") + " ========");
  141. try
  142. {
  143. var c = Editor.Compile();
  144. Editor.ShowCompileDiagnostics(c);
  145. if (c != null)
  146. {
  147. bool hasSpan = c.Spans != null && c.Spans.Count > 0;
  148. if (hasSpan)
  149. {
  150. foreach (var s in c.Spans) AddDiagnostic(s.IsWarning, s.Message, s.Line, s.Column);
  151. }
  152. else if (c.Errors != null)
  153. {
  154. foreach (var er in c.Errors) AddDiagnostic(false, er, 0, 0);
  155. }
  156. if (c.Warnings != null) foreach (var w in c.Warnings) AppendOutput(" [警告] " + w);
  157. AppendOutput(c.Success
  158. ? " ✅ 编译成功(" + (c.AssemblyBytes != null ? c.AssemblyBytes.Length : 0) + " bytes)"
  159. : " ❌ 编译失败");
  160. }
  161. }
  162. catch (Exception ex)
  163. {
  164. AppendOutput(" 💥 编译异常:" + ex.Message);
  165. AddDiagnostic(false, ex.Message, 0, 0);
  166. }
  167. RefreshErrorCount();
  168. }
  169. private void RunScript()
  170. {
  171. ClearDiagnostics();
  172. AppendOutput("======== 运行 " + DateTime.Now.ToString("HH:mm:ss") + " ========");
  173. AppendOutput("(设计期:Service/CoreManager 可用;上游流程变量无数据;Log 写入平台日志)");
  174. var tempModel = new ScriptToolModel { Code = Editor.Code ?? "" };
  175. SetDesignTimeNames(tempModel);
  176. List<string> errs, warns;
  177. BasePlugin<ScriptToolModel> script;
  178. try
  179. {
  180. script = ScriptCompiler.CreateScript(Editor.Code ?? "", tempModel,
  181. TeamAAS.Global.ServiceRegistry.Default, null, out errs, out warns);
  182. }
  183. catch (Exception ex) { AppendOutput("💥 编译异常:" + ex.Message); return; }
  184. if (script == null)
  185. {
  186. if (errs != null) foreach (var er in errs) { AppendOutput(" [错误] " + er); AddDiagnostic(false, er, 0, 0); }
  187. RefreshErrorCount();
  188. AppendOutput(" ❌ 编译失败,未运行");
  189. return;
  190. }
  191. if (warns != null) foreach (var w in warns) AppendOutput(" [警告] " + w);
  192. Dictionary<string, object> results = null;
  193. NodeRunStatus status = NodeRunStatus.Failed;
  194. var oldOut = Console.Out;
  195. var oldErr = Console.Error;
  196. var sw = new StringWriter();
  197. try
  198. {
  199. Console.SetOut(sw);
  200. Console.SetError(sw);
  201. status = script.PluginRun(CancellationToken.None, out results);
  202. }
  203. catch (Exception ex) { AppendOutput("💥 运行异常:" + ex); }
  204. finally { Console.SetOut(oldOut); Console.SetError(oldErr); }
  205. var cout = sw.ToString();
  206. if (!string.IsNullOrEmpty(cout)) AppendOutput(cout.TrimEnd());
  207. AppendOutput(" 状态 = " + status);
  208. if (results != null)
  209. foreach (var kv in results) AppendOutput(" " + kv.Key + " = " + (kv.Value ?? "null"));
  210. }
  211. /// <summary>设计期临时模型的 FlowName/NodeName 是 internal set,反射赋值以免脚本 Log 时取到 null。</summary>
  212. private static void SetDesignTimeNames(ScriptToolModel m)
  213. {
  214. SetProp(m, "FlowName", "脚本测试");
  215. SetProp(m, "NodeName", "设计期运行");
  216. }
  217. private static void SetProp(object obj, string propName, object value)
  218. {
  219. try
  220. {
  221. var p = typeof(TeamAAS.FlowEditor.Plugins.BasePluginModel).GetProperty(propName);
  222. if (p == null) return;
  223. var setter = p.GetSetMethod(true);
  224. if (setter != null) setter.Invoke(obj, new object[] { value });
  225. }
  226. catch { /* 设计期辅助,失败忽略 */ }
  227. }
  228. #endregion
  229. #region 错误列表
  230. private void ClearDiagnostics() { _diagnostics.Clear(); }
  231. private void AddDiagnostic(bool isWarning, string message, int line, int column)
  232. {
  233. _diagnostics.Add(new DiagnosticItem
  234. {
  235. IsWarning = isWarning,
  236. SeverityText = isWarning ? "警告" : "错误",
  237. SeverityBrush = isWarning ? Brushes.Goldenrod : Brushes.IndianRed,
  238. Line = Math.Max(1, line),
  239. Column = Math.Max(1, column),
  240. Display = (line > 0 ? "L" + line + ",C" + column + " " : "") + message
  241. });
  242. }
  243. private void RefreshErrorCount()
  244. {
  245. int err = 0, warn = 0;
  246. foreach (var d in _diagnostics) { if (d.IsWarning) warn++; else err++; }
  247. ErrorCount.Text = "(" + err + " 错误 / " + warn + " 警告)";
  248. }
  249. private void ErrorListBox_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
  250. {
  251. try
  252. {
  253. var it = ErrorListBox.SelectedItem as DiagnosticItem;
  254. if (it == null || it.Line < 1) return;
  255. Editor.ScrollToLine(it.Line);
  256. }
  257. catch { /* 跳转失败忽略 */ }
  258. }
  259. #endregion
  260. #region IPluginViewSave
  261. public void SaveChanges()
  262. {
  263. if (_model == null) return;
  264. _model.Code = Editor.Code ?? "";
  265. }
  266. #endregion
  267. #region IPluginViewTip
  268. private TeamAAS.Dialogs.Dialogs.PluginViewDialog HostDialog
  269. {
  270. get { return Window.GetWindow(this) as TeamAAS.Dialogs.Dialogs.PluginViewDialog; }
  271. }
  272. public void ShowTip(string message) { var d = HostDialog; if (d != null) d.ShowTip(message); }
  273. public void ShowHoverHint(string message) { var d = HostDialog; if (d != null) d.ShowHoverHint(message); }
  274. #endregion
  275. private void AppendOutput(string text)
  276. {
  277. OutputTextBox.AppendText((text ?? "") + Environment.NewLine);
  278. OutputTextBox.ScrollToEnd();
  279. }
  280. }
  281. }