Novu Tool 步骤 `enabledIntegrations` 控件移除方案:从集成标识过滤到全量投递的架构演进
2026/9/10 19:54:39 网站建设 项目流程

Novu Tool 步骤enabledIntegrations控件移除方案:从集成标识过滤到全量投递的架构演进

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

本指南围绕 Novu 仓库中 remove-enabled-integrations.md 这份实施计划展开,系统讲解 Tool 渠道步骤中enabledIntegrations(集成标识符过滤器)控件的移除目标、涉及的文件与符号、兼容性策略与验证方法。读完本文,你将理解 Tool 步骤从"按集成标识符过滤投递"演进为"始终投递到环境内全部活跃 Tool 集成"的完整技术路径,并能结合仓库源码定位每一层改动(UI、Schema、DTO、sanitize、渲染器、Worker 过滤逻辑)的实际落点。

一、方案背景:为什么要移除enabledIntegrations

在 Novu 的 Workflow 编辑器中,Tool 渠道步骤(Tool-channel step)曾经提供一项名为enabledIntegrations的步骤级控件,用于以"集成标识符(integration identifier)"为单位过滤投递目标:用户可以在步骤里勾选希望消息投递到的集成,Worker 在执行发送时依据该字段在环境内筛选出匹配的集成,再逐个派发。

该计划的核心目标是彻底移除这个控件及其全部关联逻辑——包括 UI、Schema、DTO、sanitize、渲染(render)以及 Worker 侧的过滤逻辑。移除之后,Tool 步骤的行为回归为一种简单而明确的语义:

Tool 步骤始终投递到环境中所有处于活跃状态(active)的 Tool 集成,即此前enabledIntegrations为空或未定义时的默认行为。

这一决策将"哪些集成参与投递"的职责完全交还给环境级别的集成配置,步骤本身不再持有额外的过滤状态,从而消除"环境里明明配置了集成、步骤却没投出去"这类因步骤级过滤而产生的隐性故障源。

二、范围界定:明确"不动什么"

计划文档对移除范围做了清晰的边界声明,避免误伤相邻概念:

范围项说明
enabledProviderslibs/maily-core这是邮件编辑器里"建议提供者(suggestion providers)"的配置,与 Tool 步骤的集成过滤器是完全不同的概念,保持不动
libs/internal-sdk自动生成代码,禁止手工编辑
DB 迁移不需要执行数据库迁移,理由见下文"兼容与迁移策略"

这种边界划分意味着本次改动是纯代码层的收敛:它只触及控制面板(Dashboard)的表单表现层、后端与框架的 Schema/DTO 定义层,以及 Worker 的执行过滤层,不触碰编辑器生态与数据存储结构。

三、改动清单逐层拆解

3.1 删除文件:UI 控件本体

计划中唯一需要整体删除的源文件是 Dashboard 侧的 Tool 步骤表单控件:

路径符号 / 原因
apps/dashboard/src/components/workflow-editor/steps/tool/tool-enabled-providers.tsxToolEnabledProviders——该控件专属的 UI 组件,仅用于渲染"启用哪些集成"的表单区块

从当前仓库的实际状态看,该文件已不存在于apps/dashboard/src/components/workflow-editor/steps/目录下,tool/目录中也不再保留这一控件,印证了"UI 层移除"这一步的落地结果。

3.2 Dashboard:表单接线收口

apps/dashboard/src/components/workflow-editor/steps/configure-step-form.tsx承担步骤表单的组装职责,计划要求在此文件完成三处清理:

  1. 移除ToolEnabledProviders的 import 与 JSX 使用;
  2. 移除showToolFormMiddleSection(控制"中间表单区块"是否展示的辅助逻辑);
  3. 移除registerInlineControlValues中 TOOL 分支里对enabledIntegrations默认值的写入逻辑。

对照当前源码,configure-step-form.tsx中的registerInlineControlValues(configure-step-form.tsx)只保留了两条分支:isInlineConfigurableStep时取getControlsDefaultValues(step),以及StepTypeEnum.HTTP_REQUEST时为continueOnFailure补充默认值,TOOL 分支已不复存在——移除后的defaultValues不再为 Tool 步骤注入任何enabledIntegrations默认值,表单提交的 payload 中自然也不含该键。

3.3 Schema / DTO / sanitize:定义层收敛

这一层是"移除"的核心证据链,涉及四个文件:

① Tool 控件 Zod Schema

计划要求从toolControlZodSchema中删除enabledIntegrations字段。当前实现(tool-control.schema.ts)为:

export const toolControlZodSchema = z .object({ skip: skipZodSchema, body: z.string(), }) .strict();

注意.strict()的存在:Zod 会对未知顶层键直接判失败。这意味着一旦 Schema 中不再声明enabledIntegrations,任何仍携带该键的输入都会在校验阶段被拒绝——这既是移除的手段,也是下文"陈旧键会以 step-issue 形式浮现"这一兼容性现象的直接原因。同文件生成的 JSON Schema(toolControlSchema,由zodToJsonSchema转换而来)随之只含bodyskip,且被additionalProperties: false约束。

② Schema 单测

libs/application-generic/src/schemas/control/tool-control.schema.spec.ts需同步更新 fixtures。当前测试(tool-control.schema.spec.ts)已覆盖三类行为:

  • 接受合法默认控件{ body: 'MemoryDB alert' }
  • 拒绝未知顶层键unknownKey: truesuccess === false);
  • 拒绝在控件值上嵌套providerOverrides

其中"拒绝未知顶层键"正是验证enabledIntegrations这类残留键会被.strict()拦下的关键用例;另一个用例则断言生成的 JSON Schema 属性键恰好为['body', 'skip'],从结果上确认了字段收敛。

③ Tool 控件 DTO

libs/application-generic/src/dtos/workflow/tool-control.dto.ts中的ToolControlDto计划移除enabledIntegrations字段及其校验器。当前 DTO(tool-control.dto.ts)仅保留:

export class ToolControlDto extends SkipControlDto { @ApiPropertyOptional({ description: 'Content of the tool payload.' }) @IsString() @IsOptional() body: string; }

即继承SkipControlDtoskip外,只有body一个业务字段,enabledIntegrations及其@IsString()等校验装饰器已不在其中。

④ 控件值 sanitize

libs/application-generic/src/utils/sanitize-control-values.ts中的sanitizeTool计划停止映射enabledIntegrations。当前实现(sanitize-control-values.ts):

function sanitizeTool(controlValues: WithProviderOverrides<ToolControlType>) { const mappedValues: ToolControlType = { body: sanitizeEmptyInput(controlValues.body), skip: controlValues.skip, }; return keepProviderOverrides(filterNullishValues(mappedValues) as Record<string, unknown>, controlValues); }

sanitizeTool采用白名单式拷贝:只从输入中复制bodyskip(外加透传的providerOverrides),再经filterNullishValues剔除空值。因此,即便持久化的步骤控件值里仍残留enabledIntegrations,一旦用户重新保存(save)或执行 normalize,该键就会被"洗掉"——这正是"无需 DB 迁移"策略的地基(详见第四节)。

⑤ 框架层 Tool 输出 Schema

packages/framework/src/schemas/steps/channels/tool.schema.ts中的toolOutputSchema.properties计划移除该字段。当前实现(tool.schema.ts):

const toolOutputSchema = { type: 'object', properties: { body: { type: 'string' }, }, required: ['body'], additionalProperties: false, } as const satisfies JsonSchema;

additionalProperties: false意味着任何仍携带enabledIntegrations的 in-flight bridge 输出,若经过 schema 校验都会被拒绝——这是第五节中"低风险"评估的另一处依据。对应的packages/framework/src/schemas/steps/channels/tool.schema.test.ts期望值需同步更新。

⑥ 共享 DTO:预览响应

packages/shared/src/dto/workflows/preview-step-response.dto.ts中的ToolRenderOutput计划移除该字段。当前定义(preview-step-response.dto.ts):

export class ToolRenderOutput extends RenderOutput { body: string; providerOverrides?: StepProviderOverrides; }

预览(preview)响应体只承诺body(与可选的providerOverrides),不再携带任何集成过滤信息。

3.4 API / Worker:执行链路收敛

① API 输出渲染器

apps/api/src/app/environments-v1/usecases/output-renderers/tool-output-renderer.usecase.ts计划停止向输出中拷贝enabledIntegrations。当前实现(tool-output-renderer.usecase.ts)为仅 body 输出

execute(translatedControls: Record<string, unknown>): ToolRenderOutput { return { body: (translatedControls.body as string) ?? '', }; }

渲染器只从已翻译的控件值中取出body,其余键一概不进入输出对象。

② Worker 发送用例

apps/worker/src/app/workflow/usecases/send-message/send-message-tool.usecase.ts是本次改动的执行核心,计划要求:

  • 删除filterToolIntegrationsByEnabledIdentifiers过滤函数;
  • 删除ToolStepOutputs.enabledIntegrations字段;
  • 删除过滤调用;
  • 删除requestedEnabledIntegrationsenabled_integrations_filter_matched_none失败明细(execution details);
  • 改为使用所有活跃集成
  • 简化resolveContentAndProviders使其只返回{ content }

对照当前源码,SendMessageTool.execute的集成获取逻辑(send-message-tool.usecase.ts)为:

const integrations = await this.getDecryptedIntegrations.execute( GetDecryptedIntegrationsCommand.create({ organizationId: command.organizationId, environmentId: command.environmentId, userId: command.userId, channelType: ChannelTypeEnum.TOOL, active: true, scopeToEnvironment: true, }) );

注意active: true+scopeToEnvironment: true的组合:Worker 直接拉取当前环境下全部活跃 Tool 集成,不再按步骤的enabledIntegrations做二次筛选。若环境内没有任何活跃 Tool 集成,则创建失败明细SUBSCRIBER_NO_ACTIVE_INTEGRATION(raw 中携带reason: 'no_active_tool_integrations')并返回失败;随后遍历每个集成,通过isEndpointRoutedToolProvider区分"端点路由(endpoint-routed)"与"凭据路由(credential-routed)"两类投递路径:

  • 非端点路由:直接sendToIntegration
  • 端点路由(如 PagerDuty、Opsgenie、Grafana 及routingMode: 'dynamic'的 Webhook):经resolveEndpointsByIntegration解析出该订阅者在该集成下的 channel endpoints(channelData),逐端点派发;无端点时以no_channel_endpoint_for_subscriber警告跳过。

与此同时ToolStepOutputs类型当前仅含body?: string(send-message-tool.usecase.ts),resolveContent也只消费bridgeOutputs?.body或编译后的模板内容——过滤相关字段与失败分支已从执行链路中消失。

③ Worker 单测

apps/worker/src/app/workflow/usecases/send-message/send-message-tool.usecase.spec.ts计划删除enabledIntegrations filter相关的 describe 块。当前该 spec(send-message-tool.usecase.spec.ts)的测试焦点已完全迁移到 endpoint 路由能力上:isEndpointRoutedToolProvider对 PagerDuty、Opsgenie、Grafana 恒为 true(无论 credentials 如何)、对 Webhook 依赖routingMode: 'dynamic',以及 Opsgenie 端点路由发送路径的桩(stub)验证——过滤逻辑相关用例已无踪迹。

四、兼容与迁移策略:为什么可以不做 DB 迁移

这是整个方案中最具工程价值的设计决策。已持久化的工作流步骤控件值(MongoDB 中controls.values等字段)可能仍含有enabledIntegrations,但计划明确倾向不做 DB 迁移,理由由三层机制共同兜底:

  1. sanitize 白名单洗键sanitizeTool只拷贝已知键(bodyskipproviderOverrides),下一次 normalize/save 就会把enabledIntegrations从持久化数据中剥离。也就是说,清洗是"惰性"的——不需要离线脚本,用户每次保存自然完成收敛。
  2. Worker 直接忽略:移除过滤调用后,Worker 只按active: true读取活跃集成,即使旧数据里残留该键也不会影响任何一次投递行为。
  3. 严格 Schema 兜底:Zod 的.strict()与框架输出 Schema 的additionalProperties: false意味着"如果原始校验先于 sanitize 执行,陈旧键可能以 step-issue 的形式浮现,直到工作流被重新保存"。计划文档明确认可这一行为——它与现有未知键(unknown-key)的处理策略一致,属于可接受的短期噪音,而非功能故障。

此外还需考虑在途(in-flight)bridge 输出:框架侧运行中的 bridge 输出若仍携带该键,一旦经过 schema 校验就会被拒绝(additionalProperties: false);但渲染器已不再产出该键,因此对新流量而言风险很低。计划对此的判断是"Low risk for new traffic",其成立前提正是渲染器(API 层)与执行器(Worker 层)两端的同步收敛。

五、验证方案:如何确认移除彻底

计划给出了三级验证闭环,全部可在当前仓库中对应执行:

1. Ripgrep 全仓扫描

确认 Tool 渠道相关的enabledIntegrationsToolEnabledProvidersfilterToolIntegrationsByEnabledIdentifiers不再残留(libs/maily-core中与 Tool 无关的enabledProviders属于例外,不应被误伤)。

2. 单元测试

测试文件验证点
apps/worker/src/app/workflow/usecases/send-message/send-message-tool.usecase.spec.tsWorker 执行路径不再包含过滤分支,端点路由逻辑正常
libs/application-generic/src/schemas/control/tool-control.schema.spec.tsTool 控件 Schema 属性恰为body/skip,未知键被拒
packages/framework/src/schemas/steps/channels/tool.schema.test.ts框架输出 Schema 期望值与body-only 结构一致

3. 手动验证(可选)

打开 Dashboard 中的任一 Tool 步骤:表单中"Enabled Providers"区块应已消失;触发发送后,消息应扇出(fan out)到环境内所有活跃 Tool 集成。

六、小结:移除之后的行为模型

enabledIntegrations的移除,本质上是把"投递目标的选择"从步骤级配置迁移到环境级集成配置。移除后的完整调用链可以概括为:

  1. 表单层:Tool 步骤表单不再渲染、不再默认写入任何集成过滤字段(configure-step-form.tsx);
  2. 定义层toolControlZodSchema.strict())与ToolControlDto只接受body/skip(tool-control.schema.ts、tool-control.dto.ts);
  3. 清洗层sanitizeTool以白名单方式在保存/normalize 时剥离陈旧键(sanitize-control-values.ts);
  4. 渲染层ToolOutputRendererUsecase只输出body(tool-output-renderer.usecase.ts),框架输出 Schema 以additionalProperties: false拒绝残留键(tool.schema.ts);
  5. 执行层SendMessageToolactive: true拉取环境内全部活跃 Tool 集成并逐个派发,过滤分支与相关失败明细已移除(send-message-tool.usecase.ts)。

对于使用 Novu 的团队而言,这套方案的最大收益在于语义简化:一个 Tool 步骤的投递范围 = 该环境下全部活跃 Tool 集成,排查"消息为什么没发到某个集成"时,只需检查集成自身的 active 状态与端点路由配置,而不再需要同时排查步骤里可能早已过期的过滤名单。

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询