ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SuperPlane 集成开发指南:从零编写 Trigger、Component 与前端 Mapper

SuperPlane 集成开发指南:从零编写 Trigger、Component 与前端 Mapper 【免费下载链接】superplaneOpen source factory for one-shot engineering项目地址https://gitcode.com/gh_mirrors/su/superplane点击查看免费下载SuperPlane 是一个开源的一站式工程化平台其核心能力之一是通过「集成Integration」把 GitHub、Semaphore、Slack 等外部服务接入工作流画布。本文基于仓库中的 docs/contributing/integrations.md 官方指南系统讲解如何新增一个集成、为已有集成扩展 Trigger触发器与 Component组件并配套编写 TypeScript 前端 Mapper让读者掌握一套可复制、可上线的完整集成开发流程。集成是什么一次理解 Backend Frontend 双端结构在 SuperPlane 中集成是「与外部服务的连接」它让用户可以用外部事件触发工作流执行并在工作流中对外部服务执行动作。一个集成由两部分组成后端实现Go位于pkg/integrations/app-name/负责与外部服务的 API 通信、webhook 接收与签名校验、认证配置等。前端 MapperTypeScript位于web_src/src/pages/app/mappers/integration-name/负责把 Trigger/Component 渲染到画布 UI决定事件在界面上如何展示。注意指南旧版写的是workflowv2/mappers/当前仓库实际路径已迁移为web_src/src/pages/app/mappers/开发时以实际目录为准。每个集成可以对外暴露两类能力Triggers触发器事件源用于启动工作流执行例如「On Pull Request」「On Pipeline Done」「On Issue」。Components组件工作流中可执行的动作例如「Run Workflow」「Create Release」。两者都遵循pkg/core/中定义的核心接口详见下文因此扩展方式高度一致。集成目录结构后端与前端各司其职后端集成按单一目录组织每个 Trigger 独立一个文件pkg/integrations/ ├── github/ │ ├── github.go # 主集成实现实现 core.Integration 接口 │ ├── client.go # API 客户端按需 │ ├── on_pull_request.go # Trigger 实现 │ ├── on_push.go # 另一个 Trigger │ └── on_issue.go # 又一个 Trigger └── semaphore/ ├── semaphore.go ├── client.go └── ...前端 Mapper 目录与后端一一对应每个 Trigger/Component 一个渲染文件由index.ts统一导出web_src/src/pages/app/mappers/ ├── github/ │ ├── index.ts # 导出全部 trigger/component 渲染器 │ ├── on_pull_request.ts # Trigger 渲染器 │ ├── on_push.ts │ └── on_issue.ts └── semaphore/ └── ...这种「后端一个包 前端一个目录」的对称结构让新集成从一开始就遵循社区既有模式便于 Code Review 与后续维护。创建新集成实现 core.Integration 接口并完成注册1. 创建集成包并实现主文件在pkg/integrations/integration-name/下新建主文件如myintegration.go。集成必须实现 pkg/core/integration.go 中定义的core.Integration接口package myintegration import ( github.com/superplanehq/superplane/pkg/configuration github.com/superplanehq/superplane/pkg/core github.com/superplanehq/superplane/pkg/registry ) func init() { registry.RegisterIntegration(myintegration, MyIntegration{}) } type MyIntegration struct{} type Configuration struct { APIKey string json:apiKey } type Metadata struct { // Store integration-level metadata } func (i *MyIntegration) Name() string { return myintegration } func (i *MyIntegration) Label() string { return My Integration } func (i *MyIntegration) Icon() string { return icon-name } func (i *MyIntegration) Description() string { return Description of what this integration does } func (i *MyIntegration) Configuration() []configuration.Field { return []configuration.Field{ { Name: apiKey, Label: API Key, Type: configuration.FieldTypeString, Sensitive: true, Description: Your API key, Required: true, }, } } func (i *MyIntegration) Components() []core.Component { return []core.Component{ // Add your components here } } func (i *MyIntegration) Triggers() []core.Trigger { return []core.Trigger{ // Add your triggers here } } func (i *MyIntegration) Sync(ctx core.SyncContext) error { // Validate configuration and set up the integration // Set state to ready when done: ctx.Integration.Ready() return nil } func (i *MyIntegration) HandleRequest(ctx core.HTTPRequestContext) { // Handle incoming HTTP requests (e.g., OAuth callbacks, webhooks) }关键点说明Name()返回的字符串是全局唯一标识节点画布上的 Trigger/Component通过它引用该集成注册也以它为 key。Configuration()声明连接时需要用户填写的字段Sensitive: true表示该字段为密钥如 API Key会被平台按密钥方式存储与展示。字段类型来自pkg/configuration包除FieldTypeString外还有 MultiSelect、IntegrationResource 等多种类型见后文 GitHub 实例。Sync()在配置发生变化时被调用用于校验配置并完成初始化校验通过后调用ctx.Integration.Ready()把集成状态置为 ready失败时可用Error(message)上报错误。HandleRequest()负责处理集成级 HTTP 请求如 OAuth 回调、webhook 接收它是集成与外部世界通信的入口。2. 通过 registry 注册集成注册是集成被平台发现的关键一步使用 pkg/registry/registry.go 提供的注册函数func init() { registry.RegisterIntegration(myintegration, MyIntegration{}) }如果集成需要管理 webhook则改为同时注册core.WebhookHandlerfunc init() { registry.RegisterIntegrationWithWebhookHandler(myintegration, MyIntegration{}, MyIntegrationWebhookHandler{}) }RegisterIntegrationWithWebhookHandler会把集成与 webhook 处理器成对登记见 pkg/registry/registry.gowebhook 的创建、复用与清理都将由该 handler 驱动。pkg/registry/registry.go还提供了带SetupProvider等扩展选项的RegisterIntegrationWithOptions供需要更复杂安装流程的集成使用。添加 Triggers事件如何从外部进入画布Trigger 监听外部事件并启动工作流执行。它必须实现 pkg/core/trigger.go 中的core.Trigger接口。完整生命周期为Setup()校验配置并申请 webhook → 外部服务回调HandleWebhook()→ 校验签名与过滤 →ctx.Events.Emit()发出事件启动执行。1. 创建 Trigger 文件在集成包内新建on_event.gopackage myintegration import ( encoding/json fmt net/http slices strings github.com/mitchellh/mapstructure github.com/superplanehq/superplane/pkg/configuration github.com/superplanehq/superplane/pkg/core github.com/superplanehq/superplane/pkg/crypto ) type OnEvent struct{} type OnEventMetadata struct { // Store trigger-specific metadata Resource string json:resource } type OnEventConfiguration struct { Resource string json:resource Actions []string json:action } func (t *OnEvent) Name() string { return myintegration.onEvent } func (t *OnEvent) Label() string { return On Event } func (t *OnEvent) Description() string { return Listen to event occurrences } func (t *OnEvent) Icon() string { return icon-name } func (t *OnEvent) Color() string { return gray } func (t *OnEvent) Configuration() []configuration.Field { return []configuration.Field{ { Name: resource, Label: Resource, Type: configuration.FieldTypeString, Required: true, }, { Name: actions, Label: Actions, Type: configuration.FieldTypeMultiSelect, Required: true, Default: []string{created}, TypeOptions: configuration.TypeOptions{ MultiSelect: configuration.MultiSelectTypeOptions{ Options: []configuration.FieldOption{ {Label: Created, Value: created}, {Label: Updated, Value: updated}, {Label: Deleted, Value: deleted}, }, }, }, }, } } func (t *OnEvent) Setup(ctx core.TriggerContext) error { var metadata OnEventMetadata err : mapstructure.Decode(ctx.Metadata.Get(), metadata) if err ! nil { return fmt.Errorf(failed to parse metadata: %w, err) } // If metadata is already set, trigger is already setup if metadata.Resource ! { return nil } config : OnEventConfiguration{} err mapstructure.Decode(ctx.Configuration, config) if err ! nil { return fmt.Errorf(failed to decode configuration: %w, err) } // Validate configuration if config.Resource { return fmt.Errorf(resource is required) } // Store metadata metadata.Resource config.Resource err ctx.Metadata.Set(metadata) if err ! nil { return fmt.Errorf(failed to set metadata: %w, err) } // Request webhook if needed return ctx.Integration.RequestWebhook(WebhookConfiguration{ EventType: event, Resource: config.Resource, }) } func (t *OnEvent) Hooks() []core.Hook { return []core.Hook{} } func (t *OnEvent) HandleHook(ctx core.TriggerHookContext) (map[string]any, error) { return nil, nil } func (t *OnEvent) HandleWebhook(ctx core.WebhookRequestContext) (int, *core.WebhookResponseBody, error) { // Validate webhook signature signature : ctx.Headers.Get(X-Signature) if signature { return http.StatusForbidden, nil, fmt.Errorf(invalid signature) } // Verify the signature secret, err : ctx.Webhook.GetSecret() if err ! nil { return http.StatusInternalServerError, nil, fmt.Errorf(error authenticating request) } if err : crypto.VerifySignature(secret, ctx.Body, signature); err ! nil { return http.StatusForbidden, nil, err } // Parse the webhook payload data : map[string]any{} err json.Unmarshal(ctx.Body, data) if err ! nil { return http.StatusBadRequest, nil, fmt.Errorf(error parsing request body: %v, err) } // Filter by action type config : OnEventConfiguration{} err mapstructure.Decode(ctx.Configuration, config) if err ! nil { return http.StatusInternalServerError, nil, fmt.Errorf(failed to decode configuration: %v, err) } action, ok : data[action] if !ok { return http.StatusBadRequest, nil, fmt.Errorf(missing action) } if !slices.Contains(config.Actions, action.(string)) { return http.StatusOK, nil, nil } // Emit the event to trigger workflow execution err ctx.Events.Emit(data) if err ! nil { return http.StatusInternalServerError, nil, fmt.Errorf(error emitting event: %v, err) } return http.StatusOK, nil, nil }围绕该示例结合核心源码逐层解读Setup()是幂等的先尝试从ctx.Metadata.Get()解析元数据若Resource已存在说明此前已配置过直接返回否则解码用户配置、校验必填项、写入元数据最后调用ctx.Integration.RequestWebhook(...)申请 webhook。这正是 pkg/core/integration.go 中IntegrationContext.RequestWebhook的标准用法。HandleWebhook()的返回签名(int, *core.WebhookResponseBody, error)。其中*core.WebhookResponseBody允许在需要时返回自定义响应体见 pkg/core/trigger.go传nil时服务端返回默认的空 200 OK。先验签再处理任何 webhook 都必须先校验X-Signature用ctx.Webhook.GetSecret()取到节点 webhook 密钥后调用crypto.VerifySignature验证防止伪造请求触发工作流。尽早过滤解析出action后用slices.Contains(config.Actions, action)判断本次事件是否属于用户配置的动作集合不匹配直接返回 200 而不发射事件避免无谓的流程执行。发射事件ctx.Events.Emit(data)对应 pkg/core/trigger.go 的EventContext.Emit把 webhook payload 作为事件数据发射从而启动工作流执行。2. 在集成中注册 Trigger把 Trigger 加入集成的Triggers()方法func (i *MyIntegration) Triggers() []core.Trigger { return []core.Trigger{ OnEvent{}, } }3. 实现 Webhook Handler如需要如果 Trigger 或 Component 需要 webhook就实现core.WebhookHandler接口并随集成一并注册见上文RegisterIntegrationWithWebhookHandler。该接口定义在 pkg/core/integration.go包含三个核心方法type WebhookConfiguration struct { EventType string json:eventType Resource string json:resource } type MyIntegrationWebhookHandler struct{} // CompareConfig defines when two webhook configurations are equal. // This is used to determine if an existing webhook can be reused. func (h *MyIntegrationWebhookHandler) CompareConfig(a, b any) (bool, error) { configA : WebhookConfiguration{} if err : mapstructure.Decode(a, configA); err ! nil { return false, err } configB : WebhookConfiguration{} if err : mapstructure.Decode(b, configB); err ! nil { return false, err } // Define equality based on your integrations webhook configuration. // Webhooks with matching configurations can be shared across multiple triggers/components. return configA.Resource configB.Resource configA.EventType configB.EventType, nil } // Setup creates a webhook in the external service. // This is called by the webhook provisioner for pending webhook records. func (h *MyIntegrationWebhookHandler) Setup(ctx core.WebhookHandlerContext) (any, error) { // Create webhook in the external service // Return metadata about the created webhook (e.g., webhook ID) return nil, nil } // Cleanup deletes a webhook from the external service. // This is called by the webhook cleanup worker for deleted webhook records. func (h *MyIntegrationWebhookHandler) Cleanup(ctx core.WebhookHandlerContext) error { // Delete webhook from the external service using the metadata return nil }方法职责与接口注释一致见 pkg/core/integration.goSetup由webhook provisioner供应器调用负责在外部服务中真实创建 webhook返回值作为 webhook 元数据如 webhook ID持久化。Cleanup由webhook cleanup worker清理工作者调用负责根据元数据删除外部服务中的 webhook。CompareConfig判断两份 webhook 配置是否等价决定已有 webhook 能否被复用。Webhook 共享机制一个 webhook 服务多个节点webhook 的管理逻辑集中在Integration.RequestWebhook()中当 Trigger 或 Component 申请 webhook 时上下文列出该集成名下所有已存在的 webhook对每个已存在 webhook调用你的 handler 的CompareConfig()判断配置是否匹配若匹配则把当前节点关联到该已有 webhook若都不匹配才创建新 webhook。这意味着配置相同如监听同一资源、同一事件类型的多个 Trigger/Component 可以共享同一个 webhook显著减少在外部服务中创建的 webhook 数量也简化了权限管理与清理逻辑。另外接口还提供Merge(current, requested)用于在复用时合并配置返回changedfalse表示无需更新让 webhook 复用更加精细。添加 Components工作流中的动作节点Component 是工作流中可执行的动作添加流程与 Trigger 基本对称在集成包内新建组件文件如do_action.go实现core.Component接口在集成的Components()方法中注册。core.Component的底层支撑是 pkg/core/component.go 中的ExecutionContext它向组件暴露Configuration节点配置、Metadata可读写元数据、Integration集成上下文、Secrets密钥读取、ExecutionState执行生命周期控制Pass()、Fail(reason, message)、Cancel()、Emit(channel, payloadType, payloads)等以及HTTP统一 HTTP 客户端便于单元测试与集中控制超时等能力。在Setup阶段则使用 pkg/core/component.go 中的SetupContext它与 Trigger 的Setup类似也可调用Integration.RequestWebhook为组件申请 webhook。需要说明的是仓库当前core.Component的具体方法集合已随演进扩展如Documentation()、ExampleData()等开发时以 pkg/core/component.go 的接口定义为准遵循仓库内既有组件如 pkg/integrations/github/components/ 下的run_workflow.go、create_release.go的写法即可。添加前端 Mapper把节点渲染进画布前端 Mapper 负责在 UI 中渲染 Trigger/Component决定事件如何展示、给用户呈现哪些信息。以myintegration.onEvent为例在web_src/src/pages/app/mappers/myintegration/下新建on_event.tsimport { WorkflowsWorkflowEvent } from /api-client; import { getColorClass, getBackgroundColorClass } from /utils/colors; import { TriggerRenderer, NodeInfo, ComponentDefinition } from ../types; import appIcon from /assets/icons/integrations/app-name.svg; import { TriggerProps } from /ui/trigger; interface OnEventMetadata { resource: string; } interface OnEventConfiguration { actions: string[]; } interface OnEventEventData { action?: string; // Add other fields from your webhook payload } /** * Renderer for the myintegration.onEvent trigger */ export const onEventTriggerRenderer: TriggerRenderer { getTitleAndSubtitle: (event: WorkflowsWorkflowEvent): { title: string; subtitle: string } { const eventData event.data as OnEventEventData; return { title: Event occurred, subtitle: eventData?.action || , }; }, getRootEventValues: (lastEvent: WorkflowsWorkflowEvent): Recordstring, string { const eventData lastEvent.data as OnEventEventData; return { Action: eventData?.action || , // Add other relevant fields }; }, getTriggerProps: (node: NodeInfo, definition: ComponentDefinition, lastEvent: WorkflowsWorkflowEvent) { const metadata node.metadata as unknown as OnEventMetadata; const configuration node.configuration as unknown as OnEventConfiguration; const metadataItems []; if (metadata?.resource) { metadataItems.push({ icon: database, label: metadata.resource, }); } if (configuration?.actions) { metadataItems.push({ icon: funnel, label: configuration.actions.join(, ), }); } const props: TriggerProps { title: node.name!, iconSrc: appIcon, iconBackground: bg-white, iconColor: getColorClass(definition.color), headerColor: getBackgroundColorClass(definition.color), collapsedBackground: getBackgroundColorClass(definition.color), metadata: metadataItems, }; if (lastEvent) { const eventData lastEvent.data as OnEventEventData; props.lastEventData { title: Event occurred, subtitle: eventData?.action || , receivedAt: new Date(lastEvent.createdAt!), state: triggered, eventId: lastEvent.id, }; } return props; }, };然后更新或创建index.ts统一导出import { ComponentBaseMapper, TriggerRenderer } from ../types; import { onEventTriggerRenderer } from ./on_event; export const componentMappers: Recordstring, ComponentBaseMapper {}; export const triggerRenderers: Recordstring, TriggerRenderer { onEvent: onEventTriggerRenderer, };渲染器三个方法的分工getTitleAndSubtitle定义事件在画布/运行历史中的标题与副标题如Event occurred 具体action。getRootEventValues把最新一次事件的 payload 字段映射为「根事件值」供下游节点以键值形式引用如Action。getTriggerProps组装节点在画布上的视觉呈现——标题、图标、颜色来自后端 Trigger 的Color()经getColorClass/getBackgroundColorClass转为样式类、元数据条目如监听的 resource 与 actions 漏斗过滤以及最近一次事件的摘要lastEventData。实战示例GitHub Issues Trigger 的完整解剖指南中给出了一个真实落地的 GitHub Issues Trigger 作为范本。在仓库中它实际位于 pkg/integrations/github/components/issues/on_issue.go后端 Trigger 名为github.onIssueLabel 为 On Issue。后端实现要点对照源码可以看到配置字段repository使用configuration.FieldTypeIntegrationResource类型配合TypeOptions.Resource声明资源类型为repository且UseNameAsValue: true即用户在 UI 中从集成资源下拉框选择仓库actions是FieldTypeMultiSelect默认值[opened]并提供16 种 issue 动作类型可选opened、edited、deleted、transferred、pinned、unpinned、closed、reopened、assigned、unassigned、labeled、unlabeled、locked、unlocked、milestoned、demilestoned。Setup 方法调用 GitHub 通用工具common.EnsureRepoInMetadata(...)校验所选仓库对 GitHub App 安装是否可访问并把仓库写入元数据。Webhook 处理完成签名验证与 action 过滤只有用户勾选的动作类型才会Emit事件。事件数据每个 issue 事件携带action、完整的issuetitle、body、state、labels、assignees、repository与sender信息见该文件Documentation()方法中的说明。前端渲染要点对应前端 Mapperweb_src/src/pages/app/mappers/github/ 下on_issue.ts提供getTitleAndSubtitle把事件格式化为「#123 - Issue title」形式的标题与动作副标题。getRootEventValues抽取关键字段URL、Title、Action、Author、State供下游引用。getTriggerProps渲染节点展示所选仓库与 action 元数据。该示例完整演示了「配置声明 → Setup 校验 → webhook 接收 → 过滤 → 发射 → UI 渲染」的全链路是新增 Trigger 时最值得对照的参考实现。指南引用的pkg/integrations/github/在仓库中同样存在pkg/integrations/github/github.go 为主集成实现webhook_handler.go为 webhook handlerSemaphore 集成位于pkg/integrations/semaphore/。测试与验证本地开发闭环实现完成后按以下顺序验证对应 Makefile 中的目标格式化与静态检查运行make format.go make lint make check.build.app。其中check.build.app执行go build cmd/server/main.go见 Makefile确保后端可编译。后端单测运行make test。仓库内大量集成测试可供参考例如pkg/integrations/github/components/issues/on_issue_test.go测试应覆盖主功能、边界情况与错误处理且保持确定性任意顺序可跑。前端构建运行make check.build.ui该目标在web_src下执行npm run build见 Makefile保证 TS 代码可编译。E2E 测试考虑补充端到端测试流程参见 docs/contributing/e2e-tests.md。此外集成文档是根据代码自动生成的在后端代码完成后运行make gen.components.docs该目标会清空并重建docs/components/执行go run scripts/generate_components_docs.go见 Makefile为每个集成生成一份docs/components/Name.mdx文档如现有 docs/components/GitHub.mdx、docs/components/Semaphore.mdx。不要手写docs/components/下的文件统一走生成命令。最佳实践清单命名要有描述性Trigger/Component 名称应清晰表达其行为如github.onIssue、myintegration.onEvent。在Setup()中校验配置必填项缺失要返回明确错误避免运行时才发现问题。优雅处理错误返回恰当的 HTTP 状态码与错误信息如 400 解析失败、403 签名无效、500 内部错误。用 metadata 做缓存把频繁访问的数据如资源 ID、仓库名存入元数据减少重复 API 调用。尽早过滤事件在 webhook 处理的最前端按 action 过滤避免无谓处理与流程触发。精心设计默认配置默认值应覆盖最常见场景并避免产生多余事件。例如github.onPush默认只监听main分支的提交github.onIssue默认只监听opened动作。始终校验签名所有 webhook 都必须验签确保请求来自真实外部服务。完整文档化动作类型把 Trigger 支持的所有 action 类型写入Documentation()与配置选项方便 UI 与用户理解。保持前端样式一致复用getColorClass、getBackgroundColorClass等既有工具函数与bg-white等图标背景约定跟随仓库既有 Mapper 模式。相关资源GitHub 集成后端pkg/integrations/github/Semaphore 集成后端pkg/integrations/semaphore/核心接口定义pkg/core/integration.go、pkg/core/trigger.go、pkg/core/component.go注册机制pkg/registry/registry.go前端 Mapper 目录web_src/src/pages/app/mappers/集成提交 PR 规范docs/contributing/integration-prs.md集成文档生成脚本scripts/generate_components_docs.go赞分享【免费下载链接】superplaneOpen source factory for one-shot engineering项目地址https://gitcode.com/gh_mirrors/su/superplane点击查看免费下载相关推荐从零开始Langchain-Chatchat前端架构与集成指南从零开始Langchain Chatchat前端架构与集成指南 你是否在搭建本地知识库问答系统时遇到前端集成难题本文将带你系统了解Langchain Cha人工智能大模型RAGAI Agent本地部署后端eSearch 完整指南截屏、离线 OCR 与录屏如何串成一条工作流eSearch 完整指南截屏、离线 OCR 与录屏如何串成一条工作流 想把屏幕上的文字变成可编辑内容时你是否也卡在截图、OCR 网站、复制、翻译这一连串跳转桌面应用OCR屏幕录制视频处理图像处理IntelliJ IDEA前端框架集成指南从零开始掌握现代Web开发IntelliJ IDEA前端框架集成指南从零开始掌握现代Web开发 IntelliJ IDEA作为一款功能强大的IDE不仅擅长Java开发在前端框架集成文档教程开发工具上一篇MicroK8s社区贡献案例如何修复一个真实的bug下一篇如何在5分钟内实现Parsley.js与Tailwind CSS的完美集成终极表单验证解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表