Skip to Content
技术文档10 · 开发者扩展自定义节点开发说明
本页目录

自定义节点开发说明

本文面向需要扩展规则引擎节点的开发人员和交付工程师。自定义节点不是采集协议插件,也不是数据转发插件,而是规则流程里的“功能块”:上游输出变化后写入当前节点输入,当前节点执行 ChangedAsync,再把输出传给下游节点。

阅读路线

基础节点与示例 → 生命周期与参数契约 → 排障与上线

首次开发先验证一个最小例,再按参考补全参数、失败处理和释放流程。MCP 材料入口位于页末,可按需展开。

开发前准备

先判断是不是自定义节点

需求建议选型
需要把多个变量、报警、设备状态编排成联锁、告警、推送或控制流程自定义节点。
只是在变量读入或写出前做比例换算、枚举转换、字符串解析数据转换脚本,见 脚本开发说明。
需要在内存变量里按表达式计算一个派生点内存变量脚本。
需要新接一种 PLC、仪表或协议采集采集插件,见 采集插件二开说明。
需要把数据上传到新的平台、数据库或协议服务端业务插件,见 业务插件二开说明。

源码入口

源码作用
TGateway.Application/Expressions/CustomExpressionBase.cs自定义节点基类,定义 Name、InitAsync、ChangedAsync、OutputsCommitted、SetOutput 和 CommitOutputs。
TGateway.Application/Expressions/CustomExpressionDefinition.csWEB 节点参数定义、参数方向、数据类型和生成属性规则。
TGateway.Application/Controllers/RuleEngine/GatewayCustomNodeController.cs自定义节点创建、保存、编译、批量编译、删除和热加载接口。
TGateway.Application/Entity/CustomNode.cs自定义节点数据库表,保存名称、分类、描述、代码和三类参数 JSON。
TGateway.ScriptCompiler/ExpressionCodeGenerator.cs把 WEB 节点代码包装成继承 CustomExpressionBase 的 C# 类。
TGateway.ExpressionsGenerator/SourceGenerator/ExpressionRegistrationGenerator.cs编译时扫描节点类,生成注册代码并写入 ExpressionsData。
TGateway.Application/Expressions/ExpressionsData.cs保存已注册节点信息和创建、读写、初始化、变化执行委托。
TGateway.Application/Task/RuleEngine/RuleEngineTask.cs规则流程运行时,负责创建节点实例、写入参数、初始化、输出传播、防抖和释放。
TGateway.Application/Script/EmbeddedNodes.*.cs、Script/Timers/内置数学、逻辑、比较、数据流、定时器、计数器和统计节点参考实现。
TGateway.Application/Script/VariableNode.cs内置变量通知、报警通知、设备通知、变量 RPC、MQTT/邮件/Webhook 推送节点参考实现。

生命周期与参数契约

总生命周期

  1. 在 WEB “开发配置 → 自定义节点”中新建节点,维护名称、分类、描述、代码、输入参数、输出参数和输入输出参数。
  2. 点击“编译并保存”后,Runtime 先把节点写入 custom_node 表。
  3. Runtime 调用 TGateway.ScriptCompiler,编译类型为 CustomNode。
  4. 编译器使用节点稳定类型身份,把页面代码和参数属性包装成 CustomExpressionBase 派生类;数据库节点身份为 custom-node:<Id>,显示名称不再用作节点的唯一身份。
  5. 编译产物先写入 CustomNodeDlls 下的独立暂存目录。完成装载、类型身份及整批注册验证后,才替换对应正式 DLL 并发布注册;失败保留原正式结果,最后清理暂存目录。
  6. 源生成器在 DLL 中生成 ModuleInitializer 注册代码。装载期间先暂存本程序集的注册项,验证成功后整批发布到 ExpressionsData。
  7. 规则流程启动前校验 DefinitionJson:稳定节点 typeId、命名端口、参数契约及无环拓扑。画布布局只用于呈现,不决定执行。
  8. 每个节点按 typeId 从当前节点目录绑定注册信息并创建实例。
  9. 运行时把定义中的输入、输入输出初值和节点参数写入实例,绑定当前流程的共享上下文与取消令牌。
  10. 运行时注入流程日志 Logger,设置批次接收回调,再调用所有节点的 InitAsync。
  11. 没有上游连线的起始节点会执行一次 ChangedAsync。
  12. 节点在受管执行内写入输出,成功结束后提交批次;同批状态先一起应用到目标,再触发目标调度。事件按事件语义消费。
  13. 默认 10 ms 防抖;NoDebounce 取消合并等待,但仍有串行执行、队列容量及拒绝处理。
  14. 流程停止或重载时取消生命周期令牌,等待调度结束,调用 StopAsync 并处理节点释放接口;新流程实例不继承旧内存状态。

节点编辑器保存的是草稿,编译发布的是运行版本。保存成功但编译失败时,重新打开仍可看到失败草稿;不能因此认为新逻辑已替换在线版本。已有流程持有的节点实例还应重新装载并验证。来自旧编辑版本的更新或编译请求会被拒绝,先读取当前节点再核对修改。

编辑器支持用户成员补全、XML 注释、参数提示和诊断行列定位,完整操作及可复制的 XML 示例见代码编辑、补全与错误定位。

基类契约

节点必须实现 Name、InitAsync、ChangedAsync。下面列出可用于扩展的主要契约;宿主内部回调不应由节点程序集直接访问。

成员开发要求
Name / [RuleNode]Name 是显示名称;完整类使用 [RuleNode("稳定唯一ASCII键")] 声明注册身份。WEB 节点由包装器按数据库身份生成,改显示名不改变类型身份。
InitAsync节点实例创建、参数写入、输出回调设置完成后调用一次。适合初始化输出、订阅事件、创建连接或启动定时器。
ChangedAsync起始节点启动时或上游输入变化后调用。适合读取输入、计算输出、执行写入或推送。
Logger流程专用日志。高频节点不要正常路径刷 Info 日志,只在异常、过滤、关键动作时记录。
OutputsCommitted宿主内部的批次接收入口,自定义节点程序集不能直接访问。完整类节点通过 SetOutput 修改输出;调度执行成功后自动提交,CommitOutputs 可显式提交中间批次。失败会舍弃尚未提交的输出,但不撤回已提交批次。
SmartChangedTriggerScheduler运行时防抖触发器。业务代码通常不直接调用它,由规则引擎负责。
StopAsync停止时的异步清理入口,WEB 成员代码也可覆盖;随后宿主仍处理释放接口。取消订阅、关闭通道与释放应可重复执行。
ExecutionCancellationToken本次节点执行总期限与流程取消的组合令牌。所有异步等待、网络和写入调用应传递它。
LifetimeCancellationToken长期事件订阅和定时任务使用的流程令牌,不绑定某一次已结束的执行。
EnqueueNotification广播或定时回调先固定载荷,再用此入口加入有界串行调度;回调内只做同步输出发布,不传入异步 lambda。
SkipExecution明确报告条件不满足,传稳定原因码和具体原因,调用后立即返回。不能在输出已提交或外部动作已开始后报告跳过。
SetWaitingReason报告空闲时等待条件,不改变最近执行结果;在 SkipExecution 前调用,同时传两个 null 清除说明。
Context同一流程实例的有界 JSON 共享状态,非跨流程或持久化存储。
EffectKind完整类声明纯计算、幂等或非幂等外部副作用;不能用画布参数修改该安全声明。

两种代码形态

WEB 页面节点

在自定义节点编辑器中只写类成员代码,不写完整类,不写 Name 属性,也不要重复声明页面参数。参数属性由参数定义自动生成。

例如参数定义包含小数输入 Input、Scale 和输出 Output,页面只填写以下成员。生成器负责稳定身份、参数属性和调用 SetOutput 的输出 setter:

/// <summary>初始化本实例的换算输出。</summary> public override Task InitAsync() { Output = 0; return Task.CompletedTask; } /// <summary>按当前输入与倍率计算,并交由宿主提交输出。</summary> public override Task ChangedAsync() { Output = Input * Scale; return Task.CompletedTask; }

完整类节点

完整类节点适合内置节点、外部 DLL 和需要声明副作用类别的节点。继承 CustomExpressionBase,声明唯一 [RuleNode],参数用特性标记,输出属性用 SetOutput。注册键保持稳定,不能用显示名称代替。

using System; using System.ComponentModel; using System.Threading.Tasks; using TGateway.Application; /// <summary>把工程量与高限比较,输出当前超限状态。</summary> [Category("比较运算")] [RuleNode("manual.high-limit")] public sealed class HighLimitNode : CustomExpressionBase { /// <summary>供节点库和流程面板显示的名称。</summary> public override string Name => "高限判断"; /// <summary>待比较的工程量。</summary> [ExpressionInput] public double Input { get; set; } /// <summary>采用与输入相同单位的高限。</summary> [ExpressionInput] public double Limit { get; set; } = 100; /// <summary>保存最近设置的超限状态。</summary> private bool _alarm; /// <summary>由受管输出批次传播的比较结果。</summary> [ExpressionOutput] public bool Alarm { get => _alarm; private set => SetOutput(ref _alarm, value); } /// <summary>为新实例建立明确的初始状态。</summary> public override Task InitAsync() { Alarm = false; return Task.CompletedTask; } /// <summary>每次执行重新计算严格大于关系。</summary> public override Task ChangedAsync() { Alarm = Input > Limit; return Task.CompletedTask; } }

参数模型

参数方向现场含义生成属性连线位置注意事项
输入参数上游传入或规则实例面板手填的值。[ExpressionInput] public set左侧命名端口,如 in:Input。写入输入本身不发布输出,在受管执行中计算并设置输出。
输出参数当前节点计算、过滤、写入或推送后的结果。[ExpressionOutput] private set右侧命名端口,如 out:Result。页面直接赋输出属性;完整类用 SetOutput。
输入输出参数接收上游并可向下游传播。[ExpressionInOut] public set同名左右端口,如 in:Value、out:Value。区分宿主输入写入与节点主动发布,不能只修改字段。
节点参数容量、策略及执行策略等配置。[ExpressionParameter]属性面板,无连线端口。不把配置参数当成事件或业务输入。

数据类型

WEB 参数定义支持 Int16、Int32、Int64、UInt16、UInt32、UInt64、Byte、Float、Double、Decimal、Boolean、String、Object、DateTime。端口还具有事件语义、可空性、数组维数/元素及单位等契约;连线不能依靠运行时隐式转换来掩盖不匹配,应使用对应转换节点。

类型C# 类型适合场景初始值写法
Booleanbool启停、联锁、报警状态、边沿信号。true 或 false。
Int32 / Int64int / long计数、编号、毫秒时间、枚举值。1000。
Float / Double / Decimalfloat / double / decimal模拟量、工程量、比例换算。1.5,decimal 完整类中可写 1.5m。
Stringstring设备名、变量名、Topic、URL、模板、告警文本。WEB 中通常在规则实例属性面板填写。
DateTimeDateTime时间戳、窗口期、延时判断。DateTime.UtcNow 只适合完整类默认值;页面实例建议手填。
Objectobject 或自定义类型VariableBasicData、AlarmVariable、字典、匿名对象、JSON 对象。连接上游对象输出最稳;手填 JSON 时要确认目标类型能转换。

WEB 参数定义可填写状态参数的“初始值表达式”,例如小数 1m;事件端口不允许初值。输出和内部状态在 InitAsync 中初始化,流程实例输入在规则属性面板配置。三处作用不同,不能互相代替。

运行时传播规则

规则说明
状态与事件分别传播状态默认按已提交值去重;OutputOnlyOnChange=false 时每次写入都可传播。事件端口每次写入代表一次发生,相同载荷也保留事件语义。
空值受契约约束null 必须符合端口的可空声明,不能静默丢弃空值后继续使用旧输入;JsonElement 的 Null 与未初始化的 Undefined 也应区分。
端口按名称绑定定义保存稳定 typeId 与端口键;参数排序不作为运行身份。重命名参数属于契约变化,必须同步修改使用流程并校验。
起始节点会执行一次没有上游连线的节点在流程启动后执行一次 ChangedAsync,常量、周期源、事件源都可以作为起始节点。
默认 10 ms 防抖多个上游短时间连续输出时,下游可能合并执行。需要逐次触发时在流程中开启“无限制触发”,但要评估 CPU 和外部系统压力。
环路在校验阶段拒绝当前流程必须是有向无环图。需要状态记忆时使用保持、计数、上下文或独立内存变量。
输出属于受管作用域初始化和调度执行中暂存输出;失败、取消、超时或显式跳过舍弃未提交部分。CommitOutputs 已发布的中间批次不会回滚。
外部通知先排队事件和定时回调不能在已结束的执行外直接设置输出;先冻结值,再使用 EnqueueNotification。不要脱离 ChangedAsync 启动后台任务后写输出。
分支独立投递一条连接拒绝不阻止其他目标接收;部分投递失败不回滚已接收分支。通过连接观察逐条检查。
停止时释放实例使用流程令牌终止长期任务,在 StopAsync 和释放接口中解除事件订阅、关闭连接和释放定时器。

SkipExecution 只用于本次根本不执行的条件分支,普通 return 不会自动变成 Skipped。例如输入 Enabled 为假时,可先报告等待启用,再明确跳过;最近执行结果与当前空闲条件分别显示。

/// <summary>初始化等待条件,不在启动时产生业务输出。</summary> public override Task InitAsync() => Task.CompletedTask; /// <summary>仅在启用时计算,禁用时报告明确的未执行原因。</summary> public override Task ChangedAsync() { if (!Enabled) { SetWaitingReason("WaitingForEnabled", "等待启用"); SkipExecution("Disabled", "当前节点未启用"); return Task.CompletedTask; } SetWaitingReason(null, null); Output = Input * 2m; return Task.CompletedTask; }

此 WEB 示例定义布尔输入 Enabled、小数输入 Input 与小数输出 Output。跳过不会撤回上次显示值,应以本次执行状态、原因与连接观察判断是否新发布。

基础节点与示例

内置节点类型清单

以下类型已从源码 EmbeddedNodes.*.cs 和 VariableNode.cs 核对。开发新节点时先看同类内置实现,避免重复造基础节点。

类型内置节点
数学运算加法、减法、乘法、除法、取模、幂运算、平方根、绝对值、四舍五入、数值钳制、线性缩放。
逻辑运算逻辑与、逻辑或、逻辑非、逻辑异或、逻辑与非、逻辑或非。
比较运算等于、不等于、大于、大于等于、小于、小于等于、范围内判断。
条件判断条件选择、条件选择(字符串)、阈值触发、多路分支。
字符串处理字符串拼接、字符串格式化、字符串截取、字符串替换、转大写、转小写、去除空格、包含判断、字符串长度。
类型转换转整数、转浮点数、转字符串、转布尔值、JSON 数值提取、整数转十进制、JSON/布尔状态与事件互转。
定时器延时、单次定时器、周期定时器。
计数器计数器、加减计数器。
边沿检测上升沿检测、下降沿检测、双边沿检测。
数据统计平均值、移动平均、最大最小值、累加器。
变化检测变化检测、变化率。
保持值保持、采样保持、触发器。
常量数值常量、字符串常量、布尔常量。
三角函数正弦、余弦、正切、反正切2。
触发器变量通知规则、告警通知规则、设备通知规则。
变量 RPC 节点变量 RPC 节点。
数据推送MQTT 客户端上传、邮件推送、Webhook 推送。
数据流状态合并、事件拆分、事件批处理、事件汇合、上下文写入/读取/删除。
控制流执行错误捕获,忽略成功和显式跳过,只向错误分支传播失败或取消结果。

Demo 写法约定

下面的“页面参数”按自定义节点编辑器的三类参数填写。除“完整类 Demo”外,代码都粘贴到 WEB 自定义节点代码编辑区,不要额外写类名、命名空间或 Name 属性。

常量节点

查看常量节点的参数与示例

页面参数:

方向名称类型说明
输入ValueDouble在规则流程节点属性面板填写的常量值。
输出OutputDouble输出给下游的常量值。
public override Task InitAsync() { Output = Value; return Task.CompletedTask; } public override Task ChangedAsync() { Output = Value; return Task.CompletedTask; }

数学运算节点

查看数学运算节点的参数与示例

页面参数:

方向名称类型说明
输入RawValueDouble原始量程值。
输入RawMinDouble原始下限。
输入RawMaxDouble原始上限。
输入EngMinDouble工程下限。
输入EngMaxDouble工程上限。
输出ValueDouble换算后的工程值。
输出ErrorBoolean量程配置是否错误。
public override Task InitAsync() { Value = 0; Error = false; return Task.CompletedTask; } public override Task ChangedAsync() { if (Math.Abs(RawMax - RawMin) < 0.000001) { Error = true; return Task.CompletedTask; } Error = false; Value = Math.Round((RawValue - RawMin) * (EngMax - EngMin) / (RawMax - RawMin) + EngMin, 3); return Task.CompletedTask; }

逻辑运算节点

查看逻辑运算节点的参数与示例

页面参数:

方向名称类型说明
输入AutoModeBoolean自动模式。
输入EmergencyStopBoolean急停状态。
输入PressureOkBoolean压力允许。
输入FaultBoolean故障状态。
输出AllowedBoolean是否允许启动。
public override Task InitAsync() { Allowed = false; return Task.CompletedTask; } public override Task ChangedAsync() { Allowed = AutoMode && !EmergencyStop && PressureOk && !Fault; return Task.CompletedTask; }

比较和阈值节点

查看比较和阈值节点的参数与示例

页面参数:

方向名称类型说明
输入PVDouble过程值。
输入HighLimitDouble高限值。
输入HysteresisDouble回差,防止临界点抖动。
输出AlarmBoolean当前是否报警。
输出RisingEdgeBoolean报警刚发生。
输出FallingEdgeBoolean报警刚恢复。
private bool _active; public override Task InitAsync() { _active = false; Alarm = false; RisingEdge = false; FallingEdge = false; return Task.CompletedTask; } public override Task ChangedAsync() { var previous = _active; if (!_active && PV >= HighLimit) { _active = true; } else if (_active && PV <= HighLimit - Math.Abs(Hysteresis)) { _active = false; } Alarm = _active; RisingEdge = !previous && _active; FallingEdge = previous && !_active; return Task.CompletedTask; }

条件选择节点

查看条件选择节点的参数与示例

页面参数:

方向名称类型说明
输入UseManualBoolean是否使用手动给定值。
输入ManualValueDouble手动给定值。
输入AutoValueDouble自动计算值。
输出OutputDouble最终输出值。
输出SourceString输出来源。
public override Task InitAsync() { Output = 0; Source = "Auto"; return Task.CompletedTask; } public override Task ChangedAsync() { Output = UseManual ? ManualValue : AutoValue; Source = UseManual ? "Manual" : "Auto"; return Task.CompletedTask; }

字符串处理节点

查看字符串处理节点的参数与示例

页面参数:

方向名称类型说明
输入DeviceNameString设备名。
输入VariableNameString变量名。
输入ValueTextString当前值文本。
输入UnitString单位。
输出MessageString拼好的提示文本。
public override Task InitAsync() { Message = string.Empty; return Task.CompletedTask; } public override Task ChangedAsync() { var unit = string.IsNullOrWhiteSpace(Unit) ? string.Empty : $" {Unit}"; Message = $"{DeviceName}.{VariableName} = {ValueText}{unit}"; return Task.CompletedTask; }

类型转换节点

查看类型转换节点的参数与示例

页面参数:

方向名称类型说明
输入InputTextString外部文本值。
输出ValueDouble解析后的数值。
输出SuccessBoolean是否解析成功。
输出ErrorMessageString错误信息。
public override Task InitAsync() { Value = 0; Success = false; ErrorMessage = string.Empty; return Task.CompletedTask; } public override Task ChangedAsync() { if (double.TryParse(InputText, out var number)) { Value = number; Success = true; ErrorMessage = string.Empty; } else { Success = false; ErrorMessage = $"Cannot convert to number: {InputText}"; } return Task.CompletedTask; }

定时器节点

查看定时器节点的参数与示例

这个 WEB 示例在本次受管执行内等待,完成后产生一次 Done 事件。将 Done 定义为事件输出、不填初值。总超时应大于 DelayMs;等待期间后续输入由调度器排队,不会抢占当前等待。

页面参数:

方向名称类型说明
输入TriggerBoolean触发信号。
输入DelayMsInt32延时时间,毫秒。
输出OutputBoolean延时完成输出。
事件输出DoneBoolean每次延时完成产生 true 事件。
/// <summary>初始化状态输出,不产生完成事件。</summary> public override Task InitAsync() { Output = false; return Task.CompletedTask; } /// <summary>在当前执行的取消与期限约束内等待,再提交本次结果。</summary> public override async Task ChangedAsync() { if (!Trigger) { SetWaitingReason("WaitingForTrigger", "等待触发信号"); SkipExecution("TriggerOff", "触发信号为假"); return; } if (DelayMs < 0) throw new RuleBusinessRejectedException("InvalidDelay", "DelayMs must be non-negative."); SetWaitingReason(null, null); var delay = DelayMs; await Task.Delay(delay, ExecutionCancellationToken).ConfigureAwait(false); Output = true; Done = true; }

计数器节点

查看计数器节点的参数与示例

页面参数:

方向名称类型说明
输入IncrementBoolean计数脉冲。
输入ResetBoolean复位。
输入MaxValueInt32上限。
输出CountInt32当前计数。
输出OverflowBoolean是否达到上限。
private bool _lastIncrement; private int _count; public override Task InitAsync() { _lastIncrement = false; _count = 0; Count = 0; Overflow = false; return Task.CompletedTask; } public override Task ChangedAsync() { if (Reset) { _count = 0; } else if (Increment && !_lastIncrement) { _count++; } _lastIncrement = Increment; Overflow = MaxValue > 0 && _count >= MaxValue; Count = Overflow && MaxValue > 0 ? MaxValue : _count; return Task.CompletedTask; }

边沿检测节点

查看边沿检测节点的参数与示例

页面参数:

方向名称类型说明
输入InputBoolean当前开关量。
输出RisingBoolean上升沿。
输出FallingBoolean下降沿。
输出ChangedBoolean任意变化。
private bool _previous; public override Task InitAsync() { _previous = Input; Rising = false; Falling = false; Changed = false; return Task.CompletedTask; } public override Task ChangedAsync() { Rising = Input && !_previous; Falling = !Input && _previous; Changed = Input != _previous; _previous = Input; return Task.CompletedTask; }

数据统计节点

查看数据统计节点的参数与示例

页面参数:

方向名称类型说明
输入InputDouble当前采样值。
输入WindowSizeInt32滑动窗口大小。
输出AverageDouble滑动平均值。
输出SampleCountInt32当前样本数量。
private readonly Queue<double> _samples = new(); public override Task InitAsync() { _samples.Clear(); Average = 0; SampleCount = 0; return Task.CompletedTask; } public override Task ChangedAsync() { var size = Math.Max(1, WindowSize); _samples.Enqueue(Input); while (_samples.Count > size) { _samples.Dequeue(); } SampleCount = _samples.Count; Average = Math.Round(_samples.Average(), 3); return Task.CompletedTask; }

变化检测节点

查看变化检测节点的参数与示例

页面参数:

方向名称类型说明
输入InputDouble当前值。
输入ThresholdDouble变化判定阈值。
输出ChangedBoolean是否超过阈值。
输出DeltaDouble本次变化量。
输出RatePerSecondDouble每秒变化率。
private double _lastValue; private DateTime _lastTime; public override Task InitAsync() { _lastValue = Input; _lastTime = DateTime.UtcNow; Changed = false; Delta = 0; RatePerSecond = 0; return Task.CompletedTask; } public override Task ChangedAsync() { var now = DateTime.UtcNow; var seconds = Math.Max(0.001, (now - _lastTime).TotalSeconds); var delta = Input - _lastValue; Delta = Math.Round(delta, 3); RatePerSecond = Math.Round(delta / seconds, 3); Changed = Math.Abs(delta) >= Math.Abs(Threshold); _lastValue = Input; _lastTime = now; return Task.CompletedTask; }

保持节点

查看保持节点的参数与示例

页面参数:

方向名称类型说明
输入InputDouble当前值。
输入SampleBoolean采样信号。
输入HoldBoolean保持信号。
输出OutputDouble输出值。
private double _held; public override Task InitAsync() { _held = Input; Output = Input; return Task.CompletedTask; } public override Task ChangedAsync() { if (Sample) { _held = Input; } Output = Hold ? _held : Input; return Task.CompletedTask; }

三角函数节点

查看三角函数节点的参数与示例

页面参数:

方向名称类型说明
输入AngleDouble角度或弧度。
输入UseDegreesBooleantrue 表示输入为角度。
输出SinValueDouble正弦值。
输出CosValueDouble余弦值。
public override Task InitAsync() { SinValue = 0; CosValue = 1; return Task.CompletedTask; } public override Task ChangedAsync() { var radians = UseDegrees ? Angle * Math.PI / 180.0 : Angle; SinValue = Math.Round(Math.Sin(radians), 6); CosValue = Math.Round(Math.Cos(radians), 6); return Task.CompletedTask; }

Object 结构化数据节点

查看Object 结构化数据节点的参数与示例

页面参数:

方向名称类型说明
输入InputDataObject上游传入的对象,例如变量通知输出的 VariableBasicData。
输出JsonString序列化后的 JSON。
输出HasDataBoolean是否有有效数据。
public override Task InitAsync() { Json = string.Empty; HasData = false; return Task.CompletedTask; } public override Task ChangedAsync() { HasData = InputData != null; Json = InputData == null ? string.Empty : InputData.ToSystemTextJsonString(); return Task.CompletedTask; }

事件触发示例

完整类 Demo:变量通知触发节点

这个完整类把在线变量变化变成流程输入。广播线程先筛选并固定快照,再入受管调度;状态 Value 去重,事件 Data 保留每次通知。数值接入比较节点前先按端口语义转换,再提取数值。

查看完整示例代码

using System; using System.ComponentModel; using System.Text.Json; using System.Threading.Tasks; using TGateway.Application; /// <summary>将匹配变量的冻结快照交给规则调度器消费。</summary> [Category("触发器")] [RuleNode("manual.variable-notification")] public sealed class SimpleVariableNotifyNode : CustomExpressionBase, IDisposable { /// <summary>用于节点库显示,不参与类型身份匹配。</summary> public override string Name => "变量变化触发"; /// <summary>限定通知设备,空字符串表示不限制。</summary> [ExpressionInput] public string DeviceName { get; set; } = string.Empty; /// <summary>限定变量名称,空字符串表示不限制。</summary> [ExpressionInput] public string VariableName { get; set; } = string.Empty; /// <summary>保存与运行时对象解耦的值快照。</summary> private JsonElement _value; /// <summary>发布当前值状态,相同已提交值不重复传播。</summary> [ExpressionOutput] public JsonElement Value { get => _value; private set => SetOutput(ref _value, value); } /// <summary>保存本次完整变量快照。</summary> private JsonElement _data; /// <summary>每次匹配通知都发布一次完整快照事件。</summary> [ExpressionOutput(IsEvent = true)] public JsonElement Data { get => _data; private set => SetOutput(ref _data, value); } /// <summary>流程初始化时订阅,初始化阶段不生成虚假通知。</summary> public override Task InitAsync() { GlobalData.VariableValueChangeEvent += OnVariableChanged; return Task.CompletedTask; } /// <summary>在广播入口固定本次数据,不在广播线程传播规则输出。</summary> private void OnVariableChanged(VariableRuntime runtime, VariableUpdateSource source) { if (!runtime.IsOnline || (!string.IsNullOrWhiteSpace(DeviceName) && runtime.DeviceName != DeviceName) || (!string.IsNullOrWhiteSpace(VariableName) && runtime.Name != VariableName)) return; var data = runtime.AdaptVariableBasicData(); data.UpdateSource = source; var snapshot = JsonSerializer.SerializeToElement(data); var value = JsonSerializer.SerializeToElement(data.Value); EnqueueNotification(() => { Data = snapshot; Value = value; }, nameof(Data)); } /// <summary>通知源没有需要在输入变化时重复执行的业务。</summary> public override Task ChangedAsync() => Task.CompletedTask; /// <summary>流程停止后解除全局订阅,防止保留旧实例。</summary> public void Dispose() { GlobalData.VariableValueChangeEvent -= OnVariableChanged; } }

完整类 Demo:报警通知触发节点

查看完整示例代码

using System; using System.ComponentModel; using System.Text.Json; using System.Threading.Tasks; using TGateway.Application; /// <summary>将指定级别与设备的报警快照发布为事件。</summary> [Category("触发器")] [RuleNode("manual.alarm-notification")] public sealed class SimpleAlarmNotifyNode : CustomExpressionBase, IDisposable { /// <summary>节点库显示名称。</summary> public override string Name => "报警变化触发"; /// <summary>最低允许报警级别。</summary> [ExpressionInput] public int MinLevel { get; set; } = 0; /// <summary>限定设备名称,空字符串表示不限制。</summary> [ExpressionInput] public string DeviceName { get; set; } = string.Empty; /// <summary>保存与广播对象解耦的报警快照。</summary> private JsonElement _alarm; /// <summary>每次匹配报警发布一个 JSON 事件。</summary> [ExpressionOutput(IsEvent = true)] public JsonElement Alarm { get => _alarm; private set => SetOutput(ref _alarm, value); } /// <summary>保存报警摘要。</summary> private string _text = string.Empty; /// <summary>发布最近报警的可读摘要状态。</summary> [ExpressionOutput] public string Text { get => _text; private set => SetOutput(ref _text, value); } /// <summary>在流程初始化时建立报警订阅。</summary> public override Task InitAsync() { GlobalData.AlarmChangedEvent += OnAlarmChanged; return Task.CompletedTask; } /// <summary>冻结载荷后入队,避免广播对象后续修改影响当前事件。</summary> private void OnAlarmChanged(AlarmVariable alarm) { if (alarm.AlarmLevel < MinLevel) { return; } if (!string.IsNullOrWhiteSpace(DeviceName) && alarm.DeviceName != DeviceName) { return; } var snapshot = JsonSerializer.SerializeToElement(alarm); var text = $"{alarm.DeviceName}.{alarm.Name} {alarm.EventType} {alarm.AlarmText}"; EnqueueNotification(() => { Alarm = snapshot; Text = text; }, nameof(Alarm)); } /// <summary>只由报警订阅发布业务输出。</summary> public override Task ChangedAsync() => Task.CompletedTask; /// <summary>释放流程实例持有的全局事件订阅。</summary> public void Dispose() { GlobalData.AlarmChangedEvent -= OnAlarmChanged; } }

完整类 Demo:设备状态触发节点

查看完整示例代码

using System; using System.ComponentModel; using System.Text.Json; using System.Threading.Tasks; using TGateway.Application; /// <summary>将设备状态快照交给规则的事件接收端。</summary> [Category("触发器")] [RuleNode("manual.device-notification")] public sealed class SimpleDeviceStatusNode : CustomExpressionBase, IDisposable { /// <summary>节点库显示名称。</summary> public override string Name => "设备状态触发"; /// <summary>限定设备名称,空字符串表示不限制。</summary> [ExpressionInput] public string DeviceName { get; set; } = string.Empty; /// <summary>保存本次设备快照。</summary> private JsonElement _device; /// <summary>每次匹配状态变化发布一次 JSON 事件。</summary> [ExpressionOutput(IsEvent = true)] public JsonElement Device { get => _device; private set => SetOutput(ref _device, value); } /// <summary>保存最近状态的文字表示。</summary> private string _status = string.Empty; /// <summary>发布最近设备状态。</summary> [ExpressionOutput] public string Status { get => _status; private set => SetOutput(ref _status, value); } /// <summary>在当前流程实例中订阅设备变化。</summary> public override Task InitAsync() { GlobalData.DeviceStatusChangeEvent += OnDeviceChanged; return Task.CompletedTask; } /// <summary>广播入口只固定快照并提交通知。</summary> private void OnDeviceChanged(DeviceRuntime runtime, DeviceBasicData data) { if (!string.IsNullOrWhiteSpace(DeviceName) && data.Name != DeviceName) { return; } var snapshot = JsonSerializer.SerializeToElement(data); var status = data.DeviceStatus.ToString(); EnqueueNotification(() => { Device = snapshot; Status = status; }, nameof(Device)); } /// <summary>设备变化由订阅通知处理。</summary> public override Task ChangedAsync() => Task.CompletedTask; /// <summary>解除设备事件订阅,允许旧流程实例释放。</summary> public void Dispose() { GlobalData.DeviceStatusChangeEvent -= OnDeviceChanged; } }

写回、推送与定时示例

变量 RPC 节点

RPC 在现场语言里就是“外部系统或规则流程反写点位”。这个 Demo 用上升沿触发一次写入,避免 Trigger=true 时每次输入变化都重复写。

页面参数:

方向名称类型说明
输入TriggerBoolean写入触发。
输入DeviceNameString设备名。
输入VariableNameString变量名。
输入WriteDataObject写入值。
输出SuccessBoolean是否成功。
输出MessageString结果消息。
/// <summary>记录上一触发状态,只在上升沿执行写入。</summary> private bool _lastTrigger; /// <summary>变量写入不具备可靠的远端幂等确认,禁止不明结果自动重试。</summary> protected override RuleNodeEffectKind EffectKind => RuleNodeEffectKind.NonIdempotentSideEffect; /// <summary>初始化本实例的触发与业务结果。</summary> public override Task InitAsync() { _lastTrigger = false; Success = false; Message = string.Empty; return Task.CompletedTask; } /// <summary>通过正常变量写入链路处理一次上升沿,并保留引擎失败语义。</summary> public override async Task ChangedAsync() { var rising = Trigger && !_lastTrigger; _lastTrigger = Trigger; if (!rising) { SetWaitingReason("WaitingForRisingEdge", "等待写入触发上升沿"); SkipExecution("NoRisingEdge", "本次输入没有写入触发上升沿"); return; } if (!GlobalData.TryGetVariableRuntime(DeviceName, VariableName, out var variable)) throw new RuleBusinessRejectedException("VariableNotFound", $"Variable not found: {DeviceName}.{VariableName}"); SetWaitingReason(null, null); MarkSideEffectStarted(); var result = await variable.RpcAsync( WriteData.ToSystemTextJsonElement(), "rule", ExecutionCancellationToken).ConfigureAwait(false); if (!result.IsSuccess) throw new RuleBusinessRejectedException("VariableRpcRejected", result.ErrorMessage ?? "Variable write was rejected."); MarkSideEffectConfirmed(); Success = true; Message = result.GetOperResult().ToString(); }

数据推送节点

优先使用内置 MQTT、邮件和 Webhook 节点;完整配置见规则引擎。扩展新接收协议时,参考 VariableNode.cs 中的完整类,遵守以下执行契约。

阶段开发要求
声明通过 EffectKind 声明副作用。没有可靠幂等键的发送属于 NonIdempotentSideEffect,不能声明为纯计算。
前置检查无效 URL、缺少凭据等抛出 RuleBusinessRejectedException;正常条件不满足可在动作开始前报告 Skipped。
发出动作请求前调用 MarkSideEffectStarted,把 ExecutionCancellationToken 传入网络 API,并复用受控客户端。
确认结果核对 HTTP 状态及厂商业务码;可确认完成后调用 MarkSideEffectConfirmed,再发布成功时间或结果。
失败与取消不吞异常、不把 Success=false 后正常返回当作引擎失败。明确拒绝抛业务拒绝,取消继续上抛;发出动作后结果不明由引擎归类 UnknownEffect。
重试总期限覆盖全部尝试和退避。仅明确瞬时故障可申请有限重试;非幂等动作发出后不能因超时自动重复发送。
释放停止时关闭生产准入、等待在途工作结束,再释放连接。日志只保留诊断信息,隐藏 URL 中的令牌、密码与请求头凭据。

每个节点都有 ExecutionResult 事件输出,包含成功、显式跳过、拒绝、故障、超时、取消或结果未确认等状态。需要错误支路时接“执行错误捕获”,其忽略成功与显式跳过。业务结果与执行事件分别连接。

完整类 Demo:周期定时器

此示例使用 TUtility.TimerX,周期是节点配置参数,修改后保存并重新装载流程。定时回调只提交受管通知,Tick 是事件,连续发布 true 仍表示不同周期;Enabled=false 时不发布、不计数。需要动态周期或 Cron 时直接参考内置 Script/Timers/IntervalTimerNode.cs。

查看完整示例代码

#nullable enable using System; using System.ComponentModel; using System.Threading.Tasks; using TGateway.Application; using TUtility; /// <summary>以固定周期产生事件,由宿主串行发布输出。</summary> [Category("定时器")] [RuleNode("manual.periodic-pulse")] public sealed class PeriodicPulseNode : CustomExpressionBase, IDisposable { /// <summary>节点库显示名称。</summary> public override string Name => "周期脉冲"; /// <summary>保存时固定的周期毫秒数,重新装载后生效。</summary> [ExpressionParameter] public int IntervalMs { get; set; } = 1000; /// <summary>允许当前周期发布事件。</summary> [ExpressionInput] public bool Enabled { get; set; } = true; /// <summary>保存最近一次周期事件载荷。</summary> private bool _tick; /// <summary>每次周期发布真事件,不需要先写假复位。</summary> [ExpressionOutput(IsEvent = true)] public bool Tick { get => _tick; private set => SetOutput(ref _tick, value); } /// <summary>保存当前实例的周期次数。</summary> private int _count; /// <summary>发布当前实例已经完成的周期计数。</summary> [ExpressionOutput] public int Count { get => _count; private set => SetOutput(ref _count, value); } /// <summary>由当前节点持有并释放的非重入定时任务。</summary> private TimerX? _timer; /// <summary>校验周期并建立当前流程实例的定时任务。</summary> public override Task InitAsync() { if (IntervalMs < 1) throw new RuleBusinessRejectedException("InvalidInterval", "IntervalMs must be positive."); Count = 0; ReportWaiting(); _timer = new TimerX(OnTimer, null, IntervalMs, IntervalMs); return Task.CompletedTask; } /// <summary>启用输入变化时更新空闲等待说明。</summary> public override Task ChangedAsync() { ReportWaiting(); return Task.CompletedTask; } /// <summary>定时线程不执行节点业务,只向流程队列提交固定通知。</summary> private void OnTimer(object? state) { if (!LifetimeCancellationToken.IsCancellationRequested) EnqueueNotification(PublishTick, nameof(Tick)); } /// <summary>在受管串行执行内更新计数并发布本次周期事件。</summary> private void PublishTick() { if (!Enabled) return; Count++; Tick = true; } /// <summary>空闲周期等待与执行结果分别报告。</summary> private void ReportWaiting() { SetWaitingReason(Enabled ? "WaitingForInterval" : "WaitingForEnabled", Enabled ? $"等待下次周期:{IntervalMs} ms" : "等待启用"); } /// <summary>停止时移除定时任务,流程令牌同时阻止后续通知进入。</summary> public void Dispose() { _timer?.Dispose(); _timer = null; } }

排障与上线

开发注意事项

场景要求
参数命名使用 C# 属性合法名称,建议 PascalCase,例如 DeviceName、HighLimit。不要用中文、空格、连字符。
参数重命名定义按稳定类型身份和端口名称引用。改名时同步修改流程;只调整排序不应改变端口绑定。
输出赋值页面节点直接给输出属性赋值;完整类节点必须用 SetOutput 或等价回调。只改私有字段不会触发下游。
异步耗时ChangedAsync 中访问网络、数据库、MQTT、邮件服务时必须设置超时并处理异常,不要让规则流程无限等待。
事件订阅订阅、定时器和连接属于当前流程实例。完整类实现释放接口,WEB 节点可覆盖 StopAsync;停止时解除订阅并等待在途工作结束。
高频触发默认防抖能保护下游。开启“无限制触发”前先评估最坏频率、外部系统限流和 CPU 占用。
Object 类型结构化跨节点数据使用明确的 JSON 契约;自定义 CLR 类型还须满足当前节点目录、生成器和端口契约校验,不能依赖任意对象隐式转换。
日志日志里不要输出密码、Token、证书内容。高频节点只记录异常和关键状态变化。
动态加载加载 CustomNodeDlls 中的节点程序集要求 RuntimeFeature.IsDynamicCodeSupported 为 true。

排障

现象检查链路
编译失败先看第一条红色诊断;确认页面代码没有写完整类;确认参数名不是 C# 关键字;确认代码中用到的命名空间已 using 或写全名。
编译成功但规则面板没有节点刷新规则页面;查看“已加载节点”;确认编译输出 DLL 位于 CustomNodeDlls;确认运行环境支持动态代码。
流程启动后节点不执行确认流程已启用;确认节点没有上游时可作为起始节点;有上游时确认上游输出确实发生赋值。
下游收不到值确认当前节点设置的是输出参数或输入输出参数;完整类是否使用 SetOutput;连线是否从右侧输出端口连到左侧输入端口。
下游输入为空或保留旧值核对本次是否写入端口、是否显式跳过、值是否被去重以及可空/JSON 契约;查看连接最近活动,不能以旧显示值认定本次已投递。
参数值写入失败检查规则实例属性是否能转换为参数类型;Object 参数检查 JSON 和 CustomType;数字类型检查小数/整数是否匹配。
重启后重复通知检查事件节点、定时器节点、MQTT/邮件/Webhook 节点是否实现并正确释放 IDisposable。
流程校验提示环路按诊断定位连线并拆除环路;用保持、计数、上下文或独立内存变量保存状态。
RPC 写入失败检查设备名、变量名、变量保护类型、写表达式、采集插件写入能力和设备在线状态。
推送节点阻塞检查外部 URL、DNS、证书、超时设置、代理、防火墙;外部失败应输出错误而不是吞掉异常。

上线检查清单

检查项要求
编译单个编译成功,批量编译成功,服务重启后“已加载节点”仍能看到。
参数每个参数有现场可理解的描述;输入、输出、输入输出方向正确。
流程先用 3 到 5 个变量验证起始触发、输出传播、运行值显示和日志,再扩大范围。
异常通讯失败、变量不存在、JSON 解析失败、外部推送失败都有明确输出或日志。
资源事件、定时器、连接、通道、客户端在流程停止时能释放。
性能高频流程不开无意义日志;外部请求有超时;批量推送或 RPC 有节流策略。
回退生产修改前保留旧节点代码、旧参数表和规则流程导出文件。

相关文档

可选:通过 MCP 获取开发技能和随包工程

正常开发可直接阅读上面的准备、示例和契约,不需要先配置 AI 客户端。没有源码时,可通过以下材料入口取得公开契约、同版本发布引用及配套示例。

通过目标网关 MCP 获取 tgateway-development 技能,读取 references/custom-nodes.md、references/rule-engine.md 和 references/validation.md。从 examples/index.json 按需读取节点示例,从实例节点目录核对稳定 typeId、参数和端口,再创建节点及规则连线;不要求访问产品源码。

网页节点参数单独声明,代码填写节点成员,不粘贴完整 FullSource 类或顶层 using。除编译结果外,还要检查输入输出初值、触发传播、取消和事件退订,以及规则保存后的实例重载。技能同时覆盖规则 JSON 包的生成与校验,不能只交付孤立节点而省略用户需要的规则流程。