ProductManager.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using Newtonsoft.Json;
  6. using TeamAAS.FlowEditor.Execution;
  7. using TeamAAS.FlowEditor.Models;
  8. using TeamAAS.FlowEditor.ProductModels;
  9. using TeamAAS.FlowEngine.Execution;
  10. using TeamAAS.Communication;
  11. using TeamAAS;
  12. namespace TeamAAS.FlowEditor
  13. {
  14. /// <summary>
  15. /// 产品与流程文件的持久化管理器。
  16. /// 一个产品可以包含多个流程文件,当前编辑产品和实际运行产品分别维护,
  17. /// 以支持编辑态与运行态的切换。
  18. /// 相关数据模型(ProductMeta / HomeFrameItem / FrameArrangeMode / FlowMeta)
  19. /// 见 TeamAAS.FlowEditor.ProductModels 命名空间。
  20. /// </summary>
  21. public class ProductManager
  22. {
  23. private static ProductManager _instance;
  24. public static ProductManager Instance => _instance ?? (_instance = new ProductManager());
  25. /// <summary>
  26. /// 产品列表发生变化(新增/删除/重命名)时触发,供首页等订阅方刷新产品型号下拉。
  27. /// </summary>
  28. public event Action ProductListChanged;
  29. private void RaiseProductListChanged()
  30. {
  31. try { ProductListChanged?.Invoke(); } catch { }
  32. }
  33. /// <summary>Products 根目录(统一由 PathHelper 管理,跟随 PathHelper.Root)。</summary>
  34. public static string ProductsPath => PathHelper.ProductsDir;
  35. /// <summary>流程文件统一后缀(导入/导出与每个流程存储都用它)。</summary>
  36. public const string FlowExtension = ".aas";
  37. public string CurrentProductName { get; private set; } = "";
  38. private string _runProductName = "";
  39. public string RunProductName
  40. {
  41. get => _runProductName;
  42. private set => _runProductName = value;
  43. }
  44. public bool IsEditMode => CurrentProductName != RunProductName;
  45. /// <summary>当前编辑器外壳(切换产品时登记;FlowRunner 用它判断是否与编辑态共享流程图)</summary>
  46. public FlowEditorShellViewModel ActiveEditorShell { get; private set; }
  47. /// <summary>
  48. /// 编辑器切换产品时要显示的流程列表:始终从磁盘加载编辑副本。
  49. /// 监控模式(FlowEditorShellViewModel.IsMonitorMode=true)由 Shell 主动挂接
  50. /// FlowRunner.RunTabs,不再由这里隐式挂接,避免编辑态被运行态覆盖。
  51. /// </summary>
  52. private List<FlowTabItem> LoadEditorTabs(string productName)
  53. {
  54. return LoadAllFlows(productName);
  55. }
  56. public string GetProductPath(string productName) => Path.Combine(ProductsPath, productName);
  57. public string GetProductMetaPath(string productName) =>
  58. Path.Combine(ProductsPath, productName, "product.json");
  59. public ProductMeta LoadProductMeta(string productName)
  60. {
  61. try
  62. {
  63. var path = GetProductMetaPath(productName);
  64. if (File.Exists(path))
  65. {
  66. var json = File.ReadAllText(path);
  67. var meta = JsonConvert.DeserializeObject<ProductMeta>(json);
  68. if (meta != null)
  69. {
  70. meta.Name = productName; // 目录名即产品名(防历史 json 里名称不一致)
  71. meta.MigrateLegacyFrames(); // 旧版画面配置自动迁移为画面列表
  72. return meta;
  73. }
  74. }
  75. }
  76. catch { }
  77. return new ProductMeta { Name = productName };
  78. }
  79. public void SaveProductMeta(ProductMeta meta)
  80. {
  81. try
  82. {
  83. if (string.IsNullOrWhiteSpace(meta.Name)) return;
  84. var dir = Path.Combine(ProductsPath, meta.Name);
  85. if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
  86. var path = GetProductMetaPath(meta.Name);
  87. // 旧版画面字段已迁移进 HomeFrames,置空避免再次触发迁移
  88. meta.HomeFrameCount = 0;
  89. meta.HomeFrameNames = "";
  90. meta.HomeFrameShowDot = "";
  91. var json = JsonConvert.SerializeObject(meta, Formatting.Indented);
  92. File.WriteAllText(path, json);
  93. }
  94. catch { }
  95. }
  96. public List<string> GetProductList()
  97. {
  98. var list = new List<string>();
  99. try
  100. {
  101. if (Directory.Exists(ProductsPath))
  102. {
  103. list = Directory.GetDirectories(ProductsPath)
  104. .Select(Path.GetFileName)
  105. .OrderBy(n => n)
  106. .ToList();
  107. }
  108. }
  109. catch { }
  110. return list;
  111. }
  112. public List<ProductMeta> GetProductMetaList()
  113. {
  114. var result = new List<ProductMeta>();
  115. try
  116. {
  117. if (Directory.Exists(ProductsPath))
  118. {
  119. foreach (var dir in Directory.GetDirectories(ProductsPath))
  120. {
  121. var name = Path.GetFileName(dir);
  122. var meta = LoadProductMeta(name);
  123. if (meta == null) meta = new ProductMeta { Name = name };
  124. result.Add(meta);
  125. }
  126. result = result.OrderBy(m => m.Sequence).ThenBy(m => m.Name).ToList();
  127. }
  128. }
  129. catch { }
  130. return result;
  131. }
  132. public ProductMeta GetProductBySequence(int sequence)
  133. {
  134. return GetProductMetaList().FirstOrDefault(m => m.Sequence == sequence);
  135. }
  136. public bool CreateProduct(string productName, int sequence = 0)
  137. {
  138. if (string.IsNullOrWhiteSpace(productName)) return false;
  139. try
  140. {
  141. string safeName = SanitizeName(productName);
  142. string path = Path.Combine(ProductsPath, safeName);
  143. if (Directory.Exists(path)) return false;
  144. Directory.CreateDirectory(path);
  145. var meta = new ProductMeta { Name = safeName, Sequence = sequence };
  146. SaveProductMeta(meta);
  147. RaiseProductListChanged();
  148. return true;
  149. }
  150. catch { return false; }
  151. }
  152. public bool DeleteProduct(string productName)
  153. {
  154. if (string.IsNullOrWhiteSpace(productName)) return false;
  155. try
  156. {
  157. string path = Path.Combine(ProductsPath, productName);
  158. if (!Directory.Exists(path)) return false;
  159. Directory.Delete(path, true);
  160. if (CurrentProductName == productName)
  161. CurrentProductName = "";
  162. if (RunProductName == productName)
  163. RunProductName = "";
  164. RaiseProductListChanged();
  165. return true;
  166. }
  167. catch { return false; }
  168. }
  169. public bool RenameProduct(string oldName, string newName)
  170. {
  171. if (string.IsNullOrWhiteSpace(oldName) || string.IsNullOrWhiteSpace(newName)) return false;
  172. try
  173. {
  174. string safeNew = SanitizeName(newName);
  175. string oldPath = Path.Combine(ProductsPath, oldName);
  176. string newPath = Path.Combine(ProductsPath, safeNew);
  177. if (!Directory.Exists(oldPath)) return false;
  178. if (Directory.Exists(newPath)) return false;
  179. Directory.Move(oldPath, newPath);
  180. var oldMeta = LoadProductMeta(oldName);
  181. if (oldMeta != null)
  182. {
  183. oldMeta.Name = safeNew;
  184. SaveProductMeta(oldMeta);
  185. }
  186. if (CurrentProductName == oldName)
  187. CurrentProductName = safeNew;
  188. if (RunProductName == oldName)
  189. RunProductName = safeNew;
  190. RaiseProductListChanged();
  191. return true;
  192. }
  193. catch { return false; }
  194. }
  195. public bool SetProductSequence(string productName, int sequence)
  196. {
  197. var meta = LoadProductMeta(productName);
  198. if (meta == null) return false;
  199. meta.Sequence = sequence;
  200. SaveProductMeta(meta);
  201. return true;
  202. }
  203. /// <summary>主页画面配置变化(当前产品的画面数/排列/名称被修改)通知</summary>
  204. public event Action HomeFrameConfigChanged;
  205. /// <summary>当前编辑产品的画面配置(读不到产品时返回默认值;不落盘)</summary>
  206. public ProductMeta GetHomeFrameConfig(string productName)
  207. {
  208. var meta = string.IsNullOrEmpty(productName) ? null : LoadProductMeta(productName);
  209. return meta ?? new ProductMeta { Name = productName ?? "" };
  210. }
  211. /// <summary>保存画面配置到产品并广播变化(首页按新配置重建布局)</summary>
  212. public bool SaveHomeFrameConfig(ProductMeta meta)
  213. {
  214. if (meta == null || string.IsNullOrWhiteSpace(meta.Name)) return false;
  215. SaveProductMeta(meta);
  216. HomeFrameConfigChanged?.Invoke();
  217. return true;
  218. }
  219. public List<string> GetFlowFiles(string productName)
  220. {
  221. var list = new List<string>();
  222. try
  223. {
  224. string path = Path.Combine(ProductsPath, productName);
  225. if (Directory.Exists(path))
  226. {
  227. var flowNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  228. foreach (var f in Directory.GetFiles(path, "*" + FlowExtension))
  229. flowNames.Add(Path.GetFileNameWithoutExtension(f));
  230. list = flowNames.OrderBy(n => n).ToList();
  231. }
  232. }
  233. catch { }
  234. return list;
  235. }
  236. /// <summary>
  237. /// 产品流程文件的变更指纹(文件数 + 最新修改时间Ticks),用于判断运行态流程是否需要从磁盘重载,
  238. /// 避免高频触发时每次都反序列化。涵盖 .aas 流程文件与 flows.meta.json。
  239. /// </summary>
  240. public string GetProductFlowStamp(string productName)
  241. {
  242. try
  243. {
  244. if (string.IsNullOrWhiteSpace(productName)) return "";
  245. var dir = Path.Combine(ProductsPath, productName);
  246. if (!Directory.Exists(dir)) return "";
  247. long maxTicks = 0;
  248. int count = 0;
  249. foreach (var f in Directory.GetFiles(dir))
  250. {
  251. var name = Path.GetFileName(f);
  252. var ext = Path.GetExtension(f);
  253. bool isFlow = ext.Equals(FlowExtension, StringComparison.OrdinalIgnoreCase);
  254. bool isMeta = name.Equals("flows.meta.json", StringComparison.OrdinalIgnoreCase);
  255. if (!isFlow && !isMeta) continue;
  256. count++;
  257. var t = File.GetLastWriteTimeUtc(f).Ticks;
  258. if (t > maxTicks) maxTicks = t;
  259. }
  260. return count.ToString() + "|" + maxTicks.ToString();
  261. }
  262. catch { return ""; }
  263. }
  264. public string GetFlowMetaPath(string productName) =>
  265. Path.Combine(ProductsPath, productName, "flows.meta.json");
  266. /// <summary>
  267. /// 把所有流程 Tab 的元数据(触发类型/循环次数/画布尺寸)写入 flows.meta.json。
  268. /// 在 SaveAllFlows / SaveFlow 末尾调用,保证 Tab 属性变化能持久化。
  269. /// </summary>
  270. public void SaveFlowMetaMap(string productName, IEnumerable<FlowTabItem> tabs)
  271. {
  272. // 空产品名会让 Path.Combine(ProductsPath, "", "flows.meta.json") 退化为 Products/flows.meta.json
  273. // (根目录凭空多一份),必须在写文件前拦截。
  274. if (string.IsNullOrWhiteSpace(productName)) return;
  275. try
  276. {
  277. var dir = Path.Combine(ProductsPath, productName);
  278. if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
  279. var list = new List<FlowMeta>();
  280. foreach (var tab in tabs ?? Enumerable.Empty<FlowTabItem>())
  281. {
  282. var meta = new FlowMeta
  283. {
  284. Name = tab.Name,
  285. TriggerType = (int)tab.TriggerType,
  286. LoopCount = tab.LoopCount,
  287. CanvasWidth = tab.CanvasWidth,
  288. CanvasHeight = tab.CanvasHeight,
  289. LoopIntervalMs = tab.LoopIntervalMs
  290. };
  291. list.Add(meta);
  292. // 调试日志:记录保存时的 TriggerType 值
  293. AppLogger.Info($"SaveFlowMeta: {tab.Name} TriggerType={tab.TriggerType} (int={(int)tab.TriggerType})", nameof(ProductManager));
  294. }
  295. var json = JsonConvert.SerializeObject(list, Formatting.Indented);
  296. File.WriteAllText(GetFlowMetaPath(productName), json);
  297. AppLogger.Info($"SaveFlowMeta: wrote {list.Count} entries to {GetFlowMetaPath(productName)}", nameof(ProductManager));
  298. }
  299. catch (Exception ex) { AppLogger.Error("SaveFlowMeta 异常", ex, nameof(ProductManager)); }
  300. }
  301. /// <summary>
  302. /// 读取 flows.meta.json,按 Name 索引返回字典。文件不存在返回空字典(兼容老产品目录)。
  303. /// </summary>
  304. public Dictionary<string, FlowMeta> LoadFlowMetaMap(string productName)
  305. {
  306. var map = new Dictionary<string, FlowMeta>(StringComparer.Ordinal);
  307. if (string.IsNullOrWhiteSpace(productName)) return map; // 空名会误读 Products 根目录的残留文件
  308. try
  309. {
  310. var path = GetFlowMetaPath(productName);
  311. if (!File.Exists(path))
  312. {
  313. AppLogger.Info($"LoadFlowMeta: 文件不存在 {path}", nameof(ProductManager));
  314. return map;
  315. }
  316. var json = File.ReadAllText(path);
  317. var list = JsonConvert.DeserializeObject<List<FlowMeta>>(json);
  318. if (list != null)
  319. {
  320. foreach (var m in list)
  321. {
  322. if (!string.IsNullOrEmpty(m?.Name))
  323. {
  324. map[m.Name] = m;
  325. AppLogger.Info($"LoadFlowMeta: {m.Name} TriggerType={m.TriggerType} (int={m.TriggerType})", nameof(ProductManager));
  326. }
  327. }
  328. }
  329. AppLogger.Info($"LoadFlowMeta: loaded {map.Count} entries from {path}", nameof(ProductManager));
  330. }
  331. catch (Exception ex) { AppLogger.Error("LoadFlowMeta 异常", ex, nameof(ProductManager)); }
  332. return map;
  333. }
  334. public bool SaveFlow(string productName, FlowTabItem tab)
  335. {
  336. if (string.IsNullOrWhiteSpace(productName) || tab == null) return false;
  337. try
  338. {
  339. string dir = Path.Combine(ProductsPath, productName);
  340. if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
  341. string filePath = Path.Combine(dir, tab.Name + FlowExtension); // 统一 .aas
  342. bool ok = tab.EditorVm?.ExportFlow(tab.Name, filePath) ?? false;
  343. if (ok)
  344. {
  345. SaveFlowMetaSingle(productName, tab); // 同步更新单条流程元数据
  346. }
  347. return ok;
  348. }
  349. catch { return false; }
  350. }
  351. /// <summary>
  352. /// 单条流程保存时更新其元数据:读现有 flows.meta.json,按 Name 替换/添加后写回。
  353. /// </summary>
  354. public void SaveFlowMetaSingle(string productName, FlowTabItem tab)
  355. {
  356. if (tab == null) return;
  357. try
  358. {
  359. var map = LoadFlowMetaMap(productName);
  360. map[tab.Name] = new FlowMeta
  361. {
  362. Name = tab.Name,
  363. TriggerType = (int)tab.TriggerType,
  364. LoopCount = tab.LoopCount,
  365. CanvasWidth = tab.CanvasWidth,
  366. CanvasHeight = tab.CanvasHeight,
  367. LoopIntervalMs = tab.LoopIntervalMs
  368. };
  369. var dir = Path.Combine(ProductsPath, productName);
  370. if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
  371. var json = JsonConvert.SerializeObject(map.Values.ToList(), Formatting.Indented);
  372. File.WriteAllText(GetFlowMetaPath(productName), json);
  373. }
  374. catch { }
  375. }
  376. public bool SaveAllFlows(string productName, IEnumerable<FlowTabItem> tabs)
  377. {
  378. if (string.IsNullOrWhiteSpace(productName) || tabs == null) return false;
  379. try
  380. {
  381. string dir = Path.Combine(ProductsPath, productName);
  382. if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
  383. var tabNames = new HashSet<string>(tabs.Select(t => t.Name), StringComparer.OrdinalIgnoreCase);
  384. foreach (var tab in tabs)
  385. {
  386. string filePath = Path.Combine(dir, tab.Name + FlowExtension); // 统一 .aas
  387. tab.EditorVm?.ExportFlow(tab.Name, filePath);
  388. }
  389. // 删除磁盘上已不存在于 Tab 列表中的流程文件
  390. // (例如用户删掉了"流程2"后点保存,切换产品再切回来就不会复活了)
  391. foreach (var file in Directory.GetFiles(dir, "*" + FlowExtension))
  392. {
  393. string name = Path.GetFileNameWithoutExtension(file);
  394. if (!tabNames.Contains(name))
  395. {
  396. try { File.Delete(file); }
  397. catch { /* 文件被占用等异常忽略,下次保存再清理 */ }
  398. }
  399. }
  400. // 同步写入流程 Tab 元数据(触发类型/循环次数/画布尺寸)
  401. SaveFlowMetaMap(productName, tabs);
  402. // 同步写入该产品的全局变量快照(Product 作用域变量值)
  403. GlobalVariableManager.Instance.SaveSnapshot(GetGlobalVarsPath(productName));
  404. return true;
  405. }
  406. catch { return false; }
  407. }
  408. public FlowTabItem LoadFlow(string productName, string flowName)
  409. => LoadFlow(productName, flowName, null, null);
  410. public FlowTabItem LoadFlow(string productName, string flowName, FlowMeta meta, ResultRegistry registry = null)
  411. {
  412. if (string.IsNullOrWhiteSpace(productName) || string.IsNullOrWhiteSpace(flowName)) return null;
  413. try
  414. {
  415. string filePath = Path.Combine(ProductsPath, productName, flowName + FlowExtension); // 统一 .aas
  416. if (!File.Exists(filePath)) return null;
  417. var tab = new FlowTabItem(flowName);
  418. // 应用磁盘上保存的 Tab 元数据(触发类型/循环次数/画布尺寸/循环间隔);meta 为 null 时用默认值
  419. if (meta != null)
  420. {
  421. tab.TriggerType = (TriggerType)meta.TriggerType;
  422. tab.LoopCount = meta.LoopCount;
  423. tab.CanvasWidth = meta.CanvasWidth;
  424. tab.CanvasHeight = meta.CanvasHeight;
  425. tab.LoopIntervalMs = meta.LoopIntervalMs;
  426. }
  427. FlowGraph result = FlowFileStore.Load(filePath);
  428. if (result != null && tab.EditorVm != null)
  429. {
  430. var graph = tab.EditorVm.Graph;
  431. // 绑定结果注册表:运行态=Main,编辑/调试态=Debug
  432. graph.Registry = registry ?? ResultRegistry.Debug;
  433. graph.Registry.ClearFlow(graph.GraphId);
  434. result.GraphId = graph.GraphId;
  435. result.GraphName = graph.GraphName;
  436. var idMap = new Dictionary<string, string>();
  437. foreach (var item in result.Nodes)
  438. {
  439. string oldNodeId = item.NodeId;
  440. item.InitializeNode(graph.GraphId, graph.GraphName, graph.Registry);
  441. if (!string.IsNullOrEmpty(oldNodeId))
  442. idMap[oldNodeId] = item.NodeId;
  443. }
  444. if (result.Connections != null && idMap.Count > 0)
  445. {
  446. foreach (var conn in result.Connections)
  447. {
  448. if (idMap.TryGetValue(conn.SourceNodeId, out var newSrcId))
  449. conn.SourceNodeId = newSrcId;
  450. if (idMap.TryGetValue(conn.TargetNodeId, out var newTgtId))
  451. conn.TargetNodeId = newTgtId;
  452. }
  453. }
  454. graph.Nodes.Clear();
  455. foreach (var n in result.Nodes)
  456. graph.Nodes.Add(n);
  457. graph.Connections.Clear();
  458. foreach (var c in result.Connections)
  459. graph.Connections.Add(c);
  460. tab.EditorVm.GraphDataChanged?.Invoke(graph, null);
  461. tab.EditorVm?.MarkClean(); // 刚加载的内容视为"已保存"状态
  462. }
  463. return tab;
  464. }
  465. catch { return null; }
  466. }
  467. public List<FlowTabItem> LoadAllFlows(string productName, ResultRegistry registry = null)
  468. {
  469. var tabs = new List<FlowTabItem>();
  470. var flowFiles = GetFlowFiles(productName);
  471. // 读一次元数据字典,按 Name 应用到对应 Tab(兼容老产品目录:无文件则全用默认值)
  472. var metaMap = LoadFlowMetaMap(productName);
  473. foreach (var flowName in flowFiles)
  474. {
  475. metaMap.TryGetValue(flowName, out var meta);
  476. if (meta != null)
  477. AppLogger.Info($"LoadAllFlows: {flowName} 找到 meta,TriggerType={meta.TriggerType}", nameof(ProductManager));
  478. else
  479. AppLogger.Info($"LoadAllFlows: {flowName} 未找到 meta,使用默认值", nameof(ProductManager));
  480. var tab = LoadFlow(productName, flowName, meta, registry);
  481. if (tab != null)
  482. {
  483. AppLogger.Info($"LoadAllFlows: {flowName} 加载完成,TriggerType={tab.TriggerType}", nameof(ProductManager));
  484. tabs.Add(tab);
  485. }
  486. }
  487. return tabs;
  488. }
  489. public string GetGlobalVarsPath(string productName) =>
  490. Path.Combine(ProductsPath, productName, "global_vars.json");
  491. /// <summary>
  492. /// 当前内存里的全局变量归属哪个产品。加载快照/切换产品时更新。
  493. /// 内存变量是进程级共享的,编辑态与运行态都可能改它,
  494. /// 只按 CurrentProductName 存会存错产品(例如主页已切到运行产品但编辑态还停在另一个)。
  495. /// </summary>
  496. public string GlobalVarsOwnerProduct { get; private set; } = "";
  497. /// <summary>
  498. /// 保存“当前归属产品”的全局变量快照。
  499. /// 产品切换前、以及设置页手动保存时调用;无归属产品时回退到编辑态当前产品。
  500. /// </summary>
  501. public bool SaveGlobalVarsForOwner()
  502. {
  503. var owner = !string.IsNullOrEmpty(GlobalVarsOwnerProduct) ? GlobalVarsOwnerProduct : CurrentProductName;
  504. return SaveGlobalVarsFor(owner);
  505. }
  506. /// <summary>
  507. /// 把 Product 作用域变量保存到指定产品的快照文件(Products\<名>\global_vars.json)。
  508. /// 产品切换前、以及产品变量弹窗点确定时调用。
  509. /// </summary>
  510. public bool SaveGlobalVarsFor(string productName)
  511. {
  512. if (string.IsNullOrEmpty(productName)) return false;
  513. try
  514. {
  515. GlobalVariableManager.Instance.SaveSnapshot(GetGlobalVarsPath(productName));
  516. GlobalVarsOwnerProduct = productName;
  517. return true;
  518. }
  519. catch { return false; }
  520. }
  521. /// <summary>
  522. /// 从目标产品快照加载 Product 作用域变量到 GlobalVariableManager(Global 子集不受影响)。
  523. /// 快照不存在时清空 Product 子集(切到新产品应从空开始,而非继承上个产品的变量)。
  524. /// Variables 是 ObservableCollection 绑定 UI,必须在 UI 线程调用。
  525. /// </summary>
  526. public bool LoadProductGlobalVars(string productName)
  527. {
  528. if (string.IsNullOrEmpty(productName)) return false;
  529. try
  530. {
  531. GlobalVarsOwnerProduct = productName;
  532. return GlobalVariableManager.Instance.LoadSnapshot(GetGlobalVarsPath(productName));
  533. }
  534. catch { return false; }
  535. }
  536. /// <summary>
  537. /// 释放一组 FlowTab 持有的资源:节点插件、结果字典、图节点引用。
  538. /// 切换产品 / 卸载运行产品 / 清理编辑态时统一调用。
  539. /// 安全保证:跳过仍被 FlowRunner.RunTabs 持有的运行态 Tab——
  540. /// 监控模式下编辑器的 FlowTabs 挂的就是这批对象,若在此清空会导致
  541. /// 切换产品时正在运行的流程画布变空、主页面运行也被打断。
  542. /// </summary>
  543. internal static void DisposeFlowTabs(IEnumerable<FlowTabItem> tabs)
  544. {
  545. if (tabs == null) return;
  546. var runTabs = FlowRunner.Instance.RunTabs;
  547. foreach (var tab in tabs)
  548. {
  549. if (tab == null) continue;
  550. if (runTabs != null && runTabs.Contains(tab)) continue;
  551. // 1. 释放节点插件(相机句柄、连接流、大对象等)
  552. if (tab.Graph != null)
  553. {
  554. foreach (var node in tab.Graph.Nodes)
  555. {
  556. try { node.PluginModel?.Dispose(); }
  557. catch { /* 单个插件释放失败不影响整体 */ }
  558. try
  559. {
  560. node.ResultItems?.Clear();
  561. node.ExecutionHistory?.Clear();
  562. }
  563. catch { }
  564. }
  565. // 2. 清掉该流程绑定的 ResultRegistry 实例中本流程的全部结果
  566. // (运行态图 → Main,编辑/调试态图 → Debug,互不干扰)
  567. if (!string.IsNullOrEmpty(tab.Graph.GraphId))
  568. tab.Graph.Registry?.ClearFlow(tab.Graph.GraphId);
  569. tab.Graph.Nodes.Clear();
  570. tab.Graph.Connections.Clear();
  571. }
  572. // 3. 清 EditorVm 引用
  573. if (tab.EditorVm != null)
  574. {
  575. try { tab.EditorVm.SelectedNode = null; } catch { }
  576. }
  577. }
  578. // 释放完成后强制全代 GC,确保插件引用的大对象被回收
  579. GC.Collect(2, GCCollectionMode.Forced, true);
  580. GC.WaitForPendingFinalizers();
  581. GC.Collect(2, GCCollectionMode.Forced, true);
  582. }
  583. /// <summary>
  584. /// 编辑模式:切换产品进行编辑(不影响主程序运行)。
  585. /// 注意:不再自动保存——是否保存由调用方(ProductViewModel)根据脏标记提示用户确认。
  586. /// </summary>
  587. public void SwitchProduct(string productName, FlowEditorShellViewModel shellVm)
  588. {
  589. if (shellVm == null) return;
  590. // 切换前:保存当前产品的全局变量快照和流程元数据
  591. SaveGlobalVarsForOwner();
  592. SaveFlowMetaMap(CurrentProductName, shellVm.FlowTabs);
  593. // 释放旧产品的资源(节点插件 Dispose + ResultRegistry 清理 + 图引用解除)
  594. DisposeFlowTabs(shellVm.FlowTabs);
  595. // 切换编辑产品:完全清空 Debug(含上一产品各流程与子流程残留),避免跨产品数据堆积
  596. ResultRegistry.Debug.ClearAllFlows();
  597. shellVm.FlowTabs.Clear();
  598. CurrentProductName = productName;
  599. ActiveEditorShell = shellVm;
  600. if (!string.IsNullOrEmpty(productName))
  601. {
  602. var flows = LoadEditorTabs(productName);
  603. foreach (var flow in flows)
  604. {
  605. shellVm.FlowTabs.Add(flow);
  606. }
  607. }
  608. if (shellVm.FlowTabs.Count == 0)
  609. {
  610. var defaultTab = new FlowTabItem("流程1");
  611. shellVm.FlowTabs.Add(defaultTab);
  612. }
  613. shellVm.ActiveTab = shellVm.FlowTabs.FirstOrDefault();
  614. // 加载目标产品的全局变量快照(Variables 是 ObservableCollection,UI 线程同步修改)
  615. LoadProductGlobalVars(productName);
  616. // 切换产品后刷新监控选项可用性 + 自动进入/退出监控(运行产品且运行中→默认监控)
  617. shellVm.ApplyMonitorAvailability();
  618. // 强制全代 GC + 等待终结器,确保 LOH 大对象(图像/位图源/CogRecord 等)被回收
  619. GC.Collect(2, GCCollectionMode.Forced, true);
  620. GC.WaitForPendingFinalizers();
  621. GC.Collect(2, GCCollectionMode.Forced, true);
  622. }
  623. /// <summary>
  624. /// 编辑模式:切换产品(异步版)。文件 IO(加载新产品)在后台线程执行,
  625. /// FlowTabs 等绑定 UI 的集合变更始终留在调用线程(UI 线程)。
  626. /// 不自动保存;是否保存由调用方在切换前询问用户。
  627. /// </summary>
  628. public async System.Threading.Tasks.Task SwitchProductAsync(string productName, FlowEditorShellViewModel shellVm)
  629. {
  630. if (shellVm == null) return;
  631. // 切换前:在 UI 线程保存当前产品全局变量快照和流程元数据
  632. SaveGlobalVarsForOwner();
  633. SaveFlowMetaMap(CurrentProductName, shellVm.FlowTabs);
  634. // 释放旧产品的资源(节点插件 Dispose + ResultRegistry 清理 + 图引用解除)
  635. DisposeFlowTabs(shellVm.FlowTabs);
  636. // 切换编辑产品:完全清空 Debug(含上一产品各流程与子流程残留),避免跨产品数据堆积
  637. ResultRegistry.Debug.ClearAllFlows();
  638. shellVm.FlowTabs.Clear();
  639. // 后台线程:解析目标产品流程(始终读盘,监控挂接由 Shell 主动控制)
  640. List<FlowTabItem> flows = null;
  641. await System.Threading.Tasks.Task.Run(() =>
  642. {
  643. if (!string.IsNullOrEmpty(productName))
  644. flows = LoadEditorTabs(productName);
  645. });
  646. // 调用线程(UI):应用集合变更
  647. shellVm.FlowTabs.Clear();
  648. CurrentProductName = productName;
  649. ActiveEditorShell = shellVm;
  650. if (flows != null)
  651. {
  652. foreach (var flow in flows)
  653. shellVm.FlowTabs.Add(flow);
  654. }
  655. if (shellVm.FlowTabs.Count == 0)
  656. shellVm.FlowTabs.Add(new FlowTabItem("流程1"));
  657. shellVm.ActiveTab = shellVm.FlowTabs.FirstOrDefault();
  658. // UI 线程:加载目标产品全局变量快照
  659. LoadProductGlobalVars(productName);
  660. // 切换产品后刷新监控选项可用性 + 自动进入/退出监控(运行产品且运行中→默认监控)
  661. shellVm.ApplyMonitorAvailability();
  662. // 强制全代 GC + 等待终结器,确保 LOH 大对象(图像/位图源/CogRecord 等)被回收
  663. GC.Collect(2, GCCollectionMode.Forced, true);
  664. GC.WaitForPendingFinalizers();
  665. GC.Collect(2, GCCollectionMode.Forced, true);
  666. }
  667. /// <summary>
  668. /// 运行模式:加载产品作为当前运行程序(从编辑显式加载)
  669. /// </summary>
  670. public void LoadProductToRun(string productName)
  671. {
  672. // 切换前先把上一个归属产品的全局变量落盘:
  673. // 设置页改的值只存在内存里,不先存就会随产品切换丢失
  674. if (!string.IsNullOrEmpty(GlobalVarsOwnerProduct) && GlobalVarsOwnerProduct != productName)
  675. SaveGlobalVarsForOwner();
  676. RunProductName = productName;
  677. // 主页加载运行产品:同步加载该产品的全局变量快照(与编辑模式一致)
  678. LoadProductGlobalVars(productName);
  679. }
  680. /// <summary>
  681. /// 基于序号切换产品(供外部调用,如 PLC 触发)。
  682. /// 产品切换经 LoadRunProduct → LoadProductGlobalVars 自动载入该产品的 Product 变量,
  683. /// 故此处无需再单独切换全局变量配方(配方机制已退休)。
  684. /// </summary>
  685. public bool SwitchProductBySequence(int sequence)
  686. {
  687. var meta = GetProductBySequence(sequence);
  688. if (meta == null) return false;
  689. RunProductName = meta.Name;
  690. // 加载为运行产品(FlowRunner 会复制一份运行态流程,不影响编辑态)
  691. TeamAAS.FlowEngine.Execution.FlowRunner.Instance.LoadRunProduct(meta.Name);
  692. return true;
  693. }
  694. private static string SanitizeName(string name)
  695. => TeamAAS.PathHelper.SanitizeFileName(name);
  696. }
  697. }