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为空或未定义时的默认行为。
这一决策将"哪些集成参与投递"的职责完全交还给环境级别的集成配置,步骤本身不再持有额外的过滤状态,从而消除"环境里明明配置了集成、步骤却没投出去"这类因步骤级过滤而产生的隐性故障源。
二、范围界定:明确"不动什么"
计划文档对移除范围做了清晰的边界声明,避免误伤相邻概念:
| 范围项 | 说明 |
|---|---|
enabledProviders(libs/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.tsx | ToolEnabledProviders——该控件专属的 UI 组件,仅用于渲染"启用哪些集成"的表单区块 |
从当前仓库的实际状态看,该文件已不存在于apps/dashboard/src/components/workflow-editor/steps/目录下,tool/目录中也不再保留这一控件,印证了"UI 层移除"这一步的落地结果。
3.2 Dashboard:表单接线收口
apps/dashboard/src/components/workflow-editor/steps/configure-step-form.tsx承担步骤表单的组装职责,计划要求在此文件完成三处清理:
- 移除
ToolEnabledProviders的 import 与 JSX 使用; - 移除
showToolFormMiddleSection(控制"中间表单区块"是否展示的辅助逻辑); - 移除
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转换而来)随之只含body与skip,且被additionalProperties: false约束。
② Schema 单测
libs/application-generic/src/schemas/control/tool-control.schema.spec.ts需同步更新 fixtures。当前测试(tool-control.schema.spec.ts)已覆盖三类行为:
- 接受合法默认控件
{ body: 'MemoryDB alert' }; - 拒绝未知顶层键(
unknownKey: true时success === 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; }即继承SkipControlDto的skip外,只有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采用白名单式拷贝:只从输入中复制body与skip(外加透传的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字段; - 删除过滤调用;
- 删除
requestedEnabledIntegrations及enabled_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 迁移,理由由三层机制共同兜底:
- sanitize 白名单洗键:
sanitizeTool只拷贝已知键(body、skip、providerOverrides),下一次 normalize/save 就会把enabledIntegrations从持久化数据中剥离。也就是说,清洗是"惰性"的——不需要离线脚本,用户每次保存自然完成收敛。 - Worker 直接忽略:移除过滤调用后,Worker 只按
active: true读取活跃集成,即使旧数据里残留该键也不会影响任何一次投递行为。 - 严格 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 渠道相关的enabledIntegrations、ToolEnabledProviders、filterToolIntegrationsByEnabledIdentifiers不再残留(libs/maily-core中与 Tool 无关的enabledProviders属于例外,不应被误伤)。
2. 单元测试
| 测试文件 | 验证点 |
|---|---|
apps/worker/src/app/workflow/usecases/send-message/send-message-tool.usecase.spec.ts | Worker 执行路径不再包含过滤分支,端点路由逻辑正常 |
libs/application-generic/src/schemas/control/tool-control.schema.spec.ts | Tool 控件 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的移除,本质上是把"投递目标的选择"从步骤级配置迁移到环境级集成配置。移除后的完整调用链可以概括为:
- 表单层:Tool 步骤表单不再渲染、不再默认写入任何集成过滤字段(configure-step-form.tsx);
- 定义层:
toolControlZodSchema(.strict())与ToolControlDto只接受body/skip(tool-control.schema.ts、tool-control.dto.ts); - 清洗层:
sanitizeTool以白名单方式在保存/normalize 时剥离陈旧键(sanitize-control-values.ts); - 渲染层:
ToolOutputRendererUsecase只输出body(tool-output-renderer.usecase.ts),框架输出 Schema 以additionalProperties: false拒绝残留键(tool.schema.ts); - 执行层:
SendMessageTool按active: 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),仅供参考