WebHooker/docs/zh/guide/configuration.md
RhenCloud b600f02027
feat: per-group webhook ingress, custom webhooks, GitHub App tenant isolation
Add POST /webhook/{groupId} with per-group secrets (KV tenant:{groupId}), a custom provider (X-WebHooker-Signature HMAC, arbitrary JSON -> custom events through the route pipeline), and GitHub App installation isolation (Group.installationId) with automatic provisioning on installation.created (inst-{id} groups or binding matching owners groups). Includes WebhookPanel admin UI, custom route template, docs and 157 passing tests.
2026-08-13 09:24:50 +08:00

25 KiB
Raw Blame History

配置

密钥

WebHooker 需要多个密钥才能运行。本地开发时存储在 .dev.vars 中,生产环境使用 Cloudflare Worker Secrets。

必需密钥

变量 说明
GITHUB_WEBHOOK_SECRET GitHub App 设置中的 Webhook 密钥
GITEA_WEBHOOK_SECRET Gitea 实例的 Webhook 密钥(仅接收 Gitea webhook 时需要)
GITHUB_CLIENT_ID App 设置中的 OAuth 客户端 ID
GITHUB_CLIENT_SECRET App 设置中的 OAuth 客户端密钥
DISCORD_TOKEN Discord Bot Token
TELEGRAM_TOKEN Telegram Bot TokenBotFather 获取)—— Telegram 路由必需

Note

GITHUB_APP_IDGITHUB_PRIVATE_KEY 当前未被代码使用——OAuth 流程只需要 GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET。为兼容性保留在模式中,以备日后启用 GitHub App 认证。

可选密钥

变量 说明 默认值
BASE_URL OAuth 回调的公开 URL http://localhost:8787
ADMIN_USER_IDS 允许访问 WebUI 的 GitHub 用户 ID或登录名逗号分隔 未设置时 WebUI 关闭
ALLOW_SELF_SIGNUP 开启(1/true)后,没有任何分组权限的 GitHub 用户首次登录会自动获得个人分组而非 403 关闭
AUDIT_RETENTION_DAYS 定时清理时审计日志的保留天数 90
DISCORD_PUBLIC_KEY Discord 应用的公钥(开发者门户获取),交互功能必需 未设置时交互返回 401
DISCORD_APPLICATION_ID Discord 应用 ID省略时自动获取 自动获取
TELEGRAM_WEBHOOK_SECRET POST /telegram/webhook 验签密钥X-Telegram-Bot-Api-Secret-Token 未设置时不校验
TELEGRAM_RICH_HEADER_HOST 外部 rich-header 服务的基础 URL未设置时使用内置的 GET /api/richheader 生成 Telegram 头像卡片 内置 /api/richheader

Webhook 提供方

WebHooker 通过同一个 POST /webhook 端点接收多个 forge 的 webhook按请求头自动识别来源只需把各 forge 的 webhook 指向 {BASE_URL}/webhook 即可。

提供方 事件请求头 签名请求头 签名格式 密钥
GitHub X-GitHub-Event X-Hub-Signature-256 sha256=<hex> HMAC-SHA256 GITHUB_WEBHOOK_SECRET
Gitea X-Gitea-Event X-Gitea-Signature 纯 hex HMAC-SHA256 GITEA_WEBHOOK_SECRET

Gitea payload 会被归一化为与 GitHub 相同的内部结构,因此路由、过滤器与 28 个格式化器无需改动即可复用;未知或未映射的 Gitea 事件回退到通用格式化器。仓库/提交/用户链接基于 payload 的 repository.html_url 生成,会指向你的 Gitea 实例。

Web 控制台

WebHooker 内置了位于 /admin 的配置控制台,可在浏览器中管理路由。它由 GitHub OAuth 和管理员白名单保护。

设置

  1. 配置 ADMIN_USER_IDS,填写允许管理路由的 GitHub 用户 ID也支持登录名例如 ADMIN_USER_IDS=12345,RhenCloud。未设置时控制台禁用(除非开启 ALLOW_SELF_SIGNUP)。
  2. 打开 /admin 并使用 GitHub 登录。
  3. 没有任何权限的用户收到 403,除非开启 ALLOW_SELF_SIGNUP=1(自动获得个人分组)或通过分组邀请链接加入。

端点

控制台以 SPA 形式挂在 /admin,各标签页可通过 URL 路径直达(/admin/groups/admin/logs/admin/audit)。/admin 之外且未匹配下方端点的 URL 直接返回 404,不会再被吞进控制台。

端点 说明
GET /admin 配置控制台页面
GET /admin/login 开始 GitHub OAuth 登录
GET /admin/logout 销毁会话
GET /admin/invite?token=… 接受分组邀请(浏览器页面)
GET /admin/api/me 当前会话、权限范围、分组和角色
GET /admin/api/routes 列出路由(按权限过滤)
PUT /admin/api/routes 替换路由(按分组 owner/admin 权限)
GET /admin/api/groups 列出分组 + 当前用户在各组的角色
PUT /admin/api/groups 替换分组超管全量owner 仅自己的组)
GET /admin/api/groups/:id/routes 列出某分组的路由
PUT /admin/api/groups/:id/routes 替换某分组的路由owner/admin
GET /admin/api/logs 发送日志(按可访问路由过滤)
GET /admin/api/logs/:id 单条发送日志(按权限过滤)
POST /admin/api/groups/:id/invites 创建邀请链接owner
GET /admin/api/groups/:id/invites 列出待接受邀请owner
DELETE /admin/api/invites/:token 撤销邀请owner
GET /admin/api/audit 审计日志(按可访问分组过滤)
GET /admin/api/groups/:id/webhook 分组 webhook 入口信息owner
POST /admin/api/groups/:id/webhook/regenerate 生成/重新生成分组 webhook secretowner
DELETE /admin/api/groups/:id/webhook 停用分组 webhook 入口owner

控制台支持新增、编辑、删除和开关路由。保存后立即写入 KV config:routes 并使配置缓存失效,下一次 webhook 处理即会生效。

Webhook 端点

全局端点(POST /webhook

旧版全局端点使用运维者的全局 secretGITHUB_WEBHOOK_SECRETGITEA_WEBHOOK_SECRET)验签,可分发到所有路由。GitHub App 安装事件从该端点进入;多租户场景请用分组的 installationId 做隔离。

分组端点(POST /webhook/{groupId}

每个分组可以启用独立的 webhook 入口和 secret在分组页面「Webhook 入口」面板生成owner 权限)。载荷使用分组的 secret 验签且只有该分组的路由会触发。SaaS 用户可以借此配置 Gitea、classic GitHub 或自定义 webhook无需共享也无需知道运维者的全局 secret。

  • 支持所有 providerGitHubX-Hub-Signature-256、GiteaX-Gitea-Signature)、自定义(X-WebHooker-Signature
  • secret 为 64 位十六进制字符串;重新生成后旧值立即失效
  • 去重 key 按租户隔离(delivery:{groupId}:{id}
  • 分组未配置 secret或分组不存在时返回 404

自定义 Webhook

POST /webhook/{groupId}或全局端点POST 任意 JSON并用分组的 secret 对原始 body 计算 HMAC-SHA256 放在 X-WebHooker-Signature: sha256=<hex> 头中。载荷会变成 custom 事件走标准路由管线——创建一条 event: custom 的路由(控制台有模板)即可分发到该路由的目标,并自动记录 send_logs、出现在分组的日志频道。

载荷格式:

{
  "title": "Deploy failed",
  "description": "Prod rollout failed at 12:03 UTC",
  "color": "red",
  "url": "https://ci.example.com/runs/42",
  "repo": "acme/widget",
  "author": {
    "name": "alice",
    "iconUrl": "https://…/alice.png",
    "url": "https://github.com/alice"
  },
  "fields": [{ "name": "Env", "value": "prod", "inline": true }],
  "footer": "my-monitor",
  "deliveryId": "alert-123"
}
字段 类型 说明
title string 消息标题(缺失时回退为「自定义消息」)
description string 可选的消息正文
color string 可选消息颜色:颜色词(redgreenyellowbluepurpleorangecyangray)或 #rrggbb
url string 可选标题链接
repo string 可选 owner/repo;会加在标题前并作为 footer
author object 可选的 { name, iconUrl, url }
fields object[] 可选的嵌入字段 { name, value, inline }
footer string 可选的 footer 覆盖
deliveryId string 可选的发送方去重 id重试场景

GitHub App 租户隔离

GitHub App 安装后,所有安装方的事件都会到达全局端点。要让租户互相隔离,请把每个分组绑定到应当为其提供事件的安装 ID"installationId": 12345678。该 ID 可从 App 安装 webhook 载荷(installation.id)或 GitHub App 安装页 URL 看到。即使分组的 owners 为空,来自其它安装的事件也会被拒绝。未设置 installationId 的分组保持旧行为(owners 过滤)。

绑定是自动配置的:installation.created webhook 事件会自动创建绑定到该安装的专用分组 inst-{installationId}(名称为安装账号),或自动把 owners 匹配该账号的现有分组绑定到该安装。无需手动填写 ID——安装 App 后在控制台为自动创建的分组添加路由和成员即可。

路由

路由定义了哪些事件被转发到哪些频道Discord 或 Telegram。它们以 JSON 数组形式存储在 Cloudflare KV 中,键为 config:routes

没有默认路由——每条路由必须自行定义目标频道。若未配置任何路由,则不会转发任何事件。

路由模式

{
  "id": "unique-route-id",
  "name": "可读名称",
  "enabled": true,
  "groupId": "my-group",
  "fallback": false,
  "stop": false,
  "discordRoleIds": ["111111111111111111"],
  "filters": [
    { "type": "event", "match": "push" },
    { "type": "repo", "match": "org/repo", "exclude": false }
  ],
  "targets": [
    {
      "platform": "discord",
      "channelId": "必填频道ID",
      "threadId": "可选线程ID"
    }
  ]
}

targets 数组的每一项是一个推送目标,因此一条路由可同时转发到多个频道(例如同时发到 Discord 频道 Telegram 群组)。target.platform 选择推送目标:discord(默认)或 telegramDiscordtarget.channelIdtarget.threadId 可选的子区);Telegramtarget.chatId(群组/超级群组聊天 id-1001234567890target.topicId(话题的 message_thread_id,相当于 Discord 的子区)可选。不存在默认频道回退。

Discord 身份组提醒

在路由上设置 discordRoleIds,当该路由触发时会 @提醒ping一个或多个 Discord 身份组。<@&roleId> 形式的提醒会拼接到该路由所有 Discord 目标的消息正文开头Telegram 目标会忽略此字段。只有在机器人拥有 Mention Everyone 权限(或该身份组被标记为可被提及 mentionable且机器人能看到该身份组时提醒才会真正触发通知。

{
  "id": "release-notify",
  "name": "发布时提醒",
  "enabled": true,
  "groupId": "default",
  "discordRoleIds": ["111111111111111111", "222222222222222222"],
  "filters": [{ "type": "event", "match": "release" }],
  "targets": [{ "platform": "discord", "channelId": "必填频道ID" }]
}

也可以在管理控制台的“Discord 身份组提醒”中配置。

其他路由字段:

字段 类型 必需 说明
groupId string 该路由所属分组的 id
fallback boolean true 时,仅当没有其它路由匹配该事件时才发送,其自身过滤器会被忽略
stop boolean true 且该路由匹配时,停止评估后续路由
discordRoleIds string[] 该路由触发时要在 Discord 目标中 @提醒的身份组 id

自定义路由示例

[
  {
    "id": "backend-prs",
    "name": "后端 PR",
    "enabled": true,
    "groupId": "backend-team",
    "filters": [
      { "type": "repo", "match": "myorg/backend" },
      { "type": "event", "match": "pull_request" },
      { "type": "actor", "match": "[bot]", "exclude": true }
    ],
    "targets": [
      {
        "platform": "telegram",
        "chatId": "-1001234567890",
        "topicId": "9876543210"
      }
    ]
  }
]

分组

路由隶属于分组。分组用于限定管理权限,并可限制哪些事件允许流入。它们以 JSON 数组形式存储在 Cloudflare KV 中,键为 config:groups

分组模式

{
  "id": "backend-team",
  "name": "后端团队",
  "members": [
    { "login": "rhencloud", "role": "owner" },
    { "login": "octobot", "role": "admin" },
    { "login": "reader", "role": "viewer" }
  ],
  "owners": ["myorg"],
  "providers": ["github", "gitea"],
  "installationId": 12345678,
  "logTarget": { "platform": "discord", "channelId": "123456789", "threadId": "987654321" }
}
字段 类型 必需 说明
id string 小写 ida-z0-9-),由每条路由的 groupId 引用
name string 可读的分组名称
members object[] { login, role } 列表;角色为 owneradminviewer
adminIds string[] 已废弃的旧字段;存在时按 role 为 owner 的成员处理
owners string[] 允许事件进入该分组的组织/用户登录名;为空表示不限制
providers string[] 允许进入该分组的来源平台(githubgitea);为空表示全部
installationId number 绑定到该分组的 GitHub App 安装 ID只接受该安装的事件为空表示全部
emoji boolean 是否在该分组消息中显示 emoji默认 true
lang string 该分组所有路由的消息语言(如 enzh;可通过 KV i18n:<lang> 自定义)——默认 en
logTarget object Webhook 日志频道Discord 目标 { platform, channelId, threadId? } 或 Telegram 目标 { platform, chatId, topicId? },本分组路由每次投递 webhook 时都会向其发送摘要

角色

每个分组成员拥有三种角色之一。超级管理员(ADMIN_USER_IDS)始终绕过角色限制。

角色 查看路由/日志 编辑路由 管理成员与邀请 编辑分组设置
owner ✓(owners 除外)
admin
viewer ✓(只读)

权限模型

  • 超级管理员ADMIN_USER_IDS)可查看和编辑所有分组及全部路由;只有他们能修改分组的 owners 列表。
  • owner 管理本组的路由、成员、邀请、名称、emojiproviders;不能移除最后一位 owner也没有其他 owner 时不能把自己降级。
  • admin 可编辑本组路由并查看日志;viewer 只读控制台。
  • 分组管理端点通过 /admin/api/groups/:id/routes 一次只操作一个分组;groupId 由路径参数强制指定。
  • owners 列表限定哪些事件参与者(发送者登录名)的事件会被该分组的路由投递。
  • providers 列表限定哪个 forgegithubgitea)的事件会被该分组的路由投递。即使组织/用户同名,也可以借此将 GitHub 与 Gitea 分组区分开。

Webhook 日志频道

分组可以设置 logTarget 指向一个 Discord 频道/子区或 Telegram 群组/话题。每当该分组的路由分发dispatch一个 webhook就会向那里发送一条摘要消息事件类型/动作、仓库、投递 ID以及每条「路由 × 目标」一行的 / 结果(失败时附带错误信息)。全部成功时消息为绿色,任一失败则为红色。摘要使用分组的消息语言。日志消息尽力发送,本身不会被记入 D1 发送日志。

邀请

owner及超级管理员可在分组的「成员」面板创建一次性邀请链接7 天内有效。接受邀请后用户以邀请角色(adminviewer,绝不授予 owner)加入;已有的 viewer 会被升级为 admin。邀请存储在 KV invite:{token}

自助注册

开启 ALLOW_SELF_SIGNUP=1 后,没有分组权限的 GitHub 用户首次登录会获得一个由自己担任 owner 的个人分组(u-{userId}),而不是 403。这是全自助 SaaS 部署的入口;关闭它则控制台保持仅邀请制。

过滤器类型

实操指南见过滤器教程,包含完整示例。

类型 匹配对象 示例
event GitHub 事件名称 push, pull_request, issues
repo 仓库全名 org/repo
actor 发送者登录名 username, [bot]
action 事件操作 opened, closed, published
branch 分支名称 main, develop
keyword 载荷正文中的文本 deploy, /fix\s+\d+/ (正则)

过滤器行为

  • 路由中的所有过滤器必须都匹配才触发路由AND 逻辑)
  • 在任何过滤器上设置 "exclude": true 可反转匹配逻辑NOT 逻辑)
  • 非 keyword 过滤器为精确、不区分大小写的匹配——不支持通配符(repo: "org/*" 不会匹配任何内容)
  • keyword 过滤器支持正则表达式——正则有误或超过 200 个字符时回退到子串匹配
  • branch 过滤器适用于 push、pull_request、pull_request_review、pull_request_review_comment、create/delete、workflow_run、workflow_job、check_suite、deployment 和 code_scanning_alert 事件

匹配值

过滤器接受单个字符串或字符串数组:

{ "type": "event", "match": "push" }
{ "type": "event", "match": ["push", "pull_request"] }

KV 存储布局

键模式 TTL
config:routes JSON 路由数组 永久
config:groups JSON 分组数组 永久
session:{id} 管理员会话 { userId, login } 7 天
token:{userId} { userId, accessToken, expiresAt, refreshToken? } 0.9 × Token 有效期
token-reverse:{sha256} 用于按 Token 反查的用户 id 0.9 × Token 有效期
state:{hex} { redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? } 600 秒
invite:{token} { groupId, role, expiresAt, createdBy, note? } 7 天
invite:group:{id} 每组的 Token 索引(保证邀请列表一致性) 永久
delivery:{id} Webhook 投递 id去重标记 300 秒
msg:{routeId}:{key}:{target} 原地更新用消息 id 追踪(如 workflow_run / check_run 7 天
cmd:guild:{id} 已注册命令的服务器 id去重标记 永久
cmd:registered:global 全局命令已注册标记24h 去重) 1 天
config:discord-app-id Discord 应用 id 缓存 永久
i18n:{lang} 翻译覆盖,合并到英文之上 永久

D1 存储布局

D1 数据库(DB 绑定,数据库 webhooker)包含四张表:

用途
send_logs 每次分发尝试一行(路由 id、事件、目标、成功/失败、耗时、错误码、详情)
audit_logs 每次管理操作一行(登录/登出、分组/路由/成员/邀请变更)
discord_links 映射 discord_user_idgithub_user_id,用于 Discord /gh 命令
telegram_links 映射 telegram_user_idgithub_user_id,用于 Telegram /gh 命令

audit_logs 由定时触发器按 AUDIT_RETENTION_DAYS(默认 90自动清理。