using System; using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; using TeamAAS.FlowEditor.Execution; using TeamAAS.FlowEditor.Models; using TeamAAS.FlowEditor.ProductModels; using TeamAAS.FlowEngine.Execution; using TeamAAS.Communication; using TeamAAS; namespace TeamAAS.FlowEditor { /// /// 产品与流程文件的持久化管理器。 /// 一个产品可以包含多个流程文件,当前编辑产品和实际运行产品分别维护, /// 以支持编辑态与运行态的切换。 /// 相关数据模型(ProductMeta / HomeFrameItem / FrameArrangeMode / FlowMeta) /// 见 TeamAAS.FlowEditor.ProductModels 命名空间。 /// public class ProductManager { private static ProductManager _instance; public static ProductManager Instance => _instance ?? (_instance = new ProductManager()); /// /// 产品列表发生变化(新增/删除/重命名)时触发,供首页等订阅方刷新产品型号下拉。 /// public event Action ProductListChanged; private void RaiseProductListChanged() { try { ProductListChanged?.Invoke(); } catch { } } /// Products 根目录(统一由 PathHelper 管理,跟随 PathHelper.Root)。 public static string ProductsPath => PathHelper.ProductsDir; /// 流程文件统一后缀(导入/导出与每个流程存储都用它)。 public const string FlowExtension = ".aas"; public string CurrentProductName { get; private set; } = ""; private string _runProductName = ""; public string RunProductName { get => _runProductName; private set => _runProductName = value; } public bool IsEditMode => CurrentProductName != RunProductName; /// 当前编辑器外壳(切换产品时登记;FlowRunner 用它判断是否与编辑态共享流程图) public FlowEditorShellViewModel ActiveEditorShell { get; private set; } /// /// 编辑器切换产品时要显示的流程列表:始终从磁盘加载编辑副本。 /// 监控模式(FlowEditorShellViewModel.IsMonitorMode=true)由 Shell 主动挂接 /// FlowRunner.RunTabs,不再由这里隐式挂接,避免编辑态被运行态覆盖。 /// private List LoadEditorTabs(string productName) { return LoadAllFlows(productName); } public string GetProductPath(string productName) => Path.Combine(ProductsPath, productName); public string GetProductMetaPath(string productName) => Path.Combine(ProductsPath, productName, "product.json"); public ProductMeta LoadProductMeta(string productName) { try { var path = GetProductMetaPath(productName); if (File.Exists(path)) { var json = File.ReadAllText(path); var meta = JsonConvert.DeserializeObject(json); if (meta != null) { meta.Name = productName; // 目录名即产品名(防历史 json 里名称不一致) meta.MigrateLegacyFrames(); // 旧版画面配置自动迁移为画面列表 return meta; } } } catch { } return new ProductMeta { Name = productName }; } public void SaveProductMeta(ProductMeta meta) { try { if (string.IsNullOrWhiteSpace(meta.Name)) return; var dir = Path.Combine(ProductsPath, meta.Name); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var path = GetProductMetaPath(meta.Name); // 旧版画面字段已迁移进 HomeFrames,置空避免再次触发迁移 meta.HomeFrameCount = 0; meta.HomeFrameNames = ""; meta.HomeFrameShowDot = ""; var json = JsonConvert.SerializeObject(meta, Formatting.Indented); File.WriteAllText(path, json); } catch { } } public List GetProductList() { var list = new List(); try { if (Directory.Exists(ProductsPath)) { list = Directory.GetDirectories(ProductsPath) .Select(Path.GetFileName) .OrderBy(n => n) .ToList(); } } catch { } return list; } public List GetProductMetaList() { var result = new List(); try { if (Directory.Exists(ProductsPath)) { foreach (var dir in Directory.GetDirectories(ProductsPath)) { var name = Path.GetFileName(dir); var meta = LoadProductMeta(name); if (meta == null) meta = new ProductMeta { Name = name }; result.Add(meta); } result = result.OrderBy(m => m.Sequence).ThenBy(m => m.Name).ToList(); } } catch { } return result; } public ProductMeta GetProductBySequence(int sequence) { return GetProductMetaList().FirstOrDefault(m => m.Sequence == sequence); } public bool CreateProduct(string productName, int sequence = 0) { if (string.IsNullOrWhiteSpace(productName)) return false; try { string safeName = SanitizeName(productName); string path = Path.Combine(ProductsPath, safeName); if (Directory.Exists(path)) return false; Directory.CreateDirectory(path); var meta = new ProductMeta { Name = safeName, Sequence = sequence }; SaveProductMeta(meta); RaiseProductListChanged(); return true; } catch { return false; } } public bool DeleteProduct(string productName) { if (string.IsNullOrWhiteSpace(productName)) return false; try { string path = Path.Combine(ProductsPath, productName); if (!Directory.Exists(path)) return false; Directory.Delete(path, true); if (CurrentProductName == productName) CurrentProductName = ""; if (RunProductName == productName) RunProductName = ""; RaiseProductListChanged(); return true; } catch { return false; } } public bool RenameProduct(string oldName, string newName) { if (string.IsNullOrWhiteSpace(oldName) || string.IsNullOrWhiteSpace(newName)) return false; try { string safeNew = SanitizeName(newName); string oldPath = Path.Combine(ProductsPath, oldName); string newPath = Path.Combine(ProductsPath, safeNew); if (!Directory.Exists(oldPath)) return false; if (Directory.Exists(newPath)) return false; Directory.Move(oldPath, newPath); var oldMeta = LoadProductMeta(oldName); if (oldMeta != null) { oldMeta.Name = safeNew; SaveProductMeta(oldMeta); } if (CurrentProductName == oldName) CurrentProductName = safeNew; if (RunProductName == oldName) RunProductName = safeNew; RaiseProductListChanged(); return true; } catch { return false; } } public bool SetProductSequence(string productName, int sequence) { var meta = LoadProductMeta(productName); if (meta == null) return false; meta.Sequence = sequence; SaveProductMeta(meta); return true; } /// 主页画面配置变化(当前产品的画面数/排列/名称被修改)通知 public event Action HomeFrameConfigChanged; /// 当前编辑产品的画面配置(读不到产品时返回默认值;不落盘) public ProductMeta GetHomeFrameConfig(string productName) { var meta = string.IsNullOrEmpty(productName) ? null : LoadProductMeta(productName); return meta ?? new ProductMeta { Name = productName ?? "" }; } /// 保存画面配置到产品并广播变化(首页按新配置重建布局) public bool SaveHomeFrameConfig(ProductMeta meta) { if (meta == null || string.IsNullOrWhiteSpace(meta.Name)) return false; SaveProductMeta(meta); HomeFrameConfigChanged?.Invoke(); return true; } public List GetFlowFiles(string productName) { var list = new List(); try { string path = Path.Combine(ProductsPath, productName); if (Directory.Exists(path)) { var flowNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var f in Directory.GetFiles(path, "*" + FlowExtension)) flowNames.Add(Path.GetFileNameWithoutExtension(f)); list = flowNames.OrderBy(n => n).ToList(); } } catch { } return list; } /// /// 产品流程文件的变更指纹(文件数 + 最新修改时间Ticks),用于判断运行态流程是否需要从磁盘重载, /// 避免高频触发时每次都反序列化。涵盖 .aas 流程文件与 flows.meta.json。 /// public string GetProductFlowStamp(string productName) { try { if (string.IsNullOrWhiteSpace(productName)) return ""; var dir = Path.Combine(ProductsPath, productName); if (!Directory.Exists(dir)) return ""; long maxTicks = 0; int count = 0; foreach (var f in Directory.GetFiles(dir)) { var name = Path.GetFileName(f); var ext = Path.GetExtension(f); bool isFlow = ext.Equals(FlowExtension, StringComparison.OrdinalIgnoreCase); bool isMeta = name.Equals("flows.meta.json", StringComparison.OrdinalIgnoreCase); if (!isFlow && !isMeta) continue; count++; var t = File.GetLastWriteTimeUtc(f).Ticks; if (t > maxTicks) maxTicks = t; } return count.ToString() + "|" + maxTicks.ToString(); } catch { return ""; } } public string GetFlowMetaPath(string productName) => Path.Combine(ProductsPath, productName, "flows.meta.json"); /// /// 把所有流程 Tab 的元数据(触发类型/循环次数/画布尺寸)写入 flows.meta.json。 /// 在 SaveAllFlows / SaveFlow 末尾调用,保证 Tab 属性变化能持久化。 /// public void SaveFlowMetaMap(string productName, IEnumerable tabs) { // 空产品名会让 Path.Combine(ProductsPath, "", "flows.meta.json") 退化为 Products/flows.meta.json // (根目录凭空多一份),必须在写文件前拦截。 if (string.IsNullOrWhiteSpace(productName)) return; try { var dir = Path.Combine(ProductsPath, productName); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var list = new List(); foreach (var tab in tabs ?? Enumerable.Empty()) { var meta = new FlowMeta { Name = tab.Name, TriggerType = (int)tab.TriggerType, LoopCount = tab.LoopCount, CanvasWidth = tab.CanvasWidth, CanvasHeight = tab.CanvasHeight, LoopIntervalMs = tab.LoopIntervalMs }; list.Add(meta); // 调试日志:记录保存时的 TriggerType 值 AppLogger.Info($"SaveFlowMeta: {tab.Name} TriggerType={tab.TriggerType} (int={(int)tab.TriggerType})", nameof(ProductManager)); } var json = JsonConvert.SerializeObject(list, Formatting.Indented); File.WriteAllText(GetFlowMetaPath(productName), json); AppLogger.Info($"SaveFlowMeta: wrote {list.Count} entries to {GetFlowMetaPath(productName)}", nameof(ProductManager)); } catch (Exception ex) { AppLogger.Error("SaveFlowMeta 异常", ex, nameof(ProductManager)); } } /// /// 读取 flows.meta.json,按 Name 索引返回字典。文件不存在返回空字典(兼容老产品目录)。 /// public Dictionary LoadFlowMetaMap(string productName) { var map = new Dictionary(StringComparer.Ordinal); if (string.IsNullOrWhiteSpace(productName)) return map; // 空名会误读 Products 根目录的残留文件 try { var path = GetFlowMetaPath(productName); if (!File.Exists(path)) { AppLogger.Info($"LoadFlowMeta: 文件不存在 {path}", nameof(ProductManager)); return map; } var json = File.ReadAllText(path); var list = JsonConvert.DeserializeObject>(json); if (list != null) { foreach (var m in list) { if (!string.IsNullOrEmpty(m?.Name)) { map[m.Name] = m; AppLogger.Info($"LoadFlowMeta: {m.Name} TriggerType={m.TriggerType} (int={m.TriggerType})", nameof(ProductManager)); } } } AppLogger.Info($"LoadFlowMeta: loaded {map.Count} entries from {path}", nameof(ProductManager)); } catch (Exception ex) { AppLogger.Error("LoadFlowMeta 异常", ex, nameof(ProductManager)); } return map; } public bool SaveFlow(string productName, FlowTabItem tab) { if (string.IsNullOrWhiteSpace(productName) || tab == null) return false; try { string dir = Path.Combine(ProductsPath, productName); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); string filePath = Path.Combine(dir, tab.Name + FlowExtension); // 统一 .aas bool ok = tab.EditorVm?.ExportFlow(tab.Name, filePath) ?? false; if (ok) { SaveFlowMetaSingle(productName, tab); // 同步更新单条流程元数据 } return ok; } catch { return false; } } /// /// 单条流程保存时更新其元数据:读现有 flows.meta.json,按 Name 替换/添加后写回。 /// public void SaveFlowMetaSingle(string productName, FlowTabItem tab) { if (tab == null) return; try { var map = LoadFlowMetaMap(productName); map[tab.Name] = new FlowMeta { Name = tab.Name, TriggerType = (int)tab.TriggerType, LoopCount = tab.LoopCount, CanvasWidth = tab.CanvasWidth, CanvasHeight = tab.CanvasHeight, LoopIntervalMs = tab.LoopIntervalMs }; var dir = Path.Combine(ProductsPath, productName); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var json = JsonConvert.SerializeObject(map.Values.ToList(), Formatting.Indented); File.WriteAllText(GetFlowMetaPath(productName), json); } catch { } } public bool SaveAllFlows(string productName, IEnumerable tabs) { if (string.IsNullOrWhiteSpace(productName) || tabs == null) return false; try { string dir = Path.Combine(ProductsPath, productName); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var tabNames = new HashSet(tabs.Select(t => t.Name), StringComparer.OrdinalIgnoreCase); foreach (var tab in tabs) { string filePath = Path.Combine(dir, tab.Name + FlowExtension); // 统一 .aas tab.EditorVm?.ExportFlow(tab.Name, filePath); } // 删除磁盘上已不存在于 Tab 列表中的流程文件 // (例如用户删掉了"流程2"后点保存,切换产品再切回来就不会复活了) foreach (var file in Directory.GetFiles(dir, "*" + FlowExtension)) { string name = Path.GetFileNameWithoutExtension(file); if (!tabNames.Contains(name)) { try { File.Delete(file); } catch { /* 文件被占用等异常忽略,下次保存再清理 */ } } } // 同步写入流程 Tab 元数据(触发类型/循环次数/画布尺寸) SaveFlowMetaMap(productName, tabs); // 同步写入该产品的全局变量快照(Product 作用域变量值) GlobalVariableManager.Instance.SaveSnapshot(GetGlobalVarsPath(productName)); return true; } catch { return false; } } public FlowTabItem LoadFlow(string productName, string flowName) => LoadFlow(productName, flowName, null, null); public FlowTabItem LoadFlow(string productName, string flowName, FlowMeta meta, ResultRegistry registry = null) { if (string.IsNullOrWhiteSpace(productName) || string.IsNullOrWhiteSpace(flowName)) return null; try { string filePath = Path.Combine(ProductsPath, productName, flowName + FlowExtension); // 统一 .aas if (!File.Exists(filePath)) return null; var tab = new FlowTabItem(flowName); // 应用磁盘上保存的 Tab 元数据(触发类型/循环次数/画布尺寸/循环间隔);meta 为 null 时用默认值 if (meta != null) { tab.TriggerType = (TriggerType)meta.TriggerType; tab.LoopCount = meta.LoopCount; tab.CanvasWidth = meta.CanvasWidth; tab.CanvasHeight = meta.CanvasHeight; tab.LoopIntervalMs = meta.LoopIntervalMs; } FlowGraph result = FlowFileStore.Load(filePath); if (result != null && tab.EditorVm != null) { var graph = tab.EditorVm.Graph; // 绑定结果注册表:运行态=Main,编辑/调试态=Debug graph.Registry = registry ?? ResultRegistry.Debug; graph.Registry.ClearFlow(graph.GraphId); result.GraphId = graph.GraphId; result.GraphName = graph.GraphName; var idMap = new Dictionary(); foreach (var item in result.Nodes) { string oldNodeId = item.NodeId; item.InitializeNode(graph.GraphId, graph.GraphName, graph.Registry); if (!string.IsNullOrEmpty(oldNodeId)) idMap[oldNodeId] = item.NodeId; } if (result.Connections != null && idMap.Count > 0) { foreach (var conn in result.Connections) { if (idMap.TryGetValue(conn.SourceNodeId, out var newSrcId)) conn.SourceNodeId = newSrcId; if (idMap.TryGetValue(conn.TargetNodeId, out var newTgtId)) conn.TargetNodeId = newTgtId; } } graph.Nodes.Clear(); foreach (var n in result.Nodes) graph.Nodes.Add(n); graph.Connections.Clear(); foreach (var c in result.Connections) graph.Connections.Add(c); tab.EditorVm.GraphDataChanged?.Invoke(graph, null); tab.EditorVm?.MarkClean(); // 刚加载的内容视为"已保存"状态 } return tab; } catch { return null; } } public List LoadAllFlows(string productName, ResultRegistry registry = null) { var tabs = new List(); var flowFiles = GetFlowFiles(productName); // 读一次元数据字典,按 Name 应用到对应 Tab(兼容老产品目录:无文件则全用默认值) var metaMap = LoadFlowMetaMap(productName); foreach (var flowName in flowFiles) { metaMap.TryGetValue(flowName, out var meta); if (meta != null) AppLogger.Info($"LoadAllFlows: {flowName} 找到 meta,TriggerType={meta.TriggerType}", nameof(ProductManager)); else AppLogger.Info($"LoadAllFlows: {flowName} 未找到 meta,使用默认值", nameof(ProductManager)); var tab = LoadFlow(productName, flowName, meta, registry); if (tab != null) { AppLogger.Info($"LoadAllFlows: {flowName} 加载完成,TriggerType={tab.TriggerType}", nameof(ProductManager)); tabs.Add(tab); } } return tabs; } public string GetGlobalVarsPath(string productName) => Path.Combine(ProductsPath, productName, "global_vars.json"); /// /// 当前内存里的全局变量归属哪个产品。加载快照/切换产品时更新。 /// 内存变量是进程级共享的,编辑态与运行态都可能改它, /// 只按 CurrentProductName 存会存错产品(例如主页已切到运行产品但编辑态还停在另一个)。 /// public string GlobalVarsOwnerProduct { get; private set; } = ""; /// /// 保存“当前归属产品”的全局变量快照。 /// 产品切换前、以及设置页手动保存时调用;无归属产品时回退到编辑态当前产品。 /// public bool SaveGlobalVarsForOwner() { var owner = !string.IsNullOrEmpty(GlobalVarsOwnerProduct) ? GlobalVarsOwnerProduct : CurrentProductName; return SaveGlobalVarsFor(owner); } /// /// 把 Product 作用域变量保存到指定产品的快照文件(Products\<名>\global_vars.json)。 /// 产品切换前、以及产品变量弹窗点确定时调用。 /// public bool SaveGlobalVarsFor(string productName) { if (string.IsNullOrEmpty(productName)) return false; try { GlobalVariableManager.Instance.SaveSnapshot(GetGlobalVarsPath(productName)); GlobalVarsOwnerProduct = productName; return true; } catch { return false; } } /// /// 从目标产品快照加载 Product 作用域变量到 GlobalVariableManager(Global 子集不受影响)。 /// 快照不存在时清空 Product 子集(切到新产品应从空开始,而非继承上个产品的变量)。 /// Variables 是 ObservableCollection 绑定 UI,必须在 UI 线程调用。 /// public bool LoadProductGlobalVars(string productName) { if (string.IsNullOrEmpty(productName)) return false; try { GlobalVarsOwnerProduct = productName; return GlobalVariableManager.Instance.LoadSnapshot(GetGlobalVarsPath(productName)); } catch { return false; } } /// /// 释放一组 FlowTab 持有的资源:节点插件、结果字典、图节点引用。 /// 切换产品 / 卸载运行产品 / 清理编辑态时统一调用。 /// 安全保证:跳过仍被 FlowRunner.RunTabs 持有的运行态 Tab—— /// 监控模式下编辑器的 FlowTabs 挂的就是这批对象,若在此清空会导致 /// 切换产品时正在运行的流程画布变空、主页面运行也被打断。 /// internal static void DisposeFlowTabs(IEnumerable tabs) { if (tabs == null) return; var runTabs = FlowRunner.Instance.RunTabs; foreach (var tab in tabs) { if (tab == null) continue; if (runTabs != null && runTabs.Contains(tab)) continue; // 1. 释放节点插件(相机句柄、连接流、大对象等) if (tab.Graph != null) { foreach (var node in tab.Graph.Nodes) { try { node.PluginModel?.Dispose(); } catch { /* 单个插件释放失败不影响整体 */ } try { node.ResultItems?.Clear(); node.ExecutionHistory?.Clear(); } catch { } } // 2. 清掉该流程绑定的 ResultRegistry 实例中本流程的全部结果 // (运行态图 → Main,编辑/调试态图 → Debug,互不干扰) if (!string.IsNullOrEmpty(tab.Graph.GraphId)) tab.Graph.Registry?.ClearFlow(tab.Graph.GraphId); tab.Graph.Nodes.Clear(); tab.Graph.Connections.Clear(); } // 3. 清 EditorVm 引用 if (tab.EditorVm != null) { try { tab.EditorVm.SelectedNode = null; } catch { } } } // 释放完成后强制全代 GC,确保插件引用的大对象被回收 GC.Collect(2, GCCollectionMode.Forced, true); GC.WaitForPendingFinalizers(); GC.Collect(2, GCCollectionMode.Forced, true); } /// /// 编辑模式:切换产品进行编辑(不影响主程序运行)。 /// 注意:不再自动保存——是否保存由调用方(ProductViewModel)根据脏标记提示用户确认。 /// public void SwitchProduct(string productName, FlowEditorShellViewModel shellVm) { if (shellVm == null) return; // 切换前:保存当前产品的全局变量快照和流程元数据 SaveGlobalVarsForOwner(); SaveFlowMetaMap(CurrentProductName, shellVm.FlowTabs); // 释放旧产品的资源(节点插件 Dispose + ResultRegistry 清理 + 图引用解除) DisposeFlowTabs(shellVm.FlowTabs); // 切换编辑产品:完全清空 Debug(含上一产品各流程与子流程残留),避免跨产品数据堆积 ResultRegistry.Debug.ClearAllFlows(); shellVm.FlowTabs.Clear(); CurrentProductName = productName; ActiveEditorShell = shellVm; if (!string.IsNullOrEmpty(productName)) { var flows = LoadEditorTabs(productName); foreach (var flow in flows) { shellVm.FlowTabs.Add(flow); } } if (shellVm.FlowTabs.Count == 0) { var defaultTab = new FlowTabItem("流程1"); shellVm.FlowTabs.Add(defaultTab); } shellVm.ActiveTab = shellVm.FlowTabs.FirstOrDefault(); // 加载目标产品的全局变量快照(Variables 是 ObservableCollection,UI 线程同步修改) LoadProductGlobalVars(productName); // 切换产品后刷新监控选项可用性 + 自动进入/退出监控(运行产品且运行中→默认监控) shellVm.ApplyMonitorAvailability(); // 强制全代 GC + 等待终结器,确保 LOH 大对象(图像/位图源/CogRecord 等)被回收 GC.Collect(2, GCCollectionMode.Forced, true); GC.WaitForPendingFinalizers(); GC.Collect(2, GCCollectionMode.Forced, true); } /// /// 编辑模式:切换产品(异步版)。文件 IO(加载新产品)在后台线程执行, /// FlowTabs 等绑定 UI 的集合变更始终留在调用线程(UI 线程)。 /// 不自动保存;是否保存由调用方在切换前询问用户。 /// public async System.Threading.Tasks.Task SwitchProductAsync(string productName, FlowEditorShellViewModel shellVm) { if (shellVm == null) return; // 切换前:在 UI 线程保存当前产品全局变量快照和流程元数据 SaveGlobalVarsForOwner(); SaveFlowMetaMap(CurrentProductName, shellVm.FlowTabs); // 释放旧产品的资源(节点插件 Dispose + ResultRegistry 清理 + 图引用解除) DisposeFlowTabs(shellVm.FlowTabs); // 切换编辑产品:完全清空 Debug(含上一产品各流程与子流程残留),避免跨产品数据堆积 ResultRegistry.Debug.ClearAllFlows(); shellVm.FlowTabs.Clear(); // 后台线程:解析目标产品流程(始终读盘,监控挂接由 Shell 主动控制) List flows = null; await System.Threading.Tasks.Task.Run(() => { if (!string.IsNullOrEmpty(productName)) flows = LoadEditorTabs(productName); }); // 调用线程(UI):应用集合变更 shellVm.FlowTabs.Clear(); CurrentProductName = productName; ActiveEditorShell = shellVm; if (flows != null) { foreach (var flow in flows) shellVm.FlowTabs.Add(flow); } if (shellVm.FlowTabs.Count == 0) shellVm.FlowTabs.Add(new FlowTabItem("流程1")); shellVm.ActiveTab = shellVm.FlowTabs.FirstOrDefault(); // UI 线程:加载目标产品全局变量快照 LoadProductGlobalVars(productName); // 切换产品后刷新监控选项可用性 + 自动进入/退出监控(运行产品且运行中→默认监控) shellVm.ApplyMonitorAvailability(); // 强制全代 GC + 等待终结器,确保 LOH 大对象(图像/位图源/CogRecord 等)被回收 GC.Collect(2, GCCollectionMode.Forced, true); GC.WaitForPendingFinalizers(); GC.Collect(2, GCCollectionMode.Forced, true); } /// /// 运行模式:加载产品作为当前运行程序(从编辑显式加载) /// public void LoadProductToRun(string productName) { // 切换前先把上一个归属产品的全局变量落盘: // 设置页改的值只存在内存里,不先存就会随产品切换丢失 if (!string.IsNullOrEmpty(GlobalVarsOwnerProduct) && GlobalVarsOwnerProduct != productName) SaveGlobalVarsForOwner(); RunProductName = productName; // 主页加载运行产品:同步加载该产品的全局变量快照(与编辑模式一致) LoadProductGlobalVars(productName); } /// /// 基于序号切换产品(供外部调用,如 PLC 触发)。 /// 产品切换经 LoadRunProduct → LoadProductGlobalVars 自动载入该产品的 Product 变量, /// 故此处无需再单独切换全局变量配方(配方机制已退休)。 /// public bool SwitchProductBySequence(int sequence) { var meta = GetProductBySequence(sequence); if (meta == null) return false; RunProductName = meta.Name; // 加载为运行产品(FlowRunner 会复制一份运行态流程,不影响编辑态) TeamAAS.FlowEngine.Execution.FlowRunner.Instance.LoadRunProduct(meta.Name); return true; } private static string SanitizeName(string name) => TeamAAS.PathHelper.SanitizeFileName(name); } }