Route.lang is removed; Group.lang now drives the message language for every route in the group (default en, custom via KV i18n:<lang>). dispatch.ts loads translations per group and falls back to en for groups without a lang. Admin UI: GroupEditor gains the language field, RouteEditor/RouteCard drop theirs; docs and config example updated.
18 KiB
配置
密钥
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 Token(BotFather 获取)—— Telegram 路由必需 |
Note
GITHUB_APP_ID与GITHUB_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 和管理员白名单保护。
设置
- 配置
ADMIN_USER_IDS,填写允许管理路由的 GitHub 用户 ID,也支持登录名,例如ADMIN_USER_IDS=12345,RhenCloud。未设置时控制台禁用(除非开启ALLOW_SELF_SIGNUP)。 - 打开
/admin并使用 GitHub 登录。 - 没有任何权限的用户收到
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 |
审计日志(按可访问分组过滤) |
控制台支持新增、编辑、删除和开关路由。保存后立即写入 KV config:routes 并使配置缓存失效,下一次 webhook 处理即会生效。
路由
路由定义了哪些事件被转发到哪些频道(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(默认)或 telegram。Discord 需 target.channelId(target.threadId 可选的子区);Telegram 需 target.chatId(群组/超级群组聊天 id,如 -1001234567890),target.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"]
}
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
id |
string | 是 | 小写 id(a-z0-9、-),由每条路由的 groupId 引用 |
name |
string | 是 | 可读的分组名称 |
members |
object[] | 否 | { login, role } 列表;角色为 owner、admin 或 viewer |
adminIds |
string[] | 否 | 已废弃的旧字段;存在时按 role 为 owner 的成员处理 |
owners |
string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 |
providers |
string[] | 否 | 允许进入该分组的来源平台(github、gitea);为空表示全部 |
emoji |
boolean | 否 | 是否在该分组消息中显示 emoji(默认 true) |
lang |
string | 否 | 该分组所有路由的消息语言(如 en、zh;可通过 KV i18n:<lang> 自定义)——默认 en |
角色
每个分组成员拥有三种角色之一。超级管理员(ADMIN_USER_IDS)始终绕过角色限制。
| 角色 | 查看路由/日志 | 编辑路由 | 管理成员与邀请 | 编辑分组设置 |
|---|---|---|---|---|
owner |
✓ | ✓ | ✓ | ✓(owners 除外) |
admin |
✓ | ✓ | ✗ | ✗ |
viewer |
✓(只读) | ✗ | ✗ | ✗ |
权限模型
- 超级管理员(
ADMIN_USER_IDS)可查看和编辑所有分组及全部路由;只有他们能修改分组的owners列表。 - owner 管理本组的路由、成员、邀请、名称、
emoji与providers;不能移除最后一位 owner,也没有其他 owner 时不能把自己降级。 - admin 可编辑本组路由并查看日志;viewer 只读控制台。
- 分组管理端点通过
/admin/api/groups/:id/routes一次只操作一个分组;groupId由路径参数强制指定。 owners列表限定哪些事件参与者(发送者登录名)的事件会被该分组的路由投递。providers列表限定哪个 forge(github、gitea)的事件会被该分组的路由投递。即使组织/用户同名,也可以借此将 GitHub 与 Gitea 分组区分开。
邀请
owner(及超级管理员)可在分组的「成员」面板创建一次性邀请链接,7 天内有效。接受邀请后用户以邀请角色(admin 或 viewer,绝不授予 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) |
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_id → github_user_id,用于 Discord /gh 命令 |
telegram_links |
映射 telegram_user_id → github_user_id,用于 Telegram /gh 命令 |
audit_logs 由定时触发器按 AUDIT_RETENTION_DAYS(默认 90)自动清理。