mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
docs: restructure into core-concept pages and split admin API
This commit is contained in:
parent
db49e1f01c
commit
a9e50fba50
27 changed files with 946 additions and 1203 deletions
163
README.md
163
README.md
|
|
@ -96,23 +96,7 @@ Routes are stored in KV (`config:routes` as JSON). There are **no default routes
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
`target.platform` selects the push target: `discord` (default) or `telegram`. Discord targets require `target.channelId` (optional `threadId` for a thread); Telegram targets require `target.chatId` (optional `topicId` for a topic). The legacy singular `target` field is still migrated automatically. There is no fallback to a default channel.
|
`target.platform` selects the push target: `discord` (default) or `telegram`. Discord targets require `target.channelId` (optional `threadId` for a thread); Telegram targets require `target.chatId` (optional `topicId` for a topic). Routes belong to **groups** (KV `config:groups`) that scope admin access and can restrict which org/user events flow in. See the [Routes & Targets](https://webhooker.docs.worldexecute.me/guide/routes) and [Groups & Access Control](https://webhooker.docs.worldexecute.me/guide/groups) guides for the full schema.
|
||||||
|
|
||||||
Set `discordRoleIds` on a route to ping Discord roles (身份组) whenever it fires:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "release-notify",
|
|
||||||
"name": "Notify on Release",
|
|
||||||
"enabled": true,
|
|
||||||
"groupId": "default",
|
|
||||||
"discordRoleIds": ["111111111111111111"],
|
|
||||||
"filters": [{ "type": "event", "match": "release" }],
|
|
||||||
"targets": [{ "platform": "discord", "channelId": "CHANNEL_ID" }]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Routes belong to **groups** (KV `config:groups`) that scope admin access and can restrict which org/user events flow in — including which source platform (`providers`: `github` / `gitea`). See `config.example.yaml` and `docs/guide/configuration.md` for the full schema.
|
|
||||||
|
|
||||||
### Web UI (`/admin`)
|
### Web UI (`/admin`)
|
||||||
|
|
||||||
|
|
@ -122,24 +106,11 @@ The built-in config console lets you manage routes and groups in the browser (ad
|
||||||
2. Visit `/admin` and sign in with GitHub. Users with no access get `403` — unless `ALLOW_SELF_SIGNUP=1` (they receive a personal group) or they follow a group invite link.
|
2. Visit `/admin` and sign in with GitHub. Users with no access get `403` — unless `ALLOW_SELF_SIGNUP=1` (they receive a personal group) or they follow a group invite link.
|
||||||
3. Changes are written to KV immediately and picked up by the webhook pipeline.
|
3. Changes are written to KV immediately and picked up by the webhook pipeline.
|
||||||
|
|
||||||
Sign out at `/admin/logout`.
|
Sign out at `/admin/logout`. Every group has `members` with a role (`owner` / `admin` / `viewer`); all admin operations are recorded in the D1 `audit_logs` table.
|
||||||
|
|
||||||
**Access model.** Every group has `members` with a role: `owner` (manage group, members, invites; edit routes), `admin` (edit routes, view logs), or `viewer` (read-only). Super admins (`ADMIN_USER_IDS`) bypass everything. Legacy `adminIds` are read as owners. Owners generate single-use, 7-day invite links from the group page; group admins and viewers can browse, admins edit, owners administer. All admin operations (logins, group/route/member/invite changes) are recorded in the D1 `audit_logs` table, pruned after `AUDIT_RETENTION_DAYS` (default 90).
|
|
||||||
|
|
||||||
See `config.example.yaml` for full syntax examples.
|
|
||||||
|
|
||||||
### Filter Types
|
### Filter Types
|
||||||
|
|
||||||
| Type | Matches | Notes |
|
Every filter supports plain text, `*`/`?` globs, and `/regex/` patterns (case-insensitive); set `exclude: true` to invert. See the [Filter Tutorial](https://webhooker.docs.worldexecute.me/guide/filters) for the pattern syntax and the full filter reference.
|
||||||
|-----------|----------------------------------------|--------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `event` | `push`, `pull_request`, `issues`, etc. | GitHub event name |
|
|
||||||
| `repo` | `org/repo` full name | |
|
|
||||||
| `actor` | Sender login | |
|
|
||||||
| `action` | `opened`, `closed`, `published`, etc. | |
|
|
||||||
| `branch` | Branch name | Works for push, PR/review, create/delete, workflow_run, workflow_job, check_suite, deployment, code_scanning_alert |
|
|
||||||
| `keyword` | Text in payload body | All filters support `*`/`?` globs and `/regex/` patterns (case-insensitive) |
|
|
||||||
|
|
||||||
Set `exclude: true` to invert any filter. See the [Filter Tutorial](https://webhooker.docs.worldexecute.me/guide/filters) for the pattern syntax.
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|
@ -180,62 +151,12 @@ Set `exclude: true` to invert any filter. See the [Filter Tutorial](https://webh
|
||||||
- `GET /admin/api/logs` — Send logs (scoped)
|
- `GET /admin/api/logs` — Send logs (scoped)
|
||||||
- `GET /admin/api/logs/:id` — Single send-log entry
|
- `GET /admin/api/logs/:id` — Single send-log entry
|
||||||
|
|
||||||
## GitHub App Setup
|
## Setup Guides
|
||||||
|
|
||||||
### 1. Create App
|
- **GitHub App** — create the app, subscribe to events, configure OAuth, and set the _Setup URL_ for tenant isolation: see [GitHub App Setup](https://webhooker.docs.worldexecute.me/guide/deployment#github-app-setup)
|
||||||
|
- **Discord bot** — create the bot, invite it with `applications.commands` (combined permission integer `274877910016`), and configure the Interactions Endpoint: see [Discord Bot Setup](https://webhooker.docs.worldexecute.me/guide/deployment#discord-bot-setup). The bot never connects to the Discord Gateway, so it shows as **offline** — messaging is unaffected (always REST).
|
||||||
1. Go to <https://github.com/settings/apps/new>
|
- **Telegram bot** — create the bot with [@BotFather](https://t.me/BotFather), set `TELEGRAM_TOKEN` (optional `TELEGRAM_WEBHOOK_SECRET`); the webhook is synced automatically by the scheduled trigger: see [Telegram Bot Setup](https://webhooker.docs.worldexecute.me/guide/deployment#telegram-bot-setup)
|
||||||
2. Fill in:
|
- **Deployment** — KV namespace, D1 database + migrations, secrets, and deploy: see the [Deployment guide](https://webhooker.docs.worldexecute.me/guide/deployment)
|
||||||
- **GitHub App name**: `WebHooker` (or your choice)
|
|
||||||
- **Homepage URL**: your domain
|
|
||||||
- **Webhook URL**: `https://your-domain/webhook`
|
|
||||||
- **Webhook secret**: generate and copy to `GITHUB_WEBHOOK_SECRET`
|
|
||||||
3. Set permissions:
|
|
||||||
- **Repository permissions**: Contents (read), Issues (write), Pull requests (write), Metadata (read), Checks (read), Deployments (read), Discussions (read), Code scanning alerts (read), Dependabot alerts (read)
|
|
||||||
- **Organization permissions**: Members (read) — if needed
|
|
||||||
4. Subscribe to events:
|
|
||||||
- Push, Pull request, Issues, Issue comment, Workflow run, Workflow job, Status, Deployment, Deployment status, Ping, Release, Create, Delete, Star, Fork, Check run, Check suite, Pull request review, Pull request review comment, Commit comment, Member, Label, Milestone, Discussion, Discussion comment, Repository, Code scanning alert, Dependabot alert
|
|
||||||
5. Generate private key — optional; `GITHUB_APP_ID` + `GITHUB_PRIVATE_KEY` are only used by the [App install flow](/guide/deployment#github-app-setup) to resolve the installing account's login on the post-install page.
|
|
||||||
|
|
||||||
### 2. Install App
|
|
||||||
|
|
||||||
1. After creation, go to the App settings page
|
|
||||||
2. Click "Install App" → select org/user
|
|
||||||
3. Choose repositories to monitor
|
|
||||||
|
|
||||||
### 3. Configure OAuth
|
|
||||||
|
|
||||||
1. Go to App → OAuth settings
|
|
||||||
2. Set **Callback URL**: `https://your-domain/auth/github/callback`
|
|
||||||
3. Copy Client ID and Client Secret to env
|
|
||||||
|
|
||||||
## Discord Bot Setup
|
|
||||||
|
|
||||||
Create a bot at <https://discord.com/developers/applications>, copy its token to `DISCORD_TOKEN`.
|
|
||||||
|
|
||||||
### OAuth2 Invite
|
|
||||||
|
|
||||||
Add the bot to your server with the `bot` scope and the following permissions:
|
|
||||||
|
|
||||||
| Permission | Value | Why |
|
|
||||||
|--------------------------|----------------|-------------------------------------------------|
|
|
||||||
| View Channels | `1024` | See the target channel to post messages |
|
|
||||||
| Send Messages | `2048` | Send embeds/messages to channels |
|
|
||||||
| Send Messages in Threads | `274877906944` | Send to threads when a route targets `threadId` |
|
|
||||||
|
|
||||||
Combined permission integer: `274877910016`
|
|
||||||
|
|
||||||
Invite URL (replace `CLIENT_ID` with your bot's client ID). The `applications.commands` scope is required so the slash / context-menu commands can be registered:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://discord.com/oauth2/authorize?client_id=YOUR_BOT_CLIENT_ID&permissions=274877910016&scope=bot+applications.commands
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interactions Endpoint
|
|
||||||
|
|
||||||
Copy the application **Public Key** (Developer Portal → General Information) to `DISCORD_PUBLIC_KEY` and set the **Interactions Endpoint URL** to `https://your-domain/discord/interactions`. All interactions (slash commands, buttons, modals) are verified with Ed25519 signatures.
|
|
||||||
|
|
||||||
The bot never connects to the Discord Gateway, so it shows as **offline** — messaging is unaffected (always REST).
|
|
||||||
|
|
||||||
### Bot Commands (comment on GitHub as yourself)
|
### Bot Commands (comment on GitHub as yourself)
|
||||||
|
|
||||||
|
|
@ -248,42 +169,6 @@ The bot registers native **slash** and **message context-menu** commands, synced
|
||||||
|
|
||||||
See the full reference in the [Bot Commands guide](https://webhooker.docs.worldexecute.me/guide/commands).
|
See the full reference in the [Bot Commands guide](https://webhooker.docs.worldexecute.me/guide/commands).
|
||||||
|
|
||||||
## Telegram Bot Setup
|
|
||||||
|
|
||||||
1. Create a bot with [@BotFather](https://t.me/BotFather) and copy its token to `TELEGRAM_TOKEN`.
|
|
||||||
2. (Optional) Set `TELEGRAM_WEBHOOK_SECRET`; the webhook registration passes it to Telegram as the `secret_token`, and `POST /telegram/webhook` verifies it with a timing-safe compare.
|
|
||||||
3. The worker syncs the webhook from the scheduled trigger (`setWebhook` to `{BASE_URL}/telegram/webhook`), so no manual `setWebhook` call is needed — just make sure `BASE_URL` is set.
|
|
||||||
4. Add the bot to a group (or enable topics) and route events to `chatId` / `topicId` in the route config.
|
|
||||||
|
|
||||||
In Telegram, `/gh` commands work by **replying to a notification message**: `/gh login`, `/gh logout`, `/gh comment <text>`, `/gh merge`, `/gh close`. See the [Bot Commands guide](https://webhooker.docs.worldexecute.me/guide/commands).
|
|
||||||
|
|
||||||
Avatars are rendered as a link-preview card using the built-in `GET /api/richheader` (overridable with `TELEGRAM_RICH_HEADER_HOST`).
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set secrets in Cloudflare
|
|
||||||
bunx wrangler secret put GITHUB_WEBHOOK_SECRET
|
|
||||||
bunx wrangler secret put GITHUB_CLIENT_ID
|
|
||||||
bunx wrangler secret put GITHUB_CLIENT_SECRET
|
|
||||||
bunx wrangler secret put DISCORD_TOKEN
|
|
||||||
bunx wrangler secret put DISCORD_PUBLIC_KEY
|
|
||||||
bunx wrangler secret put TELEGRAM_TOKEN
|
|
||||||
bunx wrangler secret put ADMIN_USER_IDS
|
|
||||||
|
|
||||||
# Create KV namespace
|
|
||||||
bunx wrangler kv namespace create KV
|
|
||||||
# Update wrangler.jsonc with the KV namespace ID
|
|
||||||
|
|
||||||
# Create D1 database and run migrations
|
|
||||||
bunx wrangler d1 create webhooker
|
|
||||||
# Update wrangler.jsonc d1_databases with the database ID
|
|
||||||
bun run db:migrate:prod # apply migrations to the remote D1 database
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
bunx wrangler deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -295,37 +180,7 @@ bun test # Unit tests
|
||||||
|
|
||||||
## Supported Events
|
## Supported Events
|
||||||
|
|
||||||
| Event | Formatter |
|
28 event formatters (push, pull_request, issues, workflow_run, release, ...) plus `custom` webhooks; unsupported events fall back to a generic formatter. See the full table with embed highlights in [Supported Events](https://webhooker.docs.worldexecute.me/events/supported).
|
||||||
|-------------------------------|---------------------------------------------------------|
|
|
||||||
| `push` | Commit list, branch, author |
|
|
||||||
| `pull_request` | PR title, branch, diff stats |
|
|
||||||
| `issues` | Issue title, labels, assignees |
|
|
||||||
| `issue_comment` | Comment body, issue reference |
|
|
||||||
| `workflow_run` | Workflow status, conclusion, duration (edited in place) |
|
|
||||||
| `workflow_job` | Job name, status, conclusion |
|
|
||||||
| `status` | Commit status, context, state |
|
|
||||||
| `deployment` | Environment, ref, task |
|
|
||||||
| `deployment_status` | Environment, status, commit ref |
|
|
||||||
| `check_run` | Status, conclusion, details URL (edited in place) |
|
|
||||||
| `check_suite` | Suite conclusion, head branch, commit |
|
|
||||||
| `ping` | Webhook confirmation |
|
|
||||||
| `release` | Tag, body, assets |
|
|
||||||
| `create` / `delete` | Branch/tag creation/deletion |
|
|
||||||
| `star` | Star count, repository |
|
|
||||||
| `fork` | Fork source → target |
|
|
||||||
| `pull_request_review` | Review state, body preview |
|
|
||||||
| `pull_request_review_comment` | Inline code comment, file path, line |
|
|
||||||
| `commit_comment` | Commit SHA, comment body |
|
|
||||||
| `member` | Collaborator add/remove |
|
|
||||||
| `label` | Label name, color, description |
|
|
||||||
| `milestone` | Progress bar, open/closed counts, due date |
|
|
||||||
| `discussion` | Discussion title, category, action |
|
|
||||||
| `discussion_comment` | Comment body, discussion reference |
|
|
||||||
| `repository` | Repo rename/transfer details |
|
|
||||||
| `code_scanning_alert` | Severity, rule ID, file path |
|
|
||||||
| `dependabot_alert` | Severity, package, vulnerable range, fix version |
|
|
||||||
|
|
||||||
Any other event type falls back to the generic formatter (event type, action, actor, repo, raw payload).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
164
README.zh.md
164
README.zh.md
|
|
@ -96,23 +96,7 @@ bunx wrangler dev # 启动本地开发服务器
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
`target.platform` 选择推送目标:`discord`(默认)或 `telegram`。Discord 目标需 `target.channelId`(可选 `threadId` 指向子区);Telegram 目标需 `target.chatId`(可选 `topicId` 指向话题)。旧的单数 `target` 字段仍会被自动迁移。不存在默认频道回退。
|
`target.platform` 选择推送目标:`discord`(默认)或 `telegram`。Discord 目标需 `target.channelId`(可选 `threadId` 指向子区);Telegram 目标需 `target.chatId`(可选 `topicId` 指向话题)。路由隶属于**分组**(KV `config:groups`),分组用于限定管理权限,并可限制哪些组织/用户的事件流入。完整模式见[路由与目标](https://webhooker.docs.worldexecute.me/zh/guide/routes)与[分组与访问控制](https://webhooker.docs.worldexecute.me/zh/guide/groups)指南。
|
||||||
|
|
||||||
在路由上设置 `discordRoleIds`,可在该路由触发时 @提醒 Discord 身份组:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "release-notify",
|
|
||||||
"name": "发布时提醒",
|
|
||||||
"enabled": true,
|
|
||||||
"groupId": "default",
|
|
||||||
"discordRoleIds": ["111111111111111111"],
|
|
||||||
"filters": [{ "type": "event", "match": "release" }],
|
|
||||||
"targets": [{ "platform": "discord", "channelId": "频道ID" }]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
路由隶属于**分组**(KV `config:groups`),分组用于限定管理权限,并可限制哪些组织/用户的事件流入——包括来源平台(`providers`:`github` / `gitea`)。完整模式见 `config.example.yaml` 与 `docs/zh/guide/configuration.md`。
|
|
||||||
|
|
||||||
### Web 控制台(`/admin`)
|
### Web 控制台(`/admin`)
|
||||||
|
|
||||||
|
|
@ -122,24 +106,11 @@ bunx wrangler dev # 启动本地开发服务器
|
||||||
2. 访问 `/admin` 并用 GitHub 登录。无任何权限的用户收到 `403`——除非开启 `ALLOW_SELF_SIGNUP=1`(自动获得个人分组)或通过分组邀请链接加入。
|
2. 访问 `/admin` 并用 GitHub 登录。无任何权限的用户收到 `403`——除非开启 `ALLOW_SELF_SIGNUP=1`(自动获得个人分组)或通过分组邀请链接加入。
|
||||||
3. 修改会立即写入 KV,webhook 管线随即生效。
|
3. 修改会立即写入 KV,webhook 管线随即生效。
|
||||||
|
|
||||||
在 `/admin/logout` 退出登录。
|
在 `/admin/logout` 退出登录。每个分组都有带角色的 `members`(`owner` / `admin` / `viewer`);所有管理操作(登录、分组/路由/成员/邀请变更)都会写入 D1 `audit_logs` 表。
|
||||||
|
|
||||||
**权限模型。** 每个分组都有带角色的 `members`:`owner`(管理分组、成员、邀请;可编辑路由)、`admin`(编辑路由、查看日志)、`viewer`(只读)。超级管理员(`ADMIN_USER_IDS`)绕过所有角色限制。旧的 `adminIds` 字段按 owner 读取。owner 可从分组页面生成一次性、7 天有效的邀请链接;所有管理操作(登录、分组/路由/成员/邀请变更)都会写入 D1 `audit_logs` 表,并按 `AUDIT_RETENTION_DAYS`(默认 90 天)自动清理。
|
|
||||||
|
|
||||||
完整语法示例见 `config.example.yaml`。
|
|
||||||
|
|
||||||
### 过滤器类型
|
### 过滤器类型
|
||||||
|
|
||||||
| 类型 | 匹配内容 | 备注 |
|
所有过滤器均支持纯文本、`*`/`?` 通配符与 `/正则/`(不区分大小写);设置 `exclude: true` 可取反。模式语法与完整过滤器参考见[过滤器教程](https://webhooker.docs.worldexecute.me/zh/guide/filters)。
|
||||||
|-----------|-------------------------------------|---------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `event` | `push`、`pull_request`、`issues` 等 | GitHub 事件名 |
|
|
||||||
| `repo` | `org/repo` 全名 | |
|
|
||||||
| `actor` | 发送者登录名 | |
|
|
||||||
| `action` | `opened`、`closed`、`published` 等 | |
|
|
||||||
| `branch` | 分支名 | 支持 push、PR/review、create/delete、workflow_run、workflow_job、check_suite、deployment、code_scanning_alert |
|
|
||||||
| `keyword` | payload 中的文本 | 所有过滤器均支持 `*`/`?` 通配符与 `/正则/`(不区分大小写) |
|
|
||||||
|
|
||||||
设置 `exclude: true` 可取反过滤器。模式语法见[过滤器教程](https://webhooker.docs.worldexecute.me/zh/guide/filters)。
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|
@ -180,61 +151,12 @@ bunx wrangler dev # 启动本地开发服务器
|
||||||
- `GET /admin/api/logs` — 发送日志(按权限过滤)
|
- `GET /admin/api/logs` — 发送日志(按权限过滤)
|
||||||
- `GET /admin/api/logs/:id` — 单条发送日志
|
- `GET /admin/api/logs/:id` — 单条发送日志
|
||||||
|
|
||||||
## GitHub App 配置教程
|
## 配置教程
|
||||||
|
|
||||||
### 1. 创建 App
|
- **GitHub App** — 创建应用、订阅事件、配置 OAuth 与 _Setup URL_(租户隔离):见 [GitHub App 配置](https://webhooker.docs.worldexecute.me/zh/guide/deployment#github-app-设置)
|
||||||
|
- **Discord 机器人** — 创建机器人、以 `applications.commands` scope 邀请(组合权限整数 `274877910016`)、配置 Interactions Endpoint:见 [Discord Bot 配置](https://webhooker.docs.worldexecute.me/zh/guide/deployment#discord-bot-设置)。bot 从不连接 Discord Gateway,因此显示为**离线**——消息推送不受影响(始终走 REST)。
|
||||||
1. 访问 <https://github.com/settings/apps/new>
|
- **Telegram 机器人** — 用 [@BotFather](https://t.me/BotFather) 创建机器人,设置 `TELEGRAM_TOKEN`(可选 `TELEGRAM_WEBHOOK_SECRET`);webhook 由定时任务自动同步:见 [Telegram 机器人配置](https://webhooker.docs.worldexecute.me/zh/guide/deployment#telegram-机器人配置)
|
||||||
2. 填写信息:
|
- **部署** — KV 命名空间、D1 数据库与迁移、密钥、部署:见[部署指南](https://webhooker.docs.worldexecute.me/zh/guide/deployment)
|
||||||
- **GitHub App name**:`WebHooker`(或自定义名称)
|
|
||||||
- **Homepage URL**:你的域名
|
|
||||||
- **Webhook URL**:`https://your-domain/webhook`
|
|
||||||
- **Webhook secret**:生成并复制到 `GITHUB_WEBHOOK_SECRET`
|
|
||||||
3. 设置权限:
|
|
||||||
- **Repository permissions**:Contents (read)、Issues (write)、Pull requests (write)、Metadata (read)、Checks (read)、Deployments (read)、Discussions (read)、Code scanning alerts (read)、Dependabot alerts (read)
|
|
||||||
- **Organization permissions**:Members (read) — 如需要
|
|
||||||
4. 订阅事件:Push、Pull request、Issues、Issue comment、Workflow run、Workflow job、Status、Deployment、Deployment status、Ping、Release、Create、Delete、Star、Fork、Check run、Check suite、Pull request review、Pull request review comment、Commit comment、Member、Label、Milestone、Discussion、Discussion comment、Repository、Code scanning alert、Dependabot alert
|
|
||||||
5. 生成私钥 — 可选;`GITHUB_APP_ID` + `GITHUB_PRIVATE_KEY` 仅用于 [App 安装流程](/zh/guide/deployment#github-app-设置)在安装后页面解析安装所属账号的登录名。
|
|
||||||
|
|
||||||
### 2. 安装 App
|
|
||||||
|
|
||||||
1. 创建后进入 App 设置页
|
|
||||||
2. 点击 "Install App" → 选择组织/用户
|
|
||||||
3. 选择要监控的仓库
|
|
||||||
|
|
||||||
### 3. 配置 OAuth
|
|
||||||
|
|
||||||
1. 进入 App → OAuth settings
|
|
||||||
2. 设置 **Callback URL**:`https://your-domain/auth/github/callback`
|
|
||||||
3. 复制 Client ID 和 Client Secret 到环境变量
|
|
||||||
|
|
||||||
## Discord 机器人配置
|
|
||||||
|
|
||||||
在 <https://discord.com/developers/applications> 创建机器人,将 Token 复制到 `DISCORD_TOKEN`。
|
|
||||||
|
|
||||||
### OAuth2 邀请
|
|
||||||
|
|
||||||
使用 `bot` scope 将机器人加入服务器,需要以下权限:
|
|
||||||
|
|
||||||
| 权限 | 数值 | 用途 |
|
|
||||||
|---------------------------------------------|----------------|------------------------------------------|
|
|
||||||
| 查看频道 (View Channels) | `1024` | 查看目标频道以发送消息 |
|
|
||||||
| 发送消息 (Send Messages) | `2048` | 向频道发送 embed/消息 |
|
|
||||||
| 在线程中发送消息 (Send Messages in Threads) | `274877906944` | 当路由配置了 `threadId` 时向线程发送消息 |
|
|
||||||
|
|
||||||
权限组合整数值:`274877910016`
|
|
||||||
|
|
||||||
邀请链接(将 `CLIENT_ID` 替换为机器人的 Client ID)。需要 `applications.commands` scope,否则无法注册斜杠 / 右键菜单命令:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://discord.com/oauth2/authorize?client_id=你的机器人CLIENT_ID&permissions=274877910016&scope=bot+applications.commands
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interactions Endpoint
|
|
||||||
|
|
||||||
将应用的 **Public Key**(开发者门户 → General Information)复制到 `DISCORD_PUBLIC_KEY`,并将 **Interactions Endpoint URL** 设为 `https://your-domain/discord/interactions`。所有交互(斜杠命令、按钮、modal)均通过 Ed25519 签名验证。
|
|
||||||
|
|
||||||
bot 从不连接 Discord Gateway,因此显示为**离线**——消息推送不受影响(始终走 REST)。
|
|
||||||
|
|
||||||
### Bot 指令(以本人身份评论 GitHub)
|
### Bot 指令(以本人身份评论 GitHub)
|
||||||
|
|
||||||
|
|
@ -247,46 +169,10 @@ bot 通过定时任务(每 5 分钟)同步注册原生的**斜杠命令**与
|
||||||
|
|
||||||
完整参考见[机器人命令指南](https://webhooker.docs.worldexecute.me/zh/guide/commands)。
|
完整参考见[机器人命令指南](https://webhooker.docs.worldexecute.me/zh/guide/commands)。
|
||||||
|
|
||||||
## Telegram 机器人配置
|
|
||||||
|
|
||||||
1. 用 [@BotFather](https://t.me/BotFather) 创建机器人,将 Token 复制到 `TELEGRAM_TOKEN`。
|
|
||||||
2. (可选)设置 `TELEGRAM_WEBHOOK_SECRET`;webhook 注册时会作为 `secret_token` 传给 Telegram,`POST /telegram/webhook` 使用时间安全比较校验。
|
|
||||||
3. Worker 会在定时任务中自动同步 webhook(`setWebhook` 指向 `{BASE_URL}/telegram/webhook`),因此无需手动调用 `setWebhook`——只需确保 `BASE_URL` 已设置。
|
|
||||||
4. 将机器人加入群组(或启用话题),在路由配置中用 `chatId` / `topicId` 指定目标。
|
|
||||||
|
|
||||||
在 Telegram 中,`/gh` 命令通过在通知消息上**回复**来使用:`/gh login`、`/gh logout`、`/gh comment <文本>`、`/gh merge`、`/gh close`。见[机器人命令指南](https://webhooker.docs.worldexecute.me/zh/guide/commands)。
|
|
||||||
|
|
||||||
头像使用内置 `GET /api/richheader` 渲染为链接预览卡片(可用 `TELEGRAM_RICH_HEADER_HOST` 覆盖)。
|
|
||||||
|
|
||||||
## 部署
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 在 Cloudflare 设置密钥
|
|
||||||
bunx wrangler secret put GITHUB_WEBHOOK_SECRET
|
|
||||||
bunx wrangler secret put GITHUB_CLIENT_ID
|
|
||||||
bunx wrangler secret put GITHUB_CLIENT_SECRET
|
|
||||||
bunx wrangler secret put DISCORD_TOKEN
|
|
||||||
bunx wrangler secret put DISCORD_PUBLIC_KEY
|
|
||||||
bunx wrangler secret put TELEGRAM_TOKEN
|
|
||||||
bunx wrangler secret put ADMIN_USER_IDS
|
|
||||||
|
|
||||||
# 创建 KV 命名空间
|
|
||||||
bunx wrangler kv namespace create KV
|
|
||||||
# 更新 wrangler.jsonc 中的 KV namespace ID
|
|
||||||
|
|
||||||
# 创建 D1 数据库并执行迁移
|
|
||||||
bunx wrangler d1 create webhooker
|
|
||||||
# 更新 wrangler.jsonc d1_databases 中的数据库 ID
|
|
||||||
bun run db:migrate:prod # 将迁移应用到远端 D1 数据库
|
|
||||||
|
|
||||||
# 部署
|
|
||||||
bunx wrangler deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
## 开发命令
|
## 开发命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bunx wrangler dev # 本地开发服务器(Miniflare)
|
bun run dev # Nuxt 开发服务器(HMR)
|
||||||
bun run typecheck # 类型检查
|
bun run typecheck # 类型检查
|
||||||
bun run lint # ESLint
|
bun run lint # ESLint
|
||||||
bun test # 单元测试
|
bun test # 单元测试
|
||||||
|
|
@ -294,37 +180,7 @@ bun test # 单元测试
|
||||||
|
|
||||||
## 支持的事件
|
## 支持的事件
|
||||||
|
|
||||||
| 事件 | 格式化内容 |
|
28 个事件格式化器(push、pull_request、issues、workflow_run、release 等)外加 `custom` webhook;不支持的事件回退到通用格式化器。带嵌入亮点的完整表格见[支持的事件](https://webhooker.docs.worldexecute.me/zh/events/supported)。
|
||||||
|-------------------------------|--------------------------------------|
|
|
||||||
| `push` | 提交列表、分支、作者 |
|
|
||||||
| `pull_request` | PR 标题、分支、差异统计 |
|
|
||||||
| `issues` | Issue 标题、标签、指派人 |
|
|
||||||
| `issue_comment` | 评论内容、Issue 引用 |
|
|
||||||
| `workflow_run` | 工作流状态、结论、耗时(原地更新) |
|
|
||||||
| `workflow_job` | 作业名、状态、结论 |
|
|
||||||
| `status` | 提交状态、上下文、状态值 |
|
|
||||||
| `deployment` | 环境、引用、任务 |
|
|
||||||
| `deployment_status` | 环境、状态、commit ref |
|
|
||||||
| `check_run` | 状态、结论、详情链接(原地更新) |
|
|
||||||
| `check_suite` | 套件结论、head 分支、提交 |
|
|
||||||
| `ping` | Webhook 确认 |
|
|
||||||
| `release` | Tag、内容、资产 |
|
|
||||||
| `create` / `delete` | 分支/tag 创建或删除 |
|
|
||||||
| `star` | Star 数量、仓库 |
|
|
||||||
| `fork` | Fork 来源 → 目标 |
|
|
||||||
| `pull_request_review` | 审查状态、内容预览 |
|
|
||||||
| `pull_request_review_comment` | 行内代码评论、文件路径、行号 |
|
|
||||||
| `commit_comment` | Commit SHA、评论内容 |
|
|
||||||
| `member` | 协作者添加/移除 |
|
|
||||||
| `label` | 标签名、颜色、描述 |
|
|
||||||
| `milestone` | 进度条、open/closed 计数、截止日期 |
|
|
||||||
| `discussion` | 讨论标题、分类、操作 |
|
|
||||||
| `discussion_comment` | 评论内容、讨论引用 |
|
|
||||||
| `repository` | 仓库重命名/转移详情 |
|
|
||||||
| `code_scanning_alert` | 严重程度、规则 ID、文件路径 |
|
|
||||||
| `dependabot_alert` | 严重程度、包名、受影响范围、修复版本 |
|
|
||||||
|
|
||||||
任何其他事件类型回退到通用格式化器(事件类型、操作、操作人、仓库、原始载荷)。
|
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,15 @@ export default defineConfig({
|
||||||
{ text: "Introduction", link: "/guide/introduction" },
|
{ text: "Introduction", link: "/guide/introduction" },
|
||||||
{ text: "Getting Started", link: "/guide/getting-started" },
|
{ text: "Getting Started", link: "/guide/getting-started" },
|
||||||
{ text: "Configuration", link: "/guide/configuration" },
|
{ text: "Configuration", link: "/guide/configuration" },
|
||||||
{ text: "Filter Tutorial", link: "/guide/filters" },
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Core Concepts",
|
||||||
|
items: [
|
||||||
|
{ text: "Routes & Targets", link: "/guide/routes" },
|
||||||
|
{ text: "Groups & Access Control", link: "/guide/groups" },
|
||||||
|
{ text: "Webhook Ingress & Tenancy", link: "/guide/ingress" },
|
||||||
|
{ text: "Storage Layout", link: "/guide/storage" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -43,6 +51,7 @@ export default defineConfig({
|
||||||
{ text: "Bot Commands", link: "/guide/commands" },
|
{ text: "Bot Commands", link: "/guide/commands" },
|
||||||
{ text: "Message Format", link: "/guide/message-format" },
|
{ text: "Message Format", link: "/guide/message-format" },
|
||||||
{ text: "Message Language (i18n)", link: "/guide/i18n" },
|
{ text: "Message Language (i18n)", link: "/guide/i18n" },
|
||||||
|
{ text: "Filter Tutorial", link: "/guide/filters" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -59,7 +68,8 @@ export default defineConfig({
|
||||||
{
|
{
|
||||||
text: "API Reference",
|
text: "API Reference",
|
||||||
items: [
|
items: [
|
||||||
{ text: "Overview", link: "/api/overview" },
|
{ text: "Overview (Public)", link: "/api/overview" },
|
||||||
|
{ text: "Admin API", link: "/api/admin" },
|
||||||
{ text: "OAuth", link: "/api/oauth" },
|
{ text: "OAuth", link: "/api/oauth" },
|
||||||
{ text: "Actions", link: "/api/actions" },
|
{ text: "Actions", link: "/api/actions" },
|
||||||
],
|
],
|
||||||
|
|
@ -112,7 +122,15 @@ export default defineConfig({
|
||||||
{ text: "简介", link: "/zh/guide/introduction" },
|
{ text: "简介", link: "/zh/guide/introduction" },
|
||||||
{ text: "快速开始", link: "/zh/guide/getting-started" },
|
{ text: "快速开始", link: "/zh/guide/getting-started" },
|
||||||
{ text: "配置", link: "/zh/guide/configuration" },
|
{ text: "配置", link: "/zh/guide/configuration" },
|
||||||
{ text: "过滤器教程", link: "/zh/guide/filters" },
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "核心概念",
|
||||||
|
items: [
|
||||||
|
{ text: "路由与目标", link: "/zh/guide/routes" },
|
||||||
|
{ text: "分组与访问控制", link: "/zh/guide/groups" },
|
||||||
|
{ text: "Webhook 接入与租户隔离", link: "/zh/guide/ingress" },
|
||||||
|
{ text: "存储布局", link: "/zh/guide/storage" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -121,6 +139,7 @@ export default defineConfig({
|
||||||
{ text: "机器人命令", link: "/zh/guide/commands" },
|
{ text: "机器人命令", link: "/zh/guide/commands" },
|
||||||
{ text: "消息格式", link: "/zh/guide/message-format" },
|
{ text: "消息格式", link: "/zh/guide/message-format" },
|
||||||
{ text: "消息语言 (i18n)", link: "/zh/guide/i18n" },
|
{ text: "消息语言 (i18n)", link: "/zh/guide/i18n" },
|
||||||
|
{ text: "过滤器教程", link: "/zh/guide/filters" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -137,7 +156,8 @@ export default defineConfig({
|
||||||
{
|
{
|
||||||
text: "API 参考",
|
text: "API 参考",
|
||||||
items: [
|
items: [
|
||||||
{ text: "概览", link: "/zh/api/overview" },
|
{ text: "概览(公共 API)", link: "/zh/api/overview" },
|
||||||
|
{ text: "Admin API", link: "/zh/api/admin" },
|
||||||
{ text: "OAuth", link: "/zh/api/oauth" },
|
{ text: "OAuth", link: "/zh/api/oauth" },
|
||||||
{ text: "用户操作", link: "/zh/api/actions" },
|
{ text: "用户操作", link: "/zh/api/actions" },
|
||||||
],
|
],
|
||||||
|
|
|
||||||
39
docs/api/admin.md
Normal file
39
docs/api/admin.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Admin API
|
||||||
|
|
||||||
|
Admin endpoints manage routes, groups, members, invites, webhook secrets, send logs, and the audit log. They require an admin session cookie obtained via `GET /admin/login` (GitHub OAuth); the signed-in user must be listed in `ADMIN_USER_IDS` or manage a group. See [Configuration → Web UI](../guide/configuration.md#web-ui) for setup.
|
||||||
|
|
||||||
|
The console itself is served at `/admin`; its tabs are deep-linkable via the URL path (`/admin/groups`, `/admin/logs`, `/admin/audit`).
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Description |
|
||||||
|
|-------------------------------------------------|-------------------------------------------------------------------|
|
||||||
|
| `GET /admin` | Config console UI |
|
||||||
|
| `GET /admin/login` | Start admin sign-in (GitHub OAuth) |
|
||||||
|
| `GET /admin/logout` | Sign out and destroy the session |
|
||||||
|
| `GET /admin/invite?token=…` | Accept a group invite (browser page) |
|
||||||
|
| `GET /admin/api/me` | Current session, scope, groups, and roles |
|
||||||
|
| `GET /admin/api/routes` | List routes (scoped to access) |
|
||||||
|
| `PUT /admin/api/routes` | Replace routes (owner/admin per group) |
|
||||||
|
| `GET /admin/api/groups` | List groups + the signed-in user's role in each |
|
||||||
|
| `PUT /admin/api/groups` | Replace groups (super: all; owner: own only) |
|
||||||
|
| `GET /admin/api/groups/:id/routes` | List a group's routes |
|
||||||
|
| `PUT /admin/api/groups/:id/routes` | Replace a group's routes (owner/admin) |
|
||||||
|
| `PUT /admin/api/groups/:id/rename` | Rename a group (owner); routes, webhook secret and invites follow |
|
||||||
|
| `GET /admin/api/groups/:id/invites` | List pending invites (owner) |
|
||||||
|
| `POST /admin/api/groups/:id/invites` | Create an invite link (owner) |
|
||||||
|
| `DELETE /admin/api/invites/:token` | Revoke an invite (owner) |
|
||||||
|
| `GET /admin/api/groups/:id/webhook` | Group webhook endpoint info (owner) |
|
||||||
|
| `POST /admin/api/groups/:id/webhook/regenerate` | Generate/regenerate the group webhook secret (owner) |
|
||||||
|
| `DELETE /admin/api/groups/:id/webhook` | Disable the group webhook ingress (owner) |
|
||||||
|
| `GET /admin/api/logs` | Send logs (scoped to accessible routes) |
|
||||||
|
| `GET /admin/api/logs/:id` | Single send-log entry (scoped) |
|
||||||
|
| `GET /admin/api/audit` | Audit log (scoped to accessible groups) |
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `PUT /admin/api/routes` — Body `{ "routes": Route[] }`; validates each route (id pattern, unique id within its group, name, enabled, `groupId`, filters — empty only allowed for `fallback` routes — optional `discordRoleIds` (list of role id strings), and platform-aware targets: `target.channelId` for Discord, `target.chatId` for Telegram) and persists to KV `config:routes`. Returns `200 { ok, count }` or `400 { error }` / `401 { error }` / `403 { error }`. Unchanged routes skip the full validation.
|
||||||
|
- `PUT /admin/api/groups` — Validates group ids, member roles (at least one `owner`), `providers` (`github` / `gitea`), and `installationId`.
|
||||||
|
- Limits: at most 200 routes and 100 groups per instance.
|
||||||
|
|
||||||
|
Schemas: [Routes & Targets](../guide/routes), [Groups & Access Control](../guide/groups).
|
||||||
|
|
@ -11,7 +11,7 @@ https://your-worker.workers.dev
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
| Method | Path | Auth | Description |
|
| Method | Path | Auth | Description |
|
||||||
|----------|--------------------------------------------|-------------------|--------------------------------------------------------------------|
|
|----------|---------------------------------|-------------------|--------------------------------------------------------------------|
|
||||||
| `GET` | `/health` | None | Health check |
|
| `GET` | `/health` | None | Health check |
|
||||||
| `POST` | `/webhook` | HMAC signature | GitHub / Gitea / custom webhook ingestion (provider auto-detected) |
|
| `POST` | `/webhook` | HMAC signature | GitHub / Gitea / custom webhook ingestion (provider auto-detected) |
|
||||||
| `POST` | `/webhook/:groupId` | Per-group secret | Per-group webhook ingress (only that group's routes fire) |
|
| `POST` | `/webhook/:groupId` | Per-group secret | Per-group webhook ingress (only that group's routes fire) |
|
||||||
|
|
@ -31,31 +31,12 @@ https://your-worker.workers.dev
|
||||||
| `GET` | `/admin/login` | None | Start admin sign-in (GitHub OAuth) |
|
| `GET` | `/admin/login` | None | Start admin sign-in (GitHub OAuth) |
|
||||||
| `GET` | `/admin/logout` | Admin session | Sign out and destroy the session |
|
| `GET` | `/admin/logout` | Admin session | Sign out and destroy the session |
|
||||||
| `GET` | `/admin/invite` | Admin session | Accept a group invite (browser page, `?token=…`) |
|
| `GET` | `/admin/invite` | Admin session | Accept a group invite (browser page, `?token=…`) |
|
||||||
| `GET` | `/admin/api/me` | Admin session | Current session, scope, groups, and roles |
|
|
||||||
| `GET` | `/admin/api/routes` | Admin session | List routes (scoped to access) |
|
The `/admin/api/*` endpoints (routes, groups, members, invites, webhook secrets, send logs, audit log) are documented separately in the [Admin API](./admin).
|
||||||
| `PUT` | `/admin/api/routes` | Admin session | Replace routes (owner/admin per group) |
|
|
||||||
| `GET` | `/admin/api/groups` | Admin session | List groups + the signed-in user's role in each |
|
|
||||||
| `PUT` | `/admin/api/groups` | Admin session | Replace groups (super: all; owner: own only) |
|
|
||||||
| `GET` | `/admin/api/groups/:id/routes` | Admin session | List a group's routes |
|
|
||||||
| `PUT` | `/admin/api/groups/:id/routes` | Admin session | Replace a group's routes (owner/admin) |
|
|
||||||
| `PUT` | `/admin/api/groups/:id/rename` | Admin session | Rename a group (owner); routes/secret/invites follow |
|
|
||||||
| `GET` | `/admin/api/groups/:id/invites` | Admin session | List pending invites (owner) |
|
|
||||||
| `POST` | `/admin/api/groups/:id/invites` | Admin session | Create an invite link (owner) |
|
|
||||||
| `DELETE` | `/admin/api/invites/:token` | Admin session | Revoke an invite (owner) |
|
|
||||||
| `GET` | `/admin/api/groups/:id/webhook` | Admin session | Group webhook endpoint info (owner) |
|
|
||||||
| `POST` | `/admin/api/groups/:id/webhook/regenerate` | Admin session | Generate/regenerate the group webhook secret (owner) |
|
|
||||||
| `DELETE` | `/admin/api/groups/:id/webhook` | Admin session | Disable the group webhook ingress (owner) |
|
|
||||||
| `GET` | `/admin/api/logs` | Admin session | Send logs (scoped) |
|
|
||||||
| `GET` | `/admin/api/logs/:id` | Admin session | Single send-log entry (scoped) |
|
|
||||||
| `GET` | `/admin/api/audit` | Admin session | Audit log (scoped to accessible groups) |
|
|
||||||
|
|
||||||
## Admin Console
|
## Admin Console
|
||||||
|
|
||||||
See [Configuration → Web UI](../guide/configuration.md#web-ui) for setup. Admin endpoints require a session cookie obtained via `GET /admin/login` (GitHub OAuth); the signed-in user must be listed in `ADMIN_USER_IDS` or manage a group.
|
See [Configuration → Web UI](../guide/configuration.md#web-ui) for setup, and the [Admin API](./admin) reference for all management endpoints. Admin endpoints require a session cookie obtained via `GET /admin/login` (GitHub OAuth); the signed-in user must be listed in `ADMIN_USER_IDS` or manage a group.
|
||||||
|
|
||||||
- `GET /admin` — Serves the config console HTML
|
|
||||||
- `GET /admin/api/routes` — Returns `{ "routes": Route[] }`
|
|
||||||
- `PUT /admin/api/routes` — Body `{ "routes": Route[] }`; validates each route (id pattern, unique id, name, enabled, `groupId`, filters — empty only allowed for `fallback` routes — optional `discordRoleIds` (list of role id strings), and platform-aware targets: `target.channelId` for Discord, `target.chatId` for Telegram) and persists to KV `config:routes`. Returns `200 { ok, count }` or `400 { error }` / `401 { error }` / `403 { error }`.
|
|
||||||
|
|
||||||
## Health Check
|
## Health Check
|
||||||
|
|
||||||
|
|
@ -82,7 +63,7 @@ Accepts GitHub webhook payloads. Requires valid `X-Hub-Signature-256` header.
|
||||||
**Headers:**
|
**Headers:**
|
||||||
|
|
||||||
| Header | Required | Description |
|
| Header | Required | Description |
|
||||||
|-----------------------|----------|--------------------------------------------------|
|
|-------------------------|------------|----------------------------------------------------|
|
||||||
| `X-Hub-Signature-256` | Yes | HMAC-SHA256 signature |
|
| `X-Hub-Signature-256` | Yes | HMAC-SHA256 signature |
|
||||||
| `X-GitHub-Event` | Yes | Event type name |
|
| `X-GitHub-Event` | Yes | Event type name |
|
||||||
| `X-GitHub-Delivery` | No | Unique delivery ID (used for dedup when present) |
|
| `X-GitHub-Delivery` | No | Unique delivery ID (used for dedup when present) |
|
||||||
|
|
@ -102,7 +83,7 @@ When `X-GitHub-Delivery` is present and the same delivery was already processed
|
||||||
**Error Responses:**
|
**Error Responses:**
|
||||||
|
|
||||||
| Status | Body | Cause |
|
| Status | Body | Cause |
|
||||||
|--------|----------------------------------|----------------------------------------|
|
|----------|------------------------------------|------------------------------------------|
|
||||||
| `401` | `{"error": "Invalid signature"}` | Signature verification failed |
|
| `401` | `{"error": "Invalid signature"}` | Signature verification failed |
|
||||||
| `400` | `{"error": "Invalid event"}` | Missing event header or malformed body |
|
| `400` | `{"error": "Invalid event"}` | Missing event header or malformed body |
|
||||||
| `413` | `{"error": "Request too large"}` | Body exceeds 1MB limit |
|
| `413` | `{"error": "Request too large"}` | Body exceeds 1MB limit |
|
||||||
|
|
@ -113,16 +94,16 @@ Verifies the payload against the **group's** secret (KV `tenant:{groupId}`, gene
|
||||||
|
|
||||||
### Custom Webhooks
|
### Custom Webhooks
|
||||||
|
|
||||||
Any JSON payload signed with `X-WebHooker-Signature: sha256=<hex>` (HMAC-SHA256 of the raw body, group or global secret) becomes a `custom` event. Route it with a route whose filter is `event: custom`. Payload schema: see [Configuration → Custom webhooks](../guide/configuration.md#custom-webhooks).
|
Any JSON payload signed with `X-WebHooker-Signature: sha256=<hex>` (HMAC-SHA256 of the raw body, group or global secret) becomes a `custom` event. Route it with a route whose filter is `event: custom`. Payload schema: see [Configuration → Custom webhooks](../guide/ingress.md#custom-webhooks).
|
||||||
|
|
||||||
### GitHub App Installation Events
|
### GitHub App Installation Events
|
||||||
|
|
||||||
`installation` webhook events (`created`, ...) are auto-provisioned as a fallback: a group named after the installing account (`inst-{installationId}`, bound via `installationId`) is created automatically, or existing groups whose `owners` match the installing account are bound to the installation. See [Configuration → GitHub App tenant isolation](../guide/configuration.md#github-app-tenant-isolation).
|
`installation` webhook events (`created`, ...) are auto-provisioned as a fallback: a group named after the installing account (`inst-{installationId}`, bound via `installationId`) is created automatically, or existing groups whose `owners` match the installing account are bound to the installation. See [Configuration → GitHub App tenant isolation](../guide/ingress.md#github-app-tenant-isolation).
|
||||||
|
|
||||||
The primary flow is the App's **Setup URL** — set it to `{BASE_URL}/auth/github/install`. After a user installs the App, the browser lands on:
|
The primary flow is the App's **Setup URL** — set it to `{BASE_URL}/auth/github/install`. After a user installs the App, the browser lands on:
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|-----------------------------|------------------------------------------------------------------------------------------------|
|
|----------|-------------------------------|--------------------------------------------------------------------------------------------------|
|
||||||
| `GET` | `/auth/github/install` | Choice page: bind the installation to a new group or an existing group the signed-in user owns |
|
| `GET` | `/auth/github/install` | Choice page: bind the installation to a new group or an existing group the signed-in user owns |
|
||||||
| `POST` | `/auth/github/install/bind` | Provisions the binding (owner role re-checked) and redirects to `/admin?install=ok` |
|
| `POST` | `/auth/github/install/bind` | Provisions the binding (owner role re-checked) and redirects to `/admin?install=ok` |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ WebHooker supports 28 GitHub webhook event types, each with a dedicated formatte
|
||||||
## Events Table
|
## Events Table
|
||||||
|
|
||||||
| Event | Description | Embed Highlights |
|
| Event | Description | Embed Highlights |
|
||||||
|-------------------------------|--------------------------------|------------------------------------------------------------------------------------------------------------------------|
|
|---------------------------------|----------------------------------|--------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `push` | Code pushed to a branch | Commit list, branch, author, diff stats |
|
| `push` | Code pushed to a branch | Commit list, branch, author, diff stats |
|
||||||
| `pull_request` | PR opened/closed/merged/edited | PR title, branch, diff stats, labels |
|
| `pull_request` | PR opened/closed/merged/edited | PR title, branch, diff stats, labels |
|
||||||
| `issues` | Issue opened/closed/edited | Issue title, labels, assignees |
|
| `issues` | Issue opened/closed/edited | Issue title, labels, assignees |
|
||||||
|
|
@ -34,14 +34,14 @@ WebHooker supports 28 GitHub webhook event types, each with a dedicated formatte
|
||||||
| `repository` | Repo renamed/transferred | Old → new name, changes |
|
| `repository` | Repo renamed/transferred | Old → new name, changes |
|
||||||
| `code_scanning_alert` | Code scanning alert | Severity, rule ID, file path |
|
| `code_scanning_alert` | Code scanning alert | Severity, rule ID, file path |
|
||||||
| `dependabot_alert` | Dependabot alert | Severity, package, vulnerable range, fix version |
|
| `dependabot_alert` | Dependabot alert | Severity, package, vulnerable range, fix version |
|
||||||
| `custom` | Signed custom JSON webhook | Arbitrary title/description/color/url/author/fields (see [Custom webhooks](../guide/configuration.md#custom-webhooks)) |
|
| `custom` | Signed custom JSON webhook | Arbitrary title/description/color/url/author/fields (see [Custom webhooks](../guide/ingress.md#custom-webhooks)) |
|
||||||
|
|
||||||
## Color Coding
|
## Color Coding
|
||||||
|
|
||||||
Each event type uses a distinct color in the Discord embed (from `server/lib/formatters/colors.ts`):
|
Each event type uses a distinct color in the Discord embed (from `server/lib/formatters/colors.ts`):
|
||||||
|
|
||||||
| Color | Events |
|
| Color | Events |
|
||||||
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| Green (`#2da44e`) | push, PR opened / ready for review, issue opened, workflow success, release published, check success, review approved, deployment success, member added, milestone closed, discussion answered |
|
| Green (`#2da44e`) | push, PR opened / ready for review, issue opened, workflow success, release published, check success, review approved, deployment success, member added, milestone closed, discussion answered |
|
||||||
| Red (`#f85149`) | PR closed, issue closed, workflow failure, release deleted, delete, check failure, review changes requested, deployment failure, member removed, code scanning / dependabot critical & high |
|
| Red (`#f85149`) | PR closed, issue closed, workflow failure, release deleted, delete, check failure, review changes requested, deployment failure, member removed, code scanning / dependabot critical & high |
|
||||||
| Purple (`#8957e5`) | PR merged, label, discussion |
|
| Purple (`#8957e5`) | PR merged, label, discussion |
|
||||||
|
|
@ -67,7 +67,7 @@ Any event type without a dedicated formatter falls through to the generic format
|
||||||
See the [Filter Tutorial](../guide/filters) for a hands-on guide with worked examples.
|
See the [Filter Tutorial](../guide/filters) for a hands-on guide with worked examples.
|
||||||
|
|
||||||
| Filter | Works With |
|
| Filter | Works With |
|
||||||
|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `event` | All events |
|
| `event` | All events |
|
||||||
| `repo` | All events |
|
| `repo` | All events |
|
||||||
| `actor` | All events |
|
| `actor` | All events |
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
|
This page is the reference for secrets and the Web UI. Core concepts live in dedicated pages:
|
||||||
|
|
||||||
|
| Topic | Page |
|
||||||
|
|---------------------------------------------------|----------------------------------------------------------------------|
|
||||||
|
| Routes, targets, `fallback` / `stop`, role pings | [Routes & Targets](./routes) |
|
||||||
|
| Groups, roles, invites, self sign-up, log channel | [Groups & Access Control](./groups) |
|
||||||
|
| Webhook providers, per-group ingress, custom | [Webhook Ingress & Tenancy](./ingress) |
|
||||||
|
| KV / D1 key layout | [Storage Layout](./storage) |
|
||||||
|
| Filters (pattern syntax reference) | [Filter Types](#filter-types) below / [Filter Tutorial](./filters) |
|
||||||
|
|
||||||
## Secrets
|
## Secrets
|
||||||
|
|
||||||
WebHooker requires several secrets to function. For local development, store them in `.dev.vars`. For production, use Cloudflare Worker Secrets.
|
WebHooker requires several secrets to function. For local development, store them in `.dev.vars`. For production, use Cloudflare Worker Secrets.
|
||||||
|
|
@ -38,266 +48,19 @@ WebHooker requires several secrets to function. For local development, store the
|
||||||
| `NUXT_PUBLIC_REPO_URL` | GitHub repo URL used by the landing page | Landing page defaults |
|
| `NUXT_PUBLIC_REPO_URL` | GitHub repo URL used by the landing page | Landing page defaults |
|
||||||
| `NUXT_PUBLIC_LEGAL_CONTACT` | Contact shown on `/terms` and `/privacy` | Unset → placeholder text |
|
| `NUXT_PUBLIC_LEGAL_CONTACT` | Contact shown on `/terms` and `/privacy` | Unset → placeholder text |
|
||||||
|
|
||||||
## Webhook Providers
|
|
||||||
|
|
||||||
WebHooker ingests webhooks from multiple forges through the same `POST /webhook` endpoint; the provider is auto-detected from the request headers, so point every forge's webhook at `{BASE_URL}/webhook`.
|
|
||||||
|
|
||||||
| Provider | Event header | Signature header | Signature format | Secret |
|
|
||||||
|----------|------------------|-----------------------|----------------------------|-------------------------|
|
|
||||||
| GitHub | `X-GitHub-Event` | `X-Hub-Signature-256` | `sha256=<hex>` HMAC-SHA256 | `GITHUB_WEBHOOK_SECRET` |
|
|
||||||
| Gitea | `X-Gitea-Event` | `X-Gitea-Signature` | plain hex HMAC-SHA256 | `GITEA_WEBHOOK_SECRET` |
|
|
||||||
|
|
||||||
Delivery-id dedup uses `X-GitHub-Delivery` (GitHub) or `X-Gitea-Delivery` (Gitea) when present.
|
|
||||||
|
|
||||||
Gitea payloads are normalized to the same internal shape as GitHub events, so routes, filters, and the 28 formatters work unchanged. Unknown or unmapped Gitea events fall back to the generic formatter. Repository/commit/user links are derived from the payload's `repository.html_url`, so they point at your Gitea instance.
|
|
||||||
|
|
||||||
## Web UI
|
## Web UI
|
||||||
|
|
||||||
WebHooker ships with a built-in config console at `/admin` for managing routes in the browser. It is protected by GitHub OAuth plus an admin whitelist.
|
WebHooker ships with a built-in config console at `/admin` for managing routes, groups, members, invites, send logs, and the audit log in the browser. It is protected by GitHub OAuth plus an admin whitelist.
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Configure `ADMIN_USER_IDS` with the GitHub user IDs allowed to manage everything. Logins are also accepted, e.g. `ADMIN_USER_IDS=12345,RhenCloud`. If unset, the console is disabled (unless `ALLOW_SELF_SIGNUP` is enabled).
|
1. Configure `ADMIN_USER_IDS` with the GitHub user IDs allowed to manage everything. Logins are also accepted, e.g. `ADMIN_USER_IDS=12345,RhenCloud`. If unset, the console is disabled (unless `ALLOW_SELF_SIGNUP` is enabled).
|
||||||
2. Open `/admin` and sign in with GitHub.
|
2. Open `/admin` and sign in with GitHub.
|
||||||
3. Users without any access get `403`, except when `ALLOW_SELF_SIGNUP=1` (they receive a personal group) or when they follow a group [invite link](#invites).
|
3. Users without any access get `403`, except when `ALLOW_SELF_SIGNUP=1` (they receive a personal group) or when they follow a group [invite link](./groups#invites).
|
||||||
|
|
||||||
### Endpoints
|
The console is served as an SPA at `/admin`; its tabs are deep-linkable via the URL path (`/admin/groups`, `/admin/logs`, `/admin/audit`). URLs outside `/admin` that do not match an endpoint return a plain `404` instead of the console.
|
||||||
|
|
||||||
The console is served as an SPA at `/admin`; its tabs are deep-linkable via the URL path (`/admin/groups`, `/admin/logs`, `/admin/audit`). URLs outside `/admin` that do not match an endpoint below return a plain `404` instead of the console.
|
All management endpoints (`/admin/api/*`) are documented in the [Admin API](../api/admin). Saved routes are written to KV `config:routes` immediately and the config cache is invalidated so the webhook pipeline picks them up on the next run.
|
||||||
|
|
||||||
| Endpoint | Description |
|
|
||||||
|-------------------------------------------------|-------------------------------------------------------------------|
|
|
||||||
| `GET /admin` | Config console UI |
|
|
||||||
| `GET /admin/login` | Start GitHub OAuth sign-in |
|
|
||||||
| `GET /admin/logout` | Destroy session |
|
|
||||||
| `GET /admin/invite?token=…` | Accept a group invite (browser page) |
|
|
||||||
| `GET /admin/api/me` | Current session, scope, groups, and roles |
|
|
||||||
| `GET /admin/api/routes` | List routes (scoped to access) |
|
|
||||||
| `PUT /admin/api/routes` | Replace routes (owner/admin per group) |
|
|
||||||
| `GET /admin/api/groups` | List groups + the signed-in user's role each |
|
|
||||||
| `PUT /admin/api/groups` | Replace groups (super: all; owner: own only) |
|
|
||||||
| `GET /admin/api/groups/:id/routes` | List a group's routes |
|
|
||||||
| `PUT /admin/api/groups/:id/routes` | Replace a group's routes (owner/admin) |
|
|
||||||
| `PUT /admin/api/groups/:id/rename` | Rename a group (owner); routes, webhook secret and invites follow |
|
|
||||||
| `GET /admin/api/logs` | Send logs (scoped to accessible routes) |
|
|
||||||
| `GET /admin/api/logs/:id` | Single send-log entry (scoped) |
|
|
||||||
| `POST /admin/api/groups/:id/invites` | Create an invite link (owner) |
|
|
||||||
| `GET /admin/api/groups/:id/invites` | List pending invites (owner) |
|
|
||||||
| `DELETE /admin/api/invites/:token` | Revoke an invite (owner) |
|
|
||||||
| `GET /admin/api/audit` | Audit log (scoped to accessible groups) |
|
|
||||||
| `GET /admin/api/groups/:id/webhook` | Group webhook endpoint info (owner) |
|
|
||||||
| `POST /admin/api/groups/:id/webhook/regenerate` | Generate/regenerate the group webhook secret (owner) |
|
|
||||||
| `DELETE /admin/api/groups/:id/webhook` | Disable the group webhook ingress (owner) |
|
|
||||||
|
|
||||||
The console lets you add, edit, delete, and toggle routes. Saved routes are written to KV `config:routes` immediately and the config cache is invalidated so the webhook pipeline picks them up on the next run. Limits: at most **200 routes** and **100 groups** per instance.
|
|
||||||
|
|
||||||
## Webhook Endpoints
|
|
||||||
|
|
||||||
### Global endpoint (`POST /webhook`)
|
|
||||||
|
|
||||||
The legacy global endpoint verifies payloads against the operator's global secrets (`GITHUB_WEBHOOK_SECRET`, `GITEA_WEBHOOK_SECRET`) and dispatches into **all** routes. GitHub App installations deliver here; use `installationId` on groups to keep tenants isolated.
|
|
||||||
|
|
||||||
### Per-group endpoint (`POST /webhook/{groupId}`)
|
|
||||||
|
|
||||||
Every group can opt into its own webhook ingress with an independent secret (generated from the group page — Webhook endpoint panel, owner role). Payloads are verified against the **group's** secret instead of the global ones, and only that group's routes are eligible. This is how SaaS users configure Gitea, classic GitHub, or custom webhooks without sharing (or knowing) the operator's secrets.
|
|
||||||
|
|
||||||
- Supported for any provider: GitHub (`X-Hub-Signature-256`), Gitea (`X-Gitea-Signature`), custom (`X-WebHooker-Signature`)
|
|
||||||
- The secret is a 64-char hex string; regenerate from the console invalidates the old one immediately
|
|
||||||
- Delivery-id dedup keys are tenant-scoped (`delivery:{groupId}:{id}`)
|
|
||||||
- When the group has no secret (or no longer exists) the endpoint returns `404`
|
|
||||||
|
|
||||||
### Custom webhooks
|
|
||||||
|
|
||||||
Post arbitrary JSON to `POST /webhook/{groupId}` (or the global endpoint) with the body signed as `X-WebHooker-Signature: sha256=<hmac-sha256 hex of the raw body>` using the group's secret. The payload becomes a `custom` event that flows through the normal route pipeline — create a route with `event: custom` (there is a console template) and it dispatches to that route's targets, records `send_logs`, and appears in the group's webhook log channel.
|
|
||||||
|
|
||||||
Payload schema:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|---------------|----------|------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `title` | string | Message title (falls back to "Custom message") |
|
|
||||||
| `description` | string | Optional message body |
|
|
||||||
| `color` | string | Optional embed color: a word (`red`, `green`, `yellow`, `blue`, `purple`, `orange`, `cyan`, `gray`) or `#rrggbb` |
|
|
||||||
| `url` | string | Optional link for the title |
|
|
||||||
| `repo` | string | Optional `owner/repo`; prefixes the title and is used as the footer |
|
|
||||||
| `author` | object | Optional `{ name, iconUrl, url }` |
|
|
||||||
| `fields` | object[] | Optional embed fields `{ name, value, inline }` |
|
|
||||||
| `footer` | string | Optional footer override |
|
|
||||||
| `deliveryId` | string | Optional id for sender-side dedup (retries) |
|
|
||||||
|
|
||||||
### GitHub App tenant isolation
|
|
||||||
|
|
||||||
When the GitHub App is installed, its events arrive at the global endpoint for **every** installation. To keep tenants apart, bind each group to the installation id that should feed it: `"installationId": 12345678`. The id is visible in the App's installation webhook payload (`installation.id`) or on the GitHub App installation page URL. Events from any other installation are rejected for that group even if its `owners` list is empty. Groups without `installationId` keep the legacy behavior (`owners` filtering).
|
|
||||||
|
|
||||||
Binding is **auto-configured** — the GitHub App's _Setup URL_ should point to `{BASE_URL}/auth/github/install`. Right after a user installs the App, the browser lands there (the page requires a signed-in admin session — not signed in users are redirected through the OAuth flow first) and they choose where the installation binds: a **new group** (`inst-{installationId}`, default) or any **existing group they own** (owner role checked again on submit; `POST /auth/github/install/bind` performs the provisioning). No manual id entry is needed. As a fallback (e.g. when the Setup URL is not configured), the `installation.created` webhook event creates/binds the group automatically — existing groups whose `owners` match the installing account are bound, otherwise a dedicated `inst-{installationId}` group is created. Then just add routes/members in the console.
|
|
||||||
|
|
||||||
## Routes
|
|
||||||
|
|
||||||
Routes define which events get forwarded to which channel (Discord or Telegram). They are stored in Cloudflare KV under the key `config:routes` as a JSON array.
|
|
||||||
|
|
||||||
There are **no default routes** — each route must define its own target. If no routes are configured, no events are forwarded.
|
|
||||||
|
|
||||||
### Route Schema
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "unique-route-id",
|
|
||||||
"name": "Human-readable 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": "REQUIRED_CHANNEL_ID",
|
|
||||||
"threadId": "OPTIONAL_THREAD_ID"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each entry of `targets` is a push destination, so one route can forward to several channels at once (e.g. a Discord channel **and** a Telegram group). `target.platform` selects the platform: `discord` (default) or `telegram`. For **Discord**, `target.channelId` is required (a thread in `target.threadId` is optional). For **Telegram**, `target.chatId` (the group/supergroup chat id, e.g. `-1001234567890`) is required and `target.topicId` (the `message_thread_id` of a topic, equivalent of a Discord thread) is optional. There is no fallback to a default channel.
|
|
||||||
|
|
||||||
### Discord Role Mentions
|
|
||||||
|
|
||||||
Set `discordRoleIds` on a route to ping one or more Discord roles (身份组) whenever that route fires. The mention (`<@&roleId>`) is prepended to the message content of every **Discord** target of the route; Telegram targets ignore this field. Mentions only trigger notifications when the bot has the `Mention Everyone` permission (or the role is marked mentionable), and the bot must be able to see the role.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "release-notify",
|
|
||||||
"name": "Notify on Release",
|
|
||||||
"enabled": true,
|
|
||||||
"groupId": "default",
|
|
||||||
"discordRoleIds": ["111111111111111111", "222222222222222222"],
|
|
||||||
"filters": [{ "type": "event", "match": "release" }],
|
|
||||||
"targets": [{ "platform": "discord", "channelId": "REQUIRED_CHANNEL_ID" }]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can add role ids in the admin console under _Discord role mentions_.
|
|
||||||
|
|
||||||
Other route fields:
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|------------------|----------|----------|-------------------------------------------------------------------------------------------------|
|
|
||||||
| `groupId` | string | Yes | Id of the [group](#groups) this route belongs to |
|
|
||||||
| `fallback` | boolean | No | When `true`, fires only if no non-fallback route matched the event; its own filters are ignored |
|
|
||||||
| `stop` | boolean | No | When `true` and this route matches, no further routes are evaluated for this event |
|
|
||||||
| `discordRoleIds` | string[] | No | Discord role ids to ping when this route fires; applied to Discord targets only |
|
|
||||||
|
|
||||||
### Custom Route Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "backend-prs",
|
|
||||||
"name": "Backend PRs",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Groups
|
|
||||||
|
|
||||||
Routes belong to groups. Groups scope admin access and can restrict which events flow into them. They are stored in Cloudflare KV under the key `config:groups` as a JSON array.
|
|
||||||
|
|
||||||
### Group Schema
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "backend-team",
|
|
||||||
"name": "Backend Team",
|
|
||||||
"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" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|------------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `id` | string | Yes | Lowercase id (`a-z0-9`, `-`); referenced by each route's `groupId`. Editable: renaming a group re-points its routes, per-group webhook secret and pending invites |
|
|
||||||
| `name` | string | Yes | Human-readable group name |
|
|
||||||
| `members` | object[] | No | `{ login, role }` entries; role is `owner`, `admin`, or `viewer` |
|
|
||||||
| `adminIds` | string[] | No | Deprecated legacy field; treated as `members` with role `owner` when present |
|
|
||||||
| `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all |
|
|
||||||
| `providers` | string[] | No | Source platforms allowed into this group (`github`, `gitea`); empty = all |
|
|
||||||
| `installationId` | number | No | GitHub App installation id bound to this group; only that installation's events are accepted (empty = all) |
|
|
||||||
| `emoji` | boolean | No | Whether to include emoji in this group's messages (default `true`) |
|
|
||||||
| `lang` | string | No | Message language for every route in this group (e.g. `en`, `zh`; custom via KV `i18n:<lang>`) — defaults to `en` |
|
|
||||||
| `logTarget` | object | No | Webhook log channel: a Discord `{ platform, channelId, threadId? }` or Telegram `{ platform, chatId, topicId? }` target that receives a summary of every webhook the group's routes dispatch |
|
|
||||||
|
|
||||||
### Roles
|
|
||||||
|
|
||||||
Every group member has one of three roles. Super admins (`ADMIN_USER_IDS`) always bypass them.
|
|
||||||
|
|
||||||
| Role | View routes/logs | Edit routes | Manage members & invites | Edit group settings |
|
|
||||||
|----------|------------------|-------------|--------------------------|---------------------|
|
|
||||||
| `owner` | ✓ | ✓ | ✓ | ✓ (except `owners`) |
|
|
||||||
| `admin` | ✓ | ✓ | ✗ | ✗ |
|
|
||||||
| `viewer` | ✓ (read-only) | ✗ | ✗ | ✗ |
|
|
||||||
|
|
||||||
### Access Model
|
|
||||||
|
|
||||||
- **Super admins** (`ADMIN_USER_IDS`) see and edit every group and all routes; only they can edit a group's `owners` list.
|
|
||||||
- **Owners** manage their group's routes, members, invites, name, id, `emoji`, and `providers`. They cannot remove the last owner or demote themselves when no other owner remains.
|
|
||||||
- **Admins** edit routes inside their groups and view logs; **viewers** get a read-only console.
|
|
||||||
- Group admin endpoints operate on a single group at a time via `/admin/api/groups/:id/routes`; `groupId` is forced from the path parameter.
|
|
||||||
- The `owners` list restricts which event actors (sender logins) the group's routes will dispatch at all.
|
|
||||||
- The `providers` list restricts which forge's events (`github`, `gitea`) the group's routes will dispatch. This lets you keep GitHub and Gitea groups separate even when org/user names collide.
|
|
||||||
|
|
||||||
### Webhook Log Channel
|
|
||||||
|
|
||||||
A group may set `logTarget` to a Discord channel/thread or Telegram chat/topic. Whenever the group's routes dispatch a webhook, a single summary message is sent there: the event type/action, the repo, the delivery id, and one line per route×target with an ✅/❌ outcome (including the error for failed sends; at most the first 10 lines are listed, the rest is summarized as `+N`). The message is green when every dispatch succeeded and red when any failed. The summary uses the group's message language. Log messages are sent best-effort and are not themselves recorded in the D1 send log.
|
|
||||||
|
|
||||||
### Invites
|
|
||||||
|
|
||||||
Owners (and super admins) can create single-use invite links valid for 7 days from the group's _Members_ panel. Accepting an invite adds the user with the invited role (`admin` or `viewer` — never `owner`); an existing `viewer` is upgraded to `admin`. Invites are stored in KV as `invite:{token}`.
|
|
||||||
|
|
||||||
### Self Sign-up
|
|
||||||
|
|
||||||
With `ALLOW_SELF_SIGNUP=1`, a GitHub user who has no group access gets a personal group (`u-{userId}`, owned by them) on first login instead of a `403`. This is the entry point for a fully self-service SaaS install; disable it to keep the console invite-only.
|
|
||||||
|
|
||||||
## Filter Types
|
## Filter Types
|
||||||
|
|
||||||
|
|
@ -329,37 +92,3 @@ Filters accept either a single string or an array of strings:
|
||||||
{ "type": "event", "match": "push" }
|
{ "type": "event", "match": "push" }
|
||||||
{ "type": "event", "match": ["push", "pull_request"] }
|
{ "type": "event", "match": ["push", "pull_request"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
## KV Storage Layout
|
|
||||||
|
|
||||||
| Key Pattern | Value | TTL |
|
|
||||||
|--------------------------------|-------------------------------------------------------------------------------|--------------------|
|
|
||||||
| `config:routes` | JSON array of routes | Permanent |
|
|
||||||
| `config:groups` | JSON array of groups | Permanent |
|
|
||||||
| `session:{id}` | Admin session `{ userId, login }` | 7 days |
|
|
||||||
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry |
|
|
||||||
| `token-reverse:{sha256}` | User id for reverse lookup by token | 0.9 × token expiry |
|
|
||||||
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 seconds |
|
|
||||||
| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 days |
|
|
||||||
| `invite:group:{id}` | Token index per group (keeps invite listing consistent) | Permanent |
|
|
||||||
| `delivery:{id}` | Webhook delivery id (dedup marker) | 300 seconds |
|
|
||||||
| `delivery:{groupId}:{id}` | Tenant-scoped delivery dedup for the per-group webhook ingress | 300 seconds |
|
|
||||||
| `tenant:{groupId}` | Per-group webhook secret (64-char hex, generated from the console) | Permanent |
|
|
||||||
| `msg:{routeId}:{key}:{target}` | Message id tracking for in-place updates (e.g. `workflow_run` / `check_run`) | 7 days |
|
|
||||||
| `cmd:guild:{id}` | Guild id whose commands were registered (dedup) | Permanent |
|
|
||||||
| `cmd:registered:global` | Global command registration marker (dedup) | 1 day |
|
|
||||||
| `config:discord-app-id` | Cached Discord application id | Permanent |
|
|
||||||
| `i18n:{lang}` | Translation overrides merged on top of English | Permanent |
|
|
||||||
|
|
||||||
## D1 Storage Layout
|
|
||||||
|
|
||||||
The D1 database (`DB` binding, database `webhooker`) holds four tables:
|
|
||||||
|
|
||||||
| Table | Purpose |
|
|
||||||
|------------------|------------------------------------------------------------------------------------------------|
|
|
||||||
| `send_logs` | One row per dispatch attempt (route id, event, target, ok/error, duration, error code, detail) |
|
|
||||||
| `audit_logs` | One row per admin operation (login/logout, group/route/member/invite changes) |
|
|
||||||
| `discord_links` | Maps `discord_user_id` → `github_user_id` for `/gh` Discord commands |
|
|
||||||
| `telegram_links` | Maps `telegram_user_id` → `github_user_id` for `/gh` Telegram commands |
|
|
||||||
|
|
||||||
`audit_logs` is pruned automatically by the scheduled trigger after `AUDIT_RETENTION_DAYS` (default 90).
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ Branch deletions via `git push --delete` arrive as push events with `deleted: tr
|
||||||
|
|
||||||
## How do I point a group at its own webhook endpoint?
|
## How do I point a group at its own webhook endpoint?
|
||||||
|
|
||||||
See [Per-group endpoint](./configuration#per-group-endpoint) — generate a secret from the group's **Webhook endpoint** panel (owner role), then use `POST /webhook/{groupId}` with the group secret.
|
See [Per-group endpoint](./ingress#per-group-endpoint) — generate a secret from the group's **Webhook endpoint** panel (owner role), then use `POST /webhook/{groupId}` with the group secret.
|
||||||
|
|
||||||
## Can I run this outside Cloudflare Workers?
|
## Can I run this outside Cloudflare Workers?
|
||||||
|
|
||||||
|
|
@ -34,4 +34,4 @@ No — the worker requires the KV and D1 bindings declared in `wrangler.jsonc` a
|
||||||
|
|
||||||
## Where is data stored?
|
## Where is data stored?
|
||||||
|
|
||||||
Configuration lives in Cloudflare KV (`config:routes`, `config:groups`); send/audit logs and platform↔GitHub links live in D1. See [Storage Layout](./configuration#kv-storage-layout).
|
Configuration lives in Cloudflare KV (`config:routes`, `config:groups`); send/audit logs and platform↔GitHub links live in D1. See [Storage Layout](./storage#kv-storage-layout).
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Filter Tutorial
|
# Filter Tutorial
|
||||||
|
|
||||||
Filters decide which webhook events a [route](./configuration#routes) forwards. A route fires only when **every** filter in its `filters` array matches (AND logic). This page is a hands-on tutorial: it explains how each filter type behaves and how to combine them into real-world routing rules.
|
Filters decide which webhook events a [route](./routes) forwards. A route fires only when **every** filter in its `filters` array matches (AND logic). This page is a hands-on tutorial: it explains how each filter type behaves and how to combine them into real-world routing rules.
|
||||||
|
|
||||||
See [Filter Types](./configuration#filter-types) in the configuration guide for the reference table, and [Supported Events](../events/supported) for the full event list.
|
See [Filter Types](./configuration#filter-types) in the configuration guide for the reference table, and [Supported Events](../events/supported) for the full event list.
|
||||||
|
|
||||||
|
|
|
||||||
65
docs/guide/groups.md
Normal file
65
docs/guide/groups.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Groups & Access Control
|
||||||
|
|
||||||
|
Routes belong to groups. Groups scope admin access and can restrict which events flow into them. They are stored in Cloudflare KV under the key `config:groups` as a JSON array, managed via the [Web UI](./configuration#web-ui) or the [Admin API](../api/admin). At most **100 groups** can be saved per instance.
|
||||||
|
|
||||||
|
## Group Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "backend-team",
|
||||||
|
"name": "Backend Team",
|
||||||
|
"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" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|------------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `id` | string | Yes | Lowercase id (`a-z0-9`, `-`); referenced by each route's `groupId`. Editable: renaming a group re-points its routes, per-group webhook secret and pending invites |
|
||||||
|
| `name` | string | Yes | Human-readable group name |
|
||||||
|
| `members` | object[] | No | `{ login, role }` entries; role is `owner`, `admin`, or `viewer` |
|
||||||
|
| `adminIds` | string[] | No | Deprecated legacy field; treated as `members` with role `owner` when present |
|
||||||
|
| `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all |
|
||||||
|
| `providers` | string[] | No | Source platforms allowed into this group (`github`, `gitea`); empty = all |
|
||||||
|
| `installationId` | number | No | GitHub App installation id bound to this group; only that installation's events are accepted (empty = all) |
|
||||||
|
| `emoji` | boolean | No | Whether to include emoji in this group's messages (default `true`) |
|
||||||
|
| `lang` | string | No | Message language for every route in this group (e.g. `en`, `zh`; custom via KV `i18n:<lang>`) — see [Message Language](./i18n) — defaults to `en` |
|
||||||
|
| `logTarget` | object | No | Webhook log channel: a Discord `{ platform, channelId, threadId? }` or Telegram `{ platform, chatId, topicId? }` target that receives a summary of every webhook the group's routes dispatch |
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
Every group member has one of three roles. Super admins (`ADMIN_USER_IDS`) always bypass them.
|
||||||
|
|
||||||
|
| Role | View routes/logs | Edit routes | Manage members & invites | Edit group settings |
|
||||||
|
|----------|------------------|-------------|--------------------------|---------------------|
|
||||||
|
| `owner` | ✓ | ✓ | ✓ | ✓ (except `owners`) |
|
||||||
|
| `admin` | ✓ | ✓ | ✗ | ✗ |
|
||||||
|
| `viewer` | ✓ (read-only) | ✗ | ✗ | ✗ |
|
||||||
|
|
||||||
|
## Access Model
|
||||||
|
|
||||||
|
- **Super admins** (`ADMIN_USER_IDS`) see and edit every group and all routes; only they can edit a group's `owners` list.
|
||||||
|
- **Owners** manage their group's routes, members, invites, name, id, `emoji`, and `providers`. They cannot remove the last owner or demote themselves when no other owner remains.
|
||||||
|
- **Admins** edit routes inside their groups and view logs; **viewers** get a read-only console.
|
||||||
|
- Group admin endpoints operate on a single group at a time via `/admin/api/groups/:id/routes`; `groupId` is forced from the path parameter.
|
||||||
|
- The `owners` list restricts which event actors (sender logins) the group's routes will dispatch at all.
|
||||||
|
- The `providers` list restricts which forge's events (`github`, `gitea`) the group's routes will dispatch. This lets you keep GitHub and Gitea groups separate even when org/user names collide.
|
||||||
|
|
||||||
|
## Webhook Log Channel
|
||||||
|
|
||||||
|
A group may set `logTarget` to a Discord channel/thread or Telegram chat/topic. Whenever the group's routes dispatch a webhook, a single summary message is sent there: the event type/action, the repo, the delivery id, and one line per route×target with an ✅/❌ outcome (including the error for failed sends; at most the first 10 lines are listed, the rest is summarized as `+N`). The message is green when every dispatch succeeded and red when any failed. The summary uses the group's message language. Log messages are sent best-effort and are not themselves recorded in the D1 send log.
|
||||||
|
|
||||||
|
## Invites
|
||||||
|
|
||||||
|
Owners (and super admins) can create single-use invite links valid for 7 days from the group's _Members_ panel. Accepting an invite adds the user with the invited role (`admin` or `viewer` — never `owner`); an existing `viewer` is upgraded to `admin`. Invites are stored in KV as `invite:{token}`.
|
||||||
|
|
||||||
|
## Self Sign-up
|
||||||
|
|
||||||
|
With `ALLOW_SELF_SIGNUP=1`, a GitHub user who has no group access gets a personal group (`u-{userId}`, owned by them) on first login instead of a `403`. This is the entry point for a fully self-service SaaS install; disable it to keep the console invite-only.
|
||||||
|
|
@ -4,7 +4,7 @@ Messages are rendered in the language configured per group. WebHooker ships with
|
||||||
|
|
||||||
## Per-Group Language
|
## Per-Group Language
|
||||||
|
|
||||||
Set `Group.lang` (e.g. `"zh"`) to choose the message language for every route in a group — see [Groups → Group Schema](./configuration#group-schema). The group's webhook log-channel summaries use the same language.
|
Set `Group.lang` (e.g. `"zh"`) to choose the message language for every route in a group — see [Groups → Group Schema](./groups#group-schema). The group's webhook log-channel summaries use the same language.
|
||||||
|
|
||||||
## Custom Overrides
|
## Custom Overrides
|
||||||
|
|
||||||
|
|
|
||||||
71
docs/guide/ingress.md
Normal file
71
docs/guide/ingress.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Webhook Ingress & Tenancy
|
||||||
|
|
||||||
|
## Webhook Providers
|
||||||
|
|
||||||
|
WebHooker ingests webhooks from multiple forges through the same `POST /webhook` endpoint; the provider is auto-detected from the request headers, so point every forge's webhook at `{BASE_URL}/webhook`.
|
||||||
|
|
||||||
|
| Provider | Event header | Signature header | Signature format | Secret |
|
||||||
|
|----------|------------------|-----------------------|----------------------------|-------------------------|
|
||||||
|
| GitHub | `X-GitHub-Event` | `X-Hub-Signature-256` | `sha256=<hex>` HMAC-SHA256 | `GITHUB_WEBHOOK_SECRET` |
|
||||||
|
| Gitea | `X-Gitea-Event` | `X-Gitea-Signature` | plain hex HMAC-SHA256 | `GITEA_WEBHOOK_SECRET` |
|
||||||
|
|
||||||
|
Delivery-id dedup uses `X-GitHub-Delivery` (GitHub) or `X-Gitea-Delivery` (Gitea) when present.
|
||||||
|
|
||||||
|
Gitea payloads are normalized to the same internal shape as GitHub events, so routes, filters, and the 28 formatters work unchanged. Unknown or unmapped Gitea events fall back to the generic formatter. Repository/commit/user links are derived from the payload's `repository.html_url`, so they point at your Gitea instance.
|
||||||
|
|
||||||
|
## Global Endpoint (`POST /webhook`)
|
||||||
|
|
||||||
|
The global endpoint verifies payloads against the operator's global secrets (`GITHUB_WEBHOOK_SECRET`, `GITEA_WEBHOOK_SECRET`) and dispatches into **all** routes. GitHub App installations deliver here; use `installationId` on groups to keep tenants isolated.
|
||||||
|
|
||||||
|
## Per-Group Endpoint (`POST /webhook/{groupId}`)
|
||||||
|
|
||||||
|
Every group can opt into its own webhook ingress with an independent secret (generated from the group page — Webhook endpoint panel, owner role). Payloads are verified against the **group's** secret instead of the global ones, and only that group's routes are eligible. This is how SaaS users configure Gitea, classic GitHub, or custom webhooks without sharing (or knowing) the operator's secrets.
|
||||||
|
|
||||||
|
- Supported for any provider: GitHub (`X-Hub-Signature-256`), Gitea (`X-Gitea-Signature`), custom (`X-WebHooker-Signature`)
|
||||||
|
- The secret is a 64-char hex string; regenerate from the console invalidates the old one immediately
|
||||||
|
- Delivery-id dedup keys are tenant-scoped (`delivery:{groupId}:{id}`)
|
||||||
|
- When the group has no secret (or no longer exists) the endpoint returns `404`
|
||||||
|
|
||||||
|
## Custom Webhooks
|
||||||
|
|
||||||
|
Post arbitrary JSON to `POST /webhook/{groupId}` (or the global endpoint) with the body signed as `X-WebHooker-Signature: sha256=<hmac-sha256 hex of the raw body>` using the group's secret. The payload becomes a `custom` event that flows through the normal route pipeline — create a route with `event: custom` (there is a console template) and it dispatches to that route's targets, records `send_logs`, and appears in the group's webhook log channel.
|
||||||
|
|
||||||
|
Payload schema:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---------------|----------|------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `title` | string | Message title (falls back to "Custom message") |
|
||||||
|
| `description` | string | Optional message body |
|
||||||
|
| `color` | string | Optional embed color: a word (`red`, `green`, `yellow`, `blue`, `purple`, `orange`, `cyan`, `gray`) or `#rrggbb` |
|
||||||
|
| `url` | string | Optional link for the title |
|
||||||
|
| `repo` | string | Optional `owner/repo`; prefixes the title and is used as the footer |
|
||||||
|
| `author` | object | Optional `{ name, iconUrl, url }` |
|
||||||
|
| `fields` | object[] | Optional embed fields `{ name, value, inline }` |
|
||||||
|
| `footer` | string | Optional footer override |
|
||||||
|
| `deliveryId` | string | Optional id for sender-side dedup (retries) |
|
||||||
|
|
||||||
|
## GitHub App Tenant Isolation
|
||||||
|
|
||||||
|
When the GitHub App is installed, its events arrive at the global endpoint for **every** installation. To keep tenants apart, bind each group to the installation id that should feed it: `"installationId": 12345678`. The id is visible in the App's installation webhook payload (`installation.id`) or on the GitHub App installation page URL. Events from any other installation are rejected for that group even if its `owners` list is empty. Groups without `installationId` keep the legacy behavior (`owners` filtering).
|
||||||
|
|
||||||
|
Binding is **auto-configured** — the GitHub App's _Setup URL_ should point to `{BASE_URL}/auth/github/install`. Right after a user installs the App, the browser lands there (the page requires a signed-in admin session — not signed in users are redirected through the OAuth flow first) and they choose where the installation binds: a **new group** (`inst-{installationId}`, default) or any **existing group they own** (owner role checked again on submit; `POST /auth/github/install/bind` performs the provisioning). No manual id entry is needed. As a fallback (e.g. when the Setup URL is not configured), the `installation.created` webhook event creates/binds the group automatically — existing groups whose `owners` match the installing account are bound, otherwise a dedicated `inst-{installationId}` group is created. Then just add routes/members in the console.
|
||||||
|
|
||||||
|
To show the installing account's login on the choice page, set `GITHUB_APP_ID` and `GITHUB_PRIVATE_KEY` — see [Secrets](./configuration#secrets).
|
||||||
|
|
@ -34,4 +34,4 @@ Entries are pruned automatically by the scheduled trigger after `AUDIT_RETENTION
|
||||||
|
|
||||||
## Webhook Log Channel
|
## Webhook Log Channel
|
||||||
|
|
||||||
Groups can additionally receive a per-webhook summary message in a Discord channel/thread or Telegram chat/topic — see [Groups → Webhook Log Channel](./configuration#webhook-log-channel). These summaries are best-effort and are **not** recorded in `send_logs`.
|
Groups can additionally receive a per-webhook summary message in a Discord channel/thread or Telegram chat/topic — see [Groups → Webhook Log Channel](./groups#webhook-log-channel). These summaries are best-effort and are **not** recorded in `send_logs`.
|
||||||
|
|
|
||||||
86
docs/guide/routes.md
Normal file
86
docs/guide/routes.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Routes & Targets
|
||||||
|
|
||||||
|
Routes define which events get forwarded to which channel (Discord or Telegram). They are stored in Cloudflare KV under the key `config:routes` as a JSON array, managed via the [Web UI](./configuration#web-ui), the [Admin API](../api/admin), or `config.example.yaml`.
|
||||||
|
|
||||||
|
There are **no default routes** — each route must define its own target. If no routes are configured, no events are forwarded. At most **200 routes** can be saved per instance.
|
||||||
|
|
||||||
|
## Route Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "unique-route-id",
|
||||||
|
"name": "Human-readable 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": "REQUIRED_CHANNEL_ID",
|
||||||
|
"threadId": "OPTIONAL_THREAD_ID"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each entry of `targets` is a push destination, so one route can forward to several channels at once (e.g. a Discord channel **and** a Telegram group). `target.platform` selects the platform: `discord` (default) or `telegram`. For **Discord**, `target.channelId` is required (a thread in `target.threadId` is optional). For **Telegram**, `target.chatId` (the group/supergroup chat id, e.g. `-1001234567890`) is required and `target.topicId` (the `message_thread_id` of a topic, equivalent of a Discord thread) is optional. There is no fallback to a default channel.
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|------------------|----------|----------|-------------------------------------------------------------------------------------------------|
|
||||||
|
| `groupId` | string | Yes | Id of the [group](./groups) this route belongs to |
|
||||||
|
| `fallback` | boolean | No | When `true`, fires only if no non-fallback route matched the event; its own filters are ignored |
|
||||||
|
| `stop` | boolean | No | When `true` and this route matches, no further routes are evaluated for this event |
|
||||||
|
| `discordRoleIds` | string[] | No | Discord role ids to ping when this route fires; applied to Discord targets only |
|
||||||
|
|
||||||
|
## Discord Role Mentions
|
||||||
|
|
||||||
|
Set `discordRoleIds` on a route to ping one or more Discord roles (身份组) whenever that route fires. The mention (`<@&roleId>`) is prepended to the message content of every **Discord** target of the route; Telegram targets ignore this field. Mentions only trigger notifications when the bot has the `Mention Everyone` permission (or the role is marked mentionable), and the bot must be able to see the role.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "release-notify",
|
||||||
|
"name": "Notify on Release",
|
||||||
|
"enabled": true,
|
||||||
|
"groupId": "default",
|
||||||
|
"discordRoleIds": ["111111111111111111", "222222222222222222"],
|
||||||
|
"filters": [{ "type": "event", "match": "release" }],
|
||||||
|
"targets": [{ "platform": "discord", "channelId": "REQUIRED_CHANNEL_ID" }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can add role ids in the admin console under _Discord role mentions_.
|
||||||
|
|
||||||
|
## Filters
|
||||||
|
|
||||||
|
Every route carries a `filters` array (all must match — AND logic). See the [Filter Types](./configuration#filter-types) reference and the [Filter Tutorial](./filters).
|
||||||
|
|
||||||
|
## Custom Route Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "backend-prs",
|
||||||
|
"name": "Backend PRs",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
35
docs/guide/storage.md
Normal file
35
docs/guide/storage.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Storage Layout
|
||||||
|
|
||||||
|
## KV Storage Layout
|
||||||
|
|
||||||
|
| Key Pattern | Value | TTL |
|
||||||
|
|--------------------------------|-------------------------------------------------------------------------------|--------------------|
|
||||||
|
| `config:routes` | JSON array of routes | Permanent |
|
||||||
|
| `config:groups` | JSON array of groups | Permanent |
|
||||||
|
| `session:{id}` | Admin session `{ userId, login }` | 7 days |
|
||||||
|
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry |
|
||||||
|
| `token-reverse:{sha256}` | User id for reverse lookup by token | 0.9 × token expiry |
|
||||||
|
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 seconds |
|
||||||
|
| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 days |
|
||||||
|
| `invite:group:{id}` | Token index per group (keeps invite listing consistent) | Permanent |
|
||||||
|
| `delivery:{id}` | Webhook delivery id (dedup marker) | 300 seconds |
|
||||||
|
| `delivery:{groupId}:{id}` | Tenant-scoped delivery dedup for the per-group webhook ingress | 300 seconds |
|
||||||
|
| `tenant:{groupId}` | Per-group webhook secret (64-char hex, generated from the console) | Permanent |
|
||||||
|
| `msg:{routeId}:{key}:{target}` | Message id tracking for in-place updates (e.g. `workflow_run` / `check_run`) | 7 days |
|
||||||
|
| `cmd:guild:{id}` | Guild id whose commands were registered (dedup) | Permanent |
|
||||||
|
| `cmd:registered:global` | Global command registration marker (dedup) | 1 day |
|
||||||
|
| `config:discord-app-id` | Cached Discord application id | Permanent |
|
||||||
|
| `i18n:{lang}` | Translation overrides merged on top of English | Permanent |
|
||||||
|
|
||||||
|
## D1 Storage Layout
|
||||||
|
|
||||||
|
The D1 database (`DB` binding, database `webhooker`) holds four tables:
|
||||||
|
|
||||||
|
| Table | Purpose |
|
||||||
|
|------------------|------------------------------------------------------------------------------------------------|
|
||||||
|
| `send_logs` | One row per dispatch attempt (route id, event, target, ok/error, duration, error code, detail) |
|
||||||
|
| `audit_logs` | One row per admin operation (login/logout, group/route/member/invite changes) |
|
||||||
|
| `discord_links` | Maps `discord_user_id` → `github_user_id` for `/gh` Discord commands |
|
||||||
|
| `telegram_links` | Maps `telegram_user_id` → `github_user_id` for `/gh` Telegram commands |
|
||||||
|
|
||||||
|
`audit_logs` is pruned automatically by the scheduled trigger after `AUDIT_RETENTION_DAYS` (default 90). See [Logs](./logs) for the row fields.
|
||||||
39
docs/zh/api/admin.md
Normal file
39
docs/zh/api/admin.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Admin API
|
||||||
|
|
||||||
|
管理端点用于管理路由、分组、成员、邀请、webhook 密钥、发送日志与审计日志。它们需要会话 Cookie,可通过 `GET /admin/login`(GitHub OAuth)获取;登录用户必须列在 `ADMIN_USER_IDS` 中,或管理某个分组。设置方法见[配置 → Web 控制台](../guide/configuration.md#web-ui)。
|
||||||
|
|
||||||
|
控制台本身在 `/admin` 提供;其标签页可通过 URL 路径直达(`/admin/groups`、`/admin/logs`、`/admin/audit`)。
|
||||||
|
|
||||||
|
## 端点
|
||||||
|
|
||||||
|
| 端点 | 说明 |
|
||||||
|
|-------------------------------------------------|-------------------------------------------------------------------|
|
||||||
|
| `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) |
|
||||||
|
| `PUT /admin/api/groups/:id/rename` | 重命名分组(owner);路由、webhook secret 与邀请自动跟随 |
|
||||||
|
| `GET /admin/api/groups/:id/invites` | 列出待处理的邀请(owner) |
|
||||||
|
| `POST /admin/api/groups/:id/invites` | 创建邀请链接(owner) |
|
||||||
|
| `DELETE /admin/api/invites/:token` | 撤销邀请(owner) |
|
||||||
|
| `GET /admin/api/groups/:id/webhook` | 分组 webhook 端点信息(owner) |
|
||||||
|
| `POST /admin/api/groups/:id/webhook/regenerate` | 生成/重新生成分组 webhook secret(owner) |
|
||||||
|
| `DELETE /admin/api/groups/:id/webhook` | 停用分组 webhook 入口(owner) |
|
||||||
|
| `GET /admin/api/logs` | 发送日志(按可访问的路由过滤) |
|
||||||
|
| `GET /admin/api/logs/:id` | 单条发送日志(按权限过滤) |
|
||||||
|
| `GET /admin/api/audit` | 审计日志(按可访问的分组过滤) |
|
||||||
|
|
||||||
|
## 校验
|
||||||
|
|
||||||
|
- `PUT /admin/api/routes` — 请求体为 `{ "routes": Route[] }`;校验每条路由(id 格式、组内唯一 id、name、enabled、groupId、过滤器——**仅 `fallback` 路由允许空过滤器**——可选的 `discordRoleIds`(身份组 id 字符串列表)、平台感知的 targets:Discord 需 `target.channelId`,Telegram 需 `target.chatId`)并持久化到 KV `config:routes`。返回 `200 { ok, count }` 或 `400 { error }` / `401 { error }` / `403 { error }`。未变更的路由跳过完整校验。
|
||||||
|
- `PUT /admin/api/groups` — 校验分组 id、成员角色(至少一个 `owner`)、`providers`(`github` / `gitea`)与 `installationId`。
|
||||||
|
- 上限:每个实例最多 200 条路由与 100 个分组。
|
||||||
|
|
||||||
|
模式:见[路由与目标](../guide/routes)、[分组与访问控制](../guide/groups)。
|
||||||
|
|
@ -11,7 +11,7 @@ https://your-worker.workers.dev
|
||||||
## 端点
|
## 端点
|
||||||
|
|
||||||
| 方法 | 路径 | 鉴权 | 说明 |
|
| 方法 | 路径 | 鉴权 | 说明 |
|
||||||
|----------|--------------------------------------------|--------------|------------------------------------------------------|
|
|------------|----------------------------------------------|----------------|--------------------------------------------------------|
|
||||||
| `GET` | `/health` | 无 | 健康检查 |
|
| `GET` | `/health` | 无 | 健康检查 |
|
||||||
| `POST` | `/webhook` | HMAC 签名 | GitHub / Gitea / 自定义 webhook 接入(自动识别来源) |
|
| `POST` | `/webhook` | HMAC 签名 | GitHub / Gitea / 自定义 webhook 接入(自动识别来源) |
|
||||||
| `POST` | `/webhook/:groupId` | 分组 secret | 分组级 webhook 入口(只触发该分组的路由) |
|
| `POST` | `/webhook/:groupId` | 分组 secret | 分组级 webhook 入口(只触发该分组的路由) |
|
||||||
|
|
@ -31,31 +31,12 @@ https://your-worker.workers.dev
|
||||||
| `GET` | `/admin/login` | 无 | 开始管理员登录(GitHub OAuth) |
|
| `GET` | `/admin/login` | 无 | 开始管理员登录(GitHub OAuth) |
|
||||||
| `GET` | `/admin/logout` | 管理员会话 | 退出登录并销毁会话 |
|
| `GET` | `/admin/logout` | 管理员会话 | 退出登录并销毁会话 |
|
||||||
| `GET` | `/admin/invite` | 管理员会话 | 接受分组邀请(浏览器页面,`?token=…`) |
|
| `GET` | `/admin/invite` | 管理员会话 | 接受分组邀请(浏览器页面,`?token=…`) |
|
||||||
| `GET` | `/admin/api/me` | 管理员会话 | 当前会话、权限范围、分组与角色 |
|
|
||||||
| `GET` | `/admin/api/routes` | 管理员会话 | 列出路由(按权限过滤) |
|
`/admin/api/*` 端点(路由、分组、成员、邀请、webhook 密钥、发送日志、审计日志)在 [Admin API](./admin) 中单独说明。
|
||||||
| `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) |
|
|
||||||
| `PUT` | `/admin/api/groups/:id/rename` | 管理员会话 | 重命名分组(owner);路由/secret/邀请自动跟随 |
|
|
||||||
| `GET` | `/admin/api/groups/:id/invites` | 管理员会话 | 列出待处理的邀请(owner) |
|
|
||||||
| `POST` | `/admin/api/groups/:id/invites` | 管理员会话 | 创建邀请链接(owner) |
|
|
||||||
| `DELETE` | `/admin/api/invites/:token` | 管理员会话 | 撤销邀请(owner) |
|
|
||||||
| `GET` | `/admin/api/groups/:id/webhook` | 管理员会话 | 分组 webhook 端点信息(owner) |
|
|
||||||
| `POST` | `/admin/api/groups/:id/webhook/regenerate` | 管理员会话 | 生成/重新生成分组 webhook secret(owner) |
|
|
||||||
| `DELETE` | `/admin/api/groups/:id/webhook` | 管理员会话 | 停用分组 webhook 入口(owner) |
|
|
||||||
| `GET` | `/admin/api/logs` | 管理员会话 | 发送日志(按权限过滤) |
|
|
||||||
| `GET` | `/admin/api/logs/:id` | 管理员会话 | 单条发送日志(按权限过滤) |
|
|
||||||
| `GET` | `/admin/api/audit` | 管理员会话 | 审计日志(按可访问的分组过滤) |
|
|
||||||
|
|
||||||
## 管理控制台
|
## 管理控制台
|
||||||
|
|
||||||
参见[配置 → Web 控制台](../guide/configuration.md#web-ui)了解设置方法。管理端点需要会话 Cookie,可通过 `GET /admin/login`(GitHub OAuth)获取;登录用户必须列在 `ADMIN_USER_IDS` 中,或管理某个分组。
|
参见[配置 → Web 控制台](../guide/configuration.md#web-ui)了解设置方法,管理端点的完整参考见 [Admin API](./admin)。管理端点需要会话 Cookie,可通过 `GET /admin/login`(GitHub OAuth)获取;登录用户必须列在 `ADMIN_USER_IDS` 中,或管理某个分组。
|
||||||
|
|
||||||
- `GET /admin` — 提供配置控制台 HTML
|
|
||||||
- `GET /admin/api/routes` — 返回 `{ "routes": Route[] }`
|
|
||||||
- `PUT /admin/api/routes` — 请求体为 `{ "routes": Route[] }`;校验每条路由(id 格式、唯一 id、name、enabled、groupId、过滤器——**仅 `fallback` 路由允许空过滤器**——可选的 `discordRoleIds`(身份组 id 字符串列表)、平台感知的 targets:Discord 需 `target.channelId`,Telegram 需 `target.chatId`)并持久化到 KV `config:routes`。返回 `200 { ok, count }` 或 `400 { error }` / `401 { error }` / `403 { error }`。
|
|
||||||
|
|
||||||
## 健康检查
|
## 健康检查
|
||||||
|
|
||||||
|
|
@ -82,7 +63,7 @@ POST /webhook
|
||||||
**请求头:**
|
**请求头:**
|
||||||
|
|
||||||
| 头部 | 必需 | 说明 |
|
| 头部 | 必需 | 说明 |
|
||||||
|-----------------------|------|-------------------------------|
|
|-------------------------|--------|---------------------------------|
|
||||||
| `X-Hub-Signature-256` | 是 | HMAC-SHA256 签名 |
|
| `X-Hub-Signature-256` | 是 | HMAC-SHA256 签名 |
|
||||||
| `X-GitHub-Event` | 是 | 事件类型名称 |
|
| `X-GitHub-Event` | 是 | 事件类型名称 |
|
||||||
| `X-GitHub-Delivery` | 否 | 唯一投递 ID(存在时用于去重) |
|
| `X-GitHub-Delivery` | 否 | 唯一投递 ID(存在时用于去重) |
|
||||||
|
|
@ -102,7 +83,7 @@ POST /webhook
|
||||||
**错误响应:**
|
**错误响应:**
|
||||||
|
|
||||||
| 状态码 | 响应体 | 原因 |
|
| 状态码 | 响应体 | 原因 |
|
||||||
|--------|----------------------------------|------------------------------|
|
|----------|------------------------------------|--------------------------------|
|
||||||
| `401` | `{"error": "Invalid signature"}` | 签名验证失败 |
|
| `401` | `{"error": "Invalid signature"}` | 签名验证失败 |
|
||||||
| `400` | `{"error": "Invalid event"}` | 缺少事件头或格式错误的请求体 |
|
| `400` | `{"error": "Invalid event"}` | 缺少事件头或格式错误的请求体 |
|
||||||
| `413` | `{"error": "Request too large"}` | 请求体超过 1MB 限制 |
|
| `413` | `{"error": "Request too large"}` | 请求体超过 1MB 限制 |
|
||||||
|
|
@ -113,16 +94,16 @@ POST /webhook
|
||||||
|
|
||||||
### 自定义 Webhook
|
### 自定义 Webhook
|
||||||
|
|
||||||
任意 JSON 载荷用 `X-WebHooker-Signature: sha256=<hex>`(对原始 body 的 HMAC-SHA256,使用分组或全局 secret)签名后即可成为 `custom` 事件。用 `event: custom` 过滤器的路由接收。载荷格式见[配置 → 自定义 Webhook](../guide/configuration.md#自定义-webhook)。
|
任意 JSON 载荷用 `X-WebHooker-Signature: sha256=<hex>`(对原始 body 的 HMAC-SHA256,使用分组或全局 secret)签名后即可成为 `custom` 事件。用 `event: custom` 过滤器的路由接收。载荷格式见[配置 → 自定义 Webhook](../guide/ingress.md#自定义-webhook)。
|
||||||
|
|
||||||
### GitHub App 安装事件
|
### GitHub App 安装事件
|
||||||
|
|
||||||
`installation` webhook 事件(`created` 等)作为兜底会自动配置:按安装账号自动创建分组(`inst-{installationId}`,绑定 `installationId`);或把 `owners` 匹配该账号的现有分组自动绑定到该安装。参见[配置 → GitHub App 租户隔离](../guide/configuration.md#github-app-租户隔离)。
|
`installation` webhook 事件(`created` 等)作为兜底会自动配置:按安装账号自动创建分组(`inst-{installationId}`,绑定 `installationId`);或把 `owners` 匹配该账号的现有分组自动绑定到该安装。参见[配置 → GitHub App 租户隔离](../guide/ingress.md#github-app-租户隔离)。
|
||||||
|
|
||||||
主要流程是 App 的 **Setup URL** —— 将其设置为 `{BASE_URL}/auth/github/install`。用户安装 App 后浏览器会跳转到:
|
主要流程是 App 的 **Setup URL** —— 将其设置为 `{BASE_URL}/auth/github/install`。用户安装 App 后浏览器会跳转到:
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|--------|-----------------------------|---------------------------------------------------------------|
|
|----------|-------------------------------|-----------------------------------------------------------------|
|
||||||
| `GET` | `/auth/github/install` | 选择页:将安装绑定到新分组或登录用户拥有 owner 权限的已有分组 |
|
| `GET` | `/auth/github/install` | 选择页:将安装绑定到新分组或登录用户拥有 owner 权限的已有分组 |
|
||||||
| `POST` | `/auth/github/install/bind` | 执行绑定(再次校验 owner 角色)并跳转 `/admin?install=ok` |
|
| `POST` | `/auth/github/install/bind` | 执行绑定(再次校验 owner 角色)并跳转 `/admin?install=ok` |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ WebHooker 支持 28 种 GitHub webhook 事件类型,每种都有专用的格
|
||||||
## 事件表
|
## 事件表
|
||||||
|
|
||||||
| 事件 | 说明 | 嵌入亮点 |
|
| 事件 | 说明 | 嵌入亮点 |
|
||||||
|-------------------------------|---------------------------|----------------------------------------------------------------------------------------------------------------|
|
|---------------------------------|-----------------------------|------------------------------------------------------------------------------------------------------------------|
|
||||||
| `push` | 代码推送到分支 | 提交列表、分支、作者、差异统计 |
|
| `push` | 代码推送到分支 | 提交列表、分支、作者、差异统计 |
|
||||||
| `pull_request` | PR 打开/关闭/合并/编辑 | PR 标题、分支、差异统计、标签 |
|
| `pull_request` | PR 打开/关闭/合并/编辑 | PR 标题、分支、差异统计、标签 |
|
||||||
| `issues` | 议题打开/关闭/编辑 | 议题标题、标签、指派人 |
|
| `issues` | 议题打开/关闭/编辑 | 议题标题、标签、指派人 |
|
||||||
|
|
@ -34,14 +34,14 @@ WebHooker 支持 28 种 GitHub webhook 事件类型,每种都有专用的格
|
||||||
| `repository` | 仓库重命名/转移 | 旧 → 新名称、变更 |
|
| `repository` | 仓库重命名/转移 | 旧 → 新名称、变更 |
|
||||||
| `code_scanning_alert` | 代码扫描告警 | 严重程度、规则 ID、文件路径 |
|
| `code_scanning_alert` | 代码扫描告警 | 严重程度、规则 ID、文件路径 |
|
||||||
| `dependabot_alert` | Dependabot 告警 | 严重程度、包、受影响版本、修复版本 |
|
| `dependabot_alert` | Dependabot 告警 | 严重程度、包、受影响版本、修复版本 |
|
||||||
| `custom` | 签名的自定义 JSON webhook | 任意 title/description/color/url/author/fields(见[自定义 webhook](../guide/configuration.md#自定义-webhook)) |
|
| `custom` | 签名的自定义 JSON webhook | 任意 title/description/color/url/author/fields(见[自定义 webhook](../guide/ingress.md#自定义-webhook)) |
|
||||||
|
|
||||||
## 颜色编码
|
## 颜色编码
|
||||||
|
|
||||||
每种事件类型在 Discord 嵌入中使用不同的颜色(来自 `server/lib/formatters/colors.ts`):
|
每种事件类型在 Discord 嵌入中使用不同的颜色(来自 `server/lib/formatters/colors.ts`):
|
||||||
|
|
||||||
| 颜色 | 事件 |
|
| 颜色 | 事件 |
|
||||||
|------------------|---------------------------------------------------------------------------------------------------------------------------------|
|
|--------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| 绿色 (`#2da44e`) | push、PR 打开/可审查、issue 打开、工作流成功、发布已发布、检查成功、审查已批准、部署成功、成员添加、里程碑关闭、讨论已回答 |
|
| 绿色 (`#2da44e`) | push、PR 打开/可审查、issue 打开、工作流成功、发布已发布、检查成功、审查已批准、部署成功、成员添加、里程碑关闭、讨论已回答 |
|
||||||
| 红色 (`#f85149`) | PR 关闭、issue 关闭、工作流失败、发布已删除、delete、检查失败、审查请求修改、部署失败、成员移除、代码扫描/Dependabot 严重与高危 |
|
| 红色 (`#f85149`) | PR 关闭、issue 关闭、工作流失败、发布已删除、delete、检查失败、审查请求修改、部署失败、成员移除、代码扫描/Dependabot 严重与高危 |
|
||||||
| 紫色 (`#8957e5`) | PR 合并、label、discussion |
|
| 紫色 (`#8957e5`) | PR 合并、label、discussion |
|
||||||
|
|
@ -67,7 +67,7 @@ WebHooker 支持 28 种 GitHub webhook 事件类型,每种都有专用的格
|
||||||
实操指南见[过滤器教程](../guide/filters),包含完整示例。
|
实操指南见[过滤器教程](../guide/filters),包含完整示例。
|
||||||
|
|
||||||
| 过滤器 | 适用事件 |
|
| 过滤器 | 适用事件 |
|
||||||
|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `event` | 所有事件 |
|
| `event` | 所有事件 |
|
||||||
| `repo` | 所有事件 |
|
| `repo` | 所有事件 |
|
||||||
| `actor` | 所有事件 |
|
| `actor` | 所有事件 |
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,29 @@
|
||||||
# 配置
|
# 配置
|
||||||
|
|
||||||
|
本页是密钥与 Web 控制台的参考。核心概念在独立页面中说明:
|
||||||
|
|
||||||
|
| 主题 | 页面 |
|
||||||
|
|--------------------------------------------------|----------------------------------------------------------------------|
|
||||||
|
| 路由、目标、`fallback` / `stop`、身份组提醒 | [路由与目标](./routes) |
|
||||||
|
| 分组、角色、邀请、自助注册、日志频道 | [分组与访问控制](./groups) |
|
||||||
|
| Webhook 提供方、分组入口、自定义 webhook | [Webhook 接入与租户隔离](./ingress) |
|
||||||
|
| KV / D1 键布局 | [存储布局](./storage) |
|
||||||
|
| 过滤器(模式语法参考) | 下方[过滤器类型](#过滤器类型) / [过滤器教程](./filters) |
|
||||||
|
|
||||||
## 密钥
|
## 密钥
|
||||||
|
|
||||||
WebHooker 需要多个密钥才能运行。本地开发时存储在 `.dev.vars` 中,生产环境使用 Cloudflare Worker Secrets。
|
WebHooker 的运行需要若干密钥。本地开发时放入 `.dev.vars`,生产环境使用 Cloudflare Worker Secrets。
|
||||||
|
|
||||||
### 必需密钥
|
### 必需密钥
|
||||||
|
|
||||||
| 变量 | 说明 |
|
| 变量 | 说明 |
|
||||||
|-------------------------|----------------------------------------------------------|
|
|-------------------------|-------------------------------------------------------------------|
|
||||||
| `GITHUB_WEBHOOK_SECRET` | GitHub App 设置中的 Webhook 密钥 |
|
| `GITHUB_WEBHOOK_SECRET` | GitHub App 设置中的 webhook 密钥 |
|
||||||
| `GITEA_WEBHOOK_SECRET` | Gitea 实例的 Webhook 密钥(仅接收 Gitea webhook 时需要) |
|
| `GITEA_WEBHOOK_SECRET` | Gitea 实例的 webhook 密钥(仅接收 Gitea webhook 时需要) |
|
||||||
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
|
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
|
||||||
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
|
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
|
||||||
| `DISCORD_TOKEN` | Discord Bot Token |
|
| `DISCORD_TOKEN` | Discord 机器人 Token |
|
||||||
| `TELEGRAM_TOKEN` | Telegram Bot Token(BotFather 获取)—— Telegram 路由必需 |
|
| `TELEGRAM_TOKEN` | Telegram 机器人 Token(BotFather 获取)—— Telegram 路由必需 |
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> `GITHUB_APP_ID` 与 `GITHUB_PRIVATE_KEY`(PKCS#8 PEM)用于 GitHub App **安装流程**
|
> `GITHUB_APP_ID` 与 `GITHUB_PRIVATE_KEY`(PKCS#8 PEM)用于 GitHub App **安装流程**
|
||||||
|
|
@ -25,285 +35,38 @@ WebHooker 需要多个密钥才能运行。本地开发时存储在 `.dev.vars`
|
||||||
|
|
||||||
| 变量 | 说明 | 默认值 |
|
| 变量 | 说明 | 默认值 |
|
||||||
|-----------------------------|--------------------------------------------------------------------------------------------------|-------------------------|
|
|-----------------------------|--------------------------------------------------------------------------------------------------|-------------------------|
|
||||||
| `BASE_URL` | OAuth 回调的公开 URL | `http://localhost:8787` |
|
| `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` |
|
||||||
|
| `BASE_URL` | OAuth 回调的公共 URL | `http://localhost:8787` |
|
||||||
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID(或登录名),逗号分隔 | 未设置时 WebUI 关闭 |
|
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID(或登录名),逗号分隔 | 未设置时 WebUI 关闭 |
|
||||||
| `ALLOW_SELF_SIGNUP` | 开启(`1`/`true`)后,没有任何分组权限的 GitHub 用户首次登录会自动获得个人分组而非 403 | 关闭 |
|
| `ALLOW_SELF_SIGNUP` | 开启(`1`/`true`)后,没有任何分组权限的 GitHub 用户首次登录会自动获得个人分组而非 403 | 关闭 |
|
||||||
| `AUDIT_RETENTION_DAYS` | 定时清理时审计日志的保留天数 | `90` |
|
| `AUDIT_RETENTION_DAYS` | 定时清理时审计日志的保留天数 | `90` |
|
||||||
| `NUXT_PUBLIC_DOCS_URL` | 落地页使用的文档站 URL(客户端运行时配置) | 落地页默认值 |
|
| `NUXT_PUBLIC_DOCS_URL` | 落地页使用的文档站 URL(客户端运行时配置) | 落地页默认值 |
|
||||||
| `NUXT_PUBLIC_REPO_URL` | 落地页使用的 GitHub 仓库 URL | 落地页默认值 |
|
| `NUXT_PUBLIC_REPO_URL` | 落地页使用的 GitHub 仓库 URL | 落地页默认值 |
|
||||||
| `NUXT_PUBLIC_LEGAL_CONTACT` | `/terms` 与 `/privacy` 页面展示的联系方式 | 未设置时显示占位文本 |
|
| `NUXT_PUBLIC_LEGAL_CONTACT` | `/terms` 与 `/privacy` 页面展示的联系方式 | 未设置时显示占位文本 |
|
||||||
| `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` |
|
|
||||||
|
|
||||||
投递 id 去重使用 `X-GitHub-Delivery`(GitHub)或 `X-Gitea-Delivery`(Gitea)请求头(存在时)。
|
|
||||||
|
|
||||||
Gitea payload 会被归一化为与 GitHub 相同的内部结构,因此路由、过滤器与 28 个格式化器无需改动即可复用;未知或未映射的 Gitea 事件回退到通用格式化器。仓库/提交/用户链接基于 payload 的 `repository.html_url` 生成,会指向你的 Gitea 实例。
|
|
||||||
|
|
||||||
## Web 控制台
|
## Web 控制台
|
||||||
|
|
||||||
WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理路由。它由 GitHub OAuth 和管理员白名单保护。
|
WebHooker 在 `/admin` 提供内置配置控制台,可在浏览器中管理路由、分组、成员、邀请、发送日志与审计日志。它由 GitHub OAuth 与管理员白名单保护。
|
||||||
|
|
||||||
### 设置
|
### 设置
|
||||||
|
|
||||||
1. 配置 `ADMIN_USER_IDS`,填写允许管理一切的 GitHub 用户 ID,也支持登录名,例如 `ADMIN_USER_IDS=12345,RhenCloud`。未设置时控制台禁用(除非开启 `ALLOW_SELF_SIGNUP`)。
|
1. 配置 `ADMIN_USER_IDS`,填写允许管理一切的 GitHub 用户 ID,也支持登录名,例如 `ADMIN_USER_IDS=12345,RhenCloud`。未设置时控制台禁用(除非开启 `ALLOW_SELF_SIGNUP`)。
|
||||||
2. 打开 `/admin` 并使用 GitHub 登录。
|
2. 打开 `/admin` 并使用 GitHub 登录。
|
||||||
3. 没有任何权限的用户收到 `403`,除非开启 `ALLOW_SELF_SIGNUP=1`(自动获得个人分组)或通过分组[邀请链接](#邀请)加入。
|
3. 没有任何访问权限的用户会得到 `403`,除非 `ALLOW_SELF_SIGNUP=1`(获得个人分组)或跟随分组[邀请链接](./groups#邀请)。
|
||||||
|
|
||||||
### 端点
|
控制台以 SPA 形式在 `/admin` 提供;其标签页可通过 URL 路径直达(`/admin/groups`、`/admin/logs`、`/admin/audit`)。`/admin` 之外未匹配到端点的 URL 直接返回 `404`,而不会展示控制台。
|
||||||
|
|
||||||
控制台以 SPA 形式挂在 `/admin`,各标签页可通过 URL 路径直达(`/admin/groups`、`/admin/logs`、`/admin/audit`)。`/admin` 之外且未匹配下方端点的 URL 直接返回 `404`,不会再被吞进控制台。
|
所有管理端点(`/admin/api/*`)见 [Admin API](../api/admin)。保存的路由会立即写入 KV `config:routes` 并使配置缓存失效,下一次 webhook 处理即会生效。
|
||||||
|
|
||||||
| 端点 | 说明 |
|
|
||||||
|-------------------------------------------------|----------------------------------------------------------|
|
|
||||||
| `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) |
|
|
||||||
| `PUT /admin/api/groups/:id/rename` | 重命名分组(owner);路由、webhook secret 与邀请自动跟随 |
|
|
||||||
| `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 secret(owner) |
|
|
||||||
| `DELETE /admin/api/groups/:id/webhook` | 停用分组 webhook 入口(owner) |
|
|
||||||
|
|
||||||
控制台支持新增、编辑、删除和开关路由。保存后立即写入 KV `config:routes` 并使配置缓存失效,下一次 webhook 处理即会生效。上限:每个实例最多 **200 条路由** 与 **100 个分组**。
|
|
||||||
|
|
||||||
## Webhook 端点
|
|
||||||
|
|
||||||
### 全局端点(`POST /webhook`)
|
|
||||||
|
|
||||||
旧版全局端点使用运维者的全局 secret(`GITHUB_WEBHOOK_SECRET`、`GITEA_WEBHOOK_SECRET`)验签,可分发到**所有**路由。GitHub App 安装事件从该端点进入;多租户场景请用分组的 `installationId` 做隔离。
|
|
||||||
|
|
||||||
### 分组端点(`POST /webhook/{groupId}`)
|
|
||||||
|
|
||||||
每个分组可以启用独立的 webhook 入口和 secret(在分组页面「Webhook 入口」面板生成,owner 权限)。载荷使用**分组的** secret 验签,且只有该分组的路由会触发。SaaS 用户可以借此配置 Gitea、classic GitHub 或自定义 webhook,无需共享(也无需知道)运维者的全局 secret。
|
|
||||||
|
|
||||||
- 支持所有 provider:GitHub(`X-Hub-Signature-256`)、Gitea(`X-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`、出现在分组的日志频道。
|
|
||||||
|
|
||||||
载荷格式:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"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 | 可选消息颜色:颜色词(`red`、`green`、`yellow`、`blue`、`purple`、`orange`、`cyan`、`gray`)或 `#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` 过滤)。
|
|
||||||
|
|
||||||
绑定是**自动配置**的 —— 将 GitHub App 的 _Setup URL_ 指向 `{BASE_URL}/auth/github/install`。用户安装 App 后浏览器立即跳转到该页面(页面需要已登录的管理员会话——未登录用户会先被重定向走 OAuth 流程),可选择将安装绑定到:**新分组**(`inst-{installationId}`,默认)或任意**自己拥有 owner 权限的已有分组**(提交时再次校验角色;由 `POST /auth/github/install/bind` 完成配置)。无需手动填写 ID。作为兜底(例如未配置 Setup URL 时),`installation.created` webhook 事件也会自动创建/绑定分组 —— `owners` 匹配安装账号的现有分组会被绑定,否则创建独立的 `inst-{installationId}` 分组。之后在控制台为分组添加路由和成员即可。
|
|
||||||
|
|
||||||
## 路由
|
|
||||||
|
|
||||||
路由定义了哪些事件被转发到哪些频道(Discord 或 Telegram)。它们以 JSON 数组形式存储在 Cloudflare KV 中,键为 `config:routes`。
|
|
||||||
|
|
||||||
**没有默认路由**——每条路由必须自行定义目标频道。若未配置任何路由,则不会转发任何事件。
|
|
||||||
|
|
||||||
### 路由模式
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"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)且机器人能看到该身份组时,提醒才会真正触发通知。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"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 |
|
|
||||||
|
|
||||||
### 自定义路由示例
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"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`。
|
|
||||||
|
|
||||||
### 分组模式
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"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 | 是 | 小写 id(`a-z0-9`、`-`),由每条路由的 `groupId` 引用。可修改:重命名分组会同步更新其路由、分组级 webhook secret 与待接受邀请 |
|
|
||||||
| `name` | string | 是 | 可读的分组名称 |
|
|
||||||
| `members` | object[] | 否 | `{ login, role }` 列表;角色为 `owner`、`admin` 或 `viewer` |
|
|
||||||
| `adminIds` | string[] | 否 | 已废弃的旧字段;存在时按 role 为 `owner` 的成员处理 |
|
|
||||||
| `owners` | string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 |
|
|
||||||
| `providers` | string[] | 否 | 允许进入该分组的来源平台(`github`、`gitea`);为空表示全部 |
|
|
||||||
| `installationId` | number | 否 | 绑定到该分组的 GitHub App 安装 ID;只接受该安装的事件(为空表示全部) |
|
|
||||||
| `emoji` | boolean | 否 | 是否在该分组消息中显示 emoji(默认 `true`) |
|
|
||||||
| `lang` | string | 否 | 该分组所有路由的消息语言(如 `en`、`zh`;可通过 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** 管理本组的路由、成员、邀请、名称、id、`emoji` 与 `providers`;不能移除最后一位 owner,也没有其他 owner 时不能把自己降级。
|
|
||||||
- **admin** 可编辑本组路由并查看日志;**viewer** 只读控制台。
|
|
||||||
- 分组管理端点通过 `/admin/api/groups/:id/routes` 一次只操作一个分组;`groupId` 由路径参数强制指定。
|
|
||||||
- `owners` 列表限定哪些事件参与者(发送者登录名)的事件会被该分组的路由投递。
|
|
||||||
- `providers` 列表限定哪个 forge(`github`、`gitea`)的事件会被该分组的路由投递。即使组织/用户同名,也可以借此将 GitHub 与 Gitea 分组区分开。
|
|
||||||
|
|
||||||
### Webhook 日志频道
|
|
||||||
|
|
||||||
分组可以设置 `logTarget` 指向一个 Discord 频道/子区或 Telegram 群组/话题。每当该分组的路由分发(dispatch)一个 webhook,就会向那里发送一条摘要消息:事件类型/动作、仓库、投递 ID,以及每条「路由 × 目标」一行的 ✅/❌ 结果(失败时附带错误信息;最多列出前 10 行,其余以 `+N` 汇总)。全部成功时消息为绿色,任一失败则为红色。摘要使用分组的消息语言。日志消息尽力发送,本身不会被记入 D1 发送日志。
|
|
||||||
|
|
||||||
### 邀请
|
|
||||||
|
|
||||||
owner(及超级管理员)可在分组的「成员」面板创建一次性邀请链接,7 天内有效。接受邀请后用户以邀请角色(`admin` 或 `viewer`,绝不授予 `owner`)加入;已有的 `viewer` 会被升级为 `admin`。邀请存储在 KV `invite:{token}`。
|
|
||||||
|
|
||||||
### 自助注册
|
|
||||||
|
|
||||||
开启 `ALLOW_SELF_SIGNUP=1` 后,没有分组权限的 GitHub 用户首次登录会获得一个由自己担任 owner 的个人分组(`u-{userId}`),而不是 `403`。这是全自助 SaaS 部署的入口;关闭它则控制台保持仅邀请制。
|
|
||||||
|
|
||||||
## 过滤器类型
|
## 过滤器类型
|
||||||
|
|
||||||
实操指南见[过滤器教程](./filters),包含完整示例。
|
实操指南见[过滤器教程](./filters),包含完整示例。
|
||||||
|
|
||||||
| 类型 | 匹配对象 | 示例 |
|
| 类型 | 匹配对象 | 示例 |
|
||||||
|-----------|------------------|-------------------------------------------|
|
|-----------|------------------|--------------------------------------|
|
||||||
| `event` | GitHub 事件名称 | `push`, `pull_*`, `pull_request` |
|
| `event` | GitHub 事件名称 | `push`, `pull_*`, `pull_request` |
|
||||||
| `repo` | 仓库全名 | `org/repo`, `org/*` |
|
| `repo` | 仓库全名 | `org/repo`, `org/*` |
|
||||||
| `actor` | 发送者登录名 | `username`, `[bot]`, `*[bot]` |
|
| `actor` | 发送者登录名 | `username`, `[bot]`, `*[bot]` |
|
||||||
|
|
@ -328,37 +91,3 @@ owner(及超级管理员)可在分组的「成员」面板创建一次性邀
|
||||||
{ "type": "event", "match": "push" }
|
{ "type": "event", "match": "push" }
|
||||||
{ "type": "event", "match": ["push", "pull_request"] }
|
{ "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 秒 |
|
|
||||||
| `delivery:{groupId}:{id}` | 分组级 webhook 入口的租户级投递去重 | 300 秒 |
|
|
||||||
| `tenant:{groupId}` | 分组 webhook secret(64 位 hex,控制台生成) | 永久 |
|
|
||||||
| `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_id` → `github_user_id`,用于 Discord `/gh` 命令 |
|
|
||||||
| `telegram_links` | 映射 `telegram_user_id` → `github_user_id`,用于 Telegram `/gh` 命令 |
|
|
||||||
|
|
||||||
`audit_logs` 由定时触发器按 `AUDIT_RETENTION_DAYS`(默认 90)自动清理。
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
## 如何让分组使用自己的 webhook 端点?
|
## 如何让分组使用自己的 webhook 端点?
|
||||||
|
|
||||||
见[分组端点](./configuration#分组端点)——在分组的 **Webhook 端点**面板(owner 角色)生成密钥,然后用 `POST /webhook/{groupId}` 与分组密钥发送。
|
见[分组端点](./ingress#分组端点)——在分组的 **Webhook 端点**面板(owner 角色)生成密钥,然后用 `POST /webhook/{groupId}` 与分组密钥发送。
|
||||||
|
|
||||||
## 可以脱离 Cloudflare Workers 运行吗?
|
## 可以脱离 Cloudflare Workers 运行吗?
|
||||||
|
|
||||||
|
|
@ -34,4 +34,4 @@
|
||||||
|
|
||||||
## 数据存储在哪里?
|
## 数据存储在哪里?
|
||||||
|
|
||||||
配置存于 Cloudflare KV(`config:routes`、`config:groups`);发送/审计日志与平台↔GitHub 绑定存于 D1。见[存储布局](./configuration#kv-存储布局)。
|
配置存于 Cloudflare KV(`config:routes`、`config:groups`);发送/审计日志与平台↔GitHub 绑定存于 D1。见[存储布局](./storage#kv-存储布局)。
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# 过滤器教程
|
# 过滤器教程
|
||||||
|
|
||||||
过滤器决定哪些 Webhook 事件会被[路由](./configuration#路由)转发。只有当路由 `filters` 数组中的**每一个**过滤器都匹配时,路由才会触发(AND 逻辑)。本页是一份上手教程:解释每种过滤器类型的行为,以及如何组合它们实现真实场景的路由规则。
|
过滤器决定哪些 Webhook 事件会被[路由](./routes)转发。只有当路由 `filters` 数组中的**每一个**过滤器都匹配时,路由才会触发(AND 逻辑)。本页是一份上手教程:解释每种过滤器类型的行为,以及如何组合它们实现真实场景的路由规则。
|
||||||
|
|
||||||
参考表格见配置指南的[过滤器类型](./configuration#过滤器类型),完整事件列表见[支持的事件](../events/supported)。
|
参考表格见配置指南的[过滤器类型](./configuration#过滤器类型),完整事件列表见[支持的事件](../events/supported)。
|
||||||
|
|
||||||
|
|
|
||||||
65
docs/zh/guide/groups.md
Normal file
65
docs/zh/guide/groups.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# 分组与访问控制
|
||||||
|
|
||||||
|
路由归属于分组。分组用于划分管理权限,并可限制进入其中的事件。它们以 JSON 数组形式存储在 Cloudflare KV 的 `config:groups` 键下,可通过 [Web 控制台](./configuration#web-控制台)或 [Admin API](../api/admin) 管理。每个实例最多可保存 **100 个分组**。
|
||||||
|
|
||||||
|
## 分组模式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "backend-team",
|
||||||
|
"name": "Backend Team",
|
||||||
|
"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 | 是 | 小写 id(`a-z0-9`、`-`);被每条路由的 `groupId` 引用。可编辑:重命名分组会同步其路由、分组 webhook secret 与待处理邀请 |
|
||||||
|
| `name` | string | 是 | 人类可读的分组名 |
|
||||||
|
| `members` | object[] | 否 | `{ login, role }` 条目;角色为 `owner`、`admin` 或 `viewer` |
|
||||||
|
| `adminIds` | string[] | 否 | 已废弃的旧字段;存在时视为角色为 `owner` 的 `members` |
|
||||||
|
| `owners` | string[] | 否 | 允许进入该分组的事件所属组织/用户登录名;空 = 全部 |
|
||||||
|
| `providers` | string[] | 否 | 允许进入该分组的来源平台(`github`、`gitea`);空 = 全部 |
|
||||||
|
| `installationId` | number | 否 | 绑定到该分组的 GitHub App 安装 id;仅接受该安装的事件(空 = 全部) |
|
||||||
|
| `emoji` | boolean | 否 | 该分组消息是否包含表情(默认 `true`) |
|
||||||
|
| `lang` | string | 否 | 该分组所有路由的消息语言(如 `en`、`zh`;可通过 KV `i18n:<lang>` 自定义)——见[消息语言](./i18n)——默认 `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** 管理自己分组的路由、成员、邀请、名称、id、`emoji` 与 `providers`。不能移除最后一个 owner,也不能在没有其他 owner 时降级自己。
|
||||||
|
- **Admin** 编辑自己分组内的路由并查看日志;**viewer** 只有只读控制台。
|
||||||
|
- 分组管理端点通过 `/admin/api/groups/:id/routes` 一次操作一个分组;`groupId` 强制取自路径参数。
|
||||||
|
- `owners` 列表限制该分组路由究竟会分发哪些事件操作者(发送者登录名)的事件。
|
||||||
|
- `providers` 列表限制该分组路由会分发哪个 forge(`github`、`gitea`)的事件。即使组织/用户名冲突,也可借此将 GitHub 与 Gitea 分组分开。
|
||||||
|
|
||||||
|
## Webhook 日志频道
|
||||||
|
|
||||||
|
分组可以设置 `logTarget` 指向一个 Discord 频道/子区或 Telegram 群组/话题。每当该分组的路由分发(dispatch)一个 webhook,就会向那里发送一条摘要消息:事件类型/动作、仓库、投递 ID,以及每条「路由 × 目标」一行的 ✅/❌ 结果(失败时附带错误信息;最多列出前 10 行,其余以 `+N` 汇总)。全部成功时消息为绿色,任一失败则为红色。摘要使用分组的消息语言。日志消息尽力发送,本身不会被记入 D1 发送日志。
|
||||||
|
|
||||||
|
## 邀请
|
||||||
|
|
||||||
|
Owner(与超级管理员)可以在分组的 _成员_ 面板创建单次使用、有效期 7 天的邀请链接。接受邀请后,用户以被邀请的角色(`admin` 或 `viewer`——绝不会是 `owner`)加入;已有 `viewer` 会被升级为 `admin`。邀请存储在 KV 的 `invite:{token}` 键下。
|
||||||
|
|
||||||
|
## 自助注册
|
||||||
|
|
||||||
|
设置 `ALLOW_SELF_SIGNUP=1` 后,没有分组权限的 GitHub 用户首次登录时会获得个人分组(`u-{userId}`,归其所有),而不是 `403`。这是完全自助式 SaaS 安装的入口;关闭它可保持控制台仅邀请制。
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
## 分组语言
|
## 分组语言
|
||||||
|
|
||||||
设置 `Group.lang`(如 `"zh"`)可为分组内所有路由选择消息语言——见[分组 → 分组模式](./configuration#分组模式)。分组的 webhook 日志频道摘要使用相同的语言。
|
设置 `Group.lang`(如 `"zh"`)可为分组内所有路由选择消息语言——见[分组 → 分组模式](./groups#分组模式)。分组的 webhook 日志频道摘要使用相同的语言。
|
||||||
|
|
||||||
## 自定义覆盖
|
## 自定义覆盖
|
||||||
|
|
||||||
|
|
|
||||||
71
docs/zh/guide/ingress.md
Normal file
71
docs/zh/guide/ingress.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Webhook 接入与租户隔离
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
投递 id 去重使用 `X-GitHub-Delivery`(GitHub)或 `X-Gitea-Delivery`(Gitea)请求头(存在时)。
|
||||||
|
|
||||||
|
Gitea 载荷会被归一化为与 GitHub 事件相同的内部结构,因此路由、过滤器与 28 个格式化器无需改动即可工作。未知或未映射的 Gitea 事件回退到通用格式化器。仓库/提交/用户链接取自载荷中的 `repository.html_url`,因此指向你的 Gitea 实例。
|
||||||
|
|
||||||
|
## 全局端点(`POST /webhook`)
|
||||||
|
|
||||||
|
全局端点使用运营者的全局密钥(`GITHUB_WEBHOOK_SECRET`、`GITEA_WEBHOOK_SECRET`)验签,并分发到**所有**路由。GitHub App 安装的事件都在此送达;在分组上设置 `installationId` 可保持租户隔离。
|
||||||
|
|
||||||
|
## 分组端点(`POST /webhook/{groupId}`)
|
||||||
|
|
||||||
|
每个分组都可以选择接入自己的 webhook 入口,使用独立的密钥(在分组页面的 _Webhook 端点_ 面板生成,owner 角色)。载荷使用**分组**的密钥验签,而不是全局密钥,并且只有该分组的路由会被触发。SaaS 用户以此配置 Gitea、经典 GitHub 或自定义 webhook,而无需共享(或知道)运营者的密钥。
|
||||||
|
|
||||||
|
- 支持任意提供方:GitHub(`X-Hub-Signature-256`)、Gitea(`X-Gitea-Signature`)、自定义(`X-WebHooker-Signature`)
|
||||||
|
- 密钥为 64 位 hex 字符串;在控制台重新生成会立即失效旧密钥
|
||||||
|
- 投递 id 去重键按租户隔离(`delivery:{groupId}:{id}`)
|
||||||
|
- 分组没有密钥(或已不存在)时端点返回 `404`
|
||||||
|
|
||||||
|
## 自定义 Webhook
|
||||||
|
|
||||||
|
向 `POST /webhook/{groupId}`(或全局端点)POST 任意 JSON,并使用分组密钥将原始 body 的 HMAC-SHA256 以 `X-WebHooker-Signature: sha256=<hex>` 签名。载荷会变成 `custom` 事件,走正常的路由管线——创建一条 `event: custom` 的路由(控制台有模板),即可分发到该路由的目标、记录 `send_logs`,并出现在分组的 webhook 日志频道中。
|
||||||
|
|
||||||
|
载荷模式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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 | 消息标题(缺省时为 "Custom message") |
|
||||||
|
| `description` | string | 可选消息正文 |
|
||||||
|
| `color` | string | 可选嵌入颜色:颜色词(`red`、`green`、`yellow`、`blue`、`purple`、`orange`、`cyan`、`gray`)或 `#rrggbb` |
|
||||||
|
| `url` | string | 可选的标题链接 |
|
||||||
|
| `repo` | string | 可选 `owner/repo`;作为标题前缀并用作页脚 |
|
||||||
|
| `author` | object | 可选 `{ name, iconUrl, url }` |
|
||||||
|
| `fields` | object[] | 可选嵌入字段 `{ name, value, inline }` |
|
||||||
|
| `footer` | string | 可选页脚覆盖 |
|
||||||
|
| `deliveryId` | string | 可选的发送方去重 id(重试) |
|
||||||
|
|
||||||
|
## GitHub App 租户隔离
|
||||||
|
|
||||||
|
GitHub App 安装后,**所有**安装方的事件都会到达全局端点。要让租户互相隔离,请把每个分组绑定到应当为其提供事件的安装 ID:`"installationId": 12345678`。该 ID 可从 App 安装 webhook 载荷(`installation.id`)或 GitHub App 安装页 URL 看到。即使分组的 `owners` 为空,来自其它安装的事件也会被拒绝。未设置 `installationId` 的分组保持旧行为(`owners` 过滤)。
|
||||||
|
|
||||||
|
绑定是**自动配置**的 —— 将 GitHub App 的 _Setup URL_ 指向 `{BASE_URL}/auth/github/install`。用户安装 App 后浏览器立即跳转到该页面(页面需要已登录的管理员会话——未登录用户会先被重定向走 OAuth 流程),可选择将安装绑定到:**新分组**(`inst-{installationId}`,默认)或任意**自己拥有 owner 权限的已有分组**(提交时再次校验角色;由 `POST /auth/github/install/bind` 完成配置)。无需手动填写 ID。作为兜底(例如未配置 Setup URL 时),`installation.created` webhook 事件也会自动创建/绑定分组 —— `owners` 匹配安装账号的现有分组会被绑定,否则创建独立的 `inst-{installationId}` 分组。之后在控制台为分组添加路由和成员即可。
|
||||||
|
|
||||||
|
要在选择页显示安装所属账号的登录名,请设置 `GITHUB_APP_ID` 与 `GITHUB_PRIVATE_KEY`——见[密钥](./configuration#密钥)。
|
||||||
|
|
@ -34,4 +34,4 @@
|
||||||
|
|
||||||
## Webhook 日志频道
|
## Webhook 日志频道
|
||||||
|
|
||||||
分组还可以在 Discord 频道/子区或 Telegram 群组/话题中接收每条 webhook 的摘要消息——见[分组 → Webhook 日志频道](./configuration#webhook-日志频道)。这些摘要是尽力发送的,**不会**记录到 `send_logs`。
|
分组还可以在 Discord 频道/子区或 Telegram 群组/话题中接收每条 webhook 的摘要消息——见[分组 → Webhook 日志频道](./groups#webhook-日志频道)。这些摘要是尽力发送的,**不会**记录到 `send_logs`。
|
||||||
|
|
|
||||||
86
docs/zh/guide/routes.md
Normal file
86
docs/zh/guide/routes.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# 路由与目标
|
||||||
|
|
||||||
|
路由决定哪些事件被转发到哪个频道(Discord 或 Telegram)。它们以 JSON 数组形式存储在 Cloudflare KV 的 `config:routes` 键下,可通过 [Web 控制台](./configuration#web-控制台)、[Admin API](../api/admin) 或 `config.example.yaml` 管理。
|
||||||
|
|
||||||
|
**没有默认路由**——每条路由都必须定义自己的目标。未配置任何路由时不会转发任何事件。每个实例最多可保存 **200 条路由**。
|
||||||
|
|
||||||
|
## 路由模式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "unique-route-id",
|
||||||
|
"name": "Human-readable 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": "REQUIRED_CHANNEL_ID",
|
||||||
|
"threadId": "OPTIONAL_THREAD_ID"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`targets` 的每一项都是一个推送目标,因此一条路由可同时转发到多个频道(例如一个 Discord 频道**和**一个 Telegram 群组)。`target.platform` 选择平台:`discord`(默认)或 `telegram`。**Discord** 目标要求 `target.channelId`(可选 `target.threadId` 指定子区);**Telegram** 目标要求 `target.chatId`(群组/超级群组 id,如 `-1001234567890`),可选 `target.topicId`(话题的 `message_thread_id`,相当于 Discord 子区)。没有默认频道回退。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必需 | 说明 |
|
||||||
|
|------------------|----------|------|-----------------------------------------------------------------------------|
|
||||||
|
| `groupId` | string | 是 | 路由所属[分组](./groups)的 id |
|
||||||
|
| `fallback` | boolean | 否 | 为 `true` 时仅在没有其他非 fallback 路由匹配时才触发;其自身过滤器被忽略 |
|
||||||
|
| `stop` | boolean | 否 | 为 `true` 且该路由匹配时,不再评估后续路由 |
|
||||||
|
| `discordRoleIds` | string[] | 否 | 路由触发时要提醒的 Discord 身份组 id;仅对 Discord 目标生效 |
|
||||||
|
|
||||||
|
## Discord 身份组提醒
|
||||||
|
|
||||||
|
在路由上设置 `discordRoleIds` 可在其触发时提醒一个或多个 Discord 身份组(角色)。提醒(`<@&roleId>`)会加在路由所有 **Discord** 目标的消息内容前;Telegram 目标忽略该字段。只有当机器人拥有 `Mention Everyone` 权限(或身份组标记为可提及)且能看到该身份组时,提醒才会触发通知。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "release-notify",
|
||||||
|
"name": "Notify on Release",
|
||||||
|
"enabled": true,
|
||||||
|
"groupId": "default",
|
||||||
|
"discordRoleIds": ["111111111111111111", "222222222222222222"],
|
||||||
|
"filters": [{ "type": "event", "match": "release" }],
|
||||||
|
"targets": [{ "platform": "discord", "channelId": "REQUIRED_CHANNEL_ID" }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以在管理控制台的 _Discord 身份组提醒_ 中填写身份组 id。
|
||||||
|
|
||||||
|
## 过滤器
|
||||||
|
|
||||||
|
每条路由携带 `filters` 数组(全部匹配才触发——AND 逻辑)。见[过滤器类型](./configuration#过滤器类型)参考与[过滤器教程](./filters)。
|
||||||
|
|
||||||
|
## 自定义路由示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "backend-prs",
|
||||||
|
"name": "Backend PRs",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
35
docs/zh/guide/storage.md
Normal file
35
docs/zh/guide/storage.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# 存储布局
|
||||||
|
|
||||||
|
## 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 秒 |
|
||||||
|
| `delivery:{groupId}:{id}` | 分组级 webhook 入口的租户级投递去重 | 300 秒 |
|
||||||
|
| `tenant:{groupId}` | 分组 webhook secret(64 位 hex,控制台生成) | 永久 |
|
||||||
|
| `msg:{routeId}:{key}:{target}` | 原地更新用消息 id 追踪(如 `workflow_run` / `check_run`) | 7 天 |
|
||||||
|
| `cmd:guild:{id}` | 已注册命令的服务器 id(去重) | 永久 |
|
||||||
|
| `cmd:registered:global` | 全局命令注册标记(去重) | 1 天 |
|
||||||
|
| `config:discord-app-id` | 缓存的 Discord 应用 id | 永久 |
|
||||||
|
| `i18n:{lang}` | 叠加在英文之上的翻译覆盖 | 永久 |
|
||||||
|
|
||||||
|
## D1 存储布局
|
||||||
|
|
||||||
|
D1 数据库(`DB` 绑定,数据库 `webhooker`)包含四张表:
|
||||||
|
|
||||||
|
| 表 | 用途 |
|
||||||
|
|-------------------|--------------------------------------------------------------------------------------------------|
|
||||||
|
| `send_logs` | 每次分发尝试一行(路由 id、事件、目标、ok/error、耗时、错误码、详情) |
|
||||||
|
| `audit_logs` | 每次管理员操作一行(登录/登出、分组/路由/成员/邀请变更) |
|
||||||
|
| `discord_links` | 映射 `discord_user_id` → `github_user_id`,供 `/gh` Discord 命令使用 |
|
||||||
|
| `telegram_links` | 映射 `telegram_user_id` → `github_user_id`,供 `/gh` Telegram 命令使用 |
|
||||||
|
|
||||||
|
`audit_logs` 由定时任务在 `AUDIT_RETENTION_DAYS`(默认 90)后自动清理。行字段说明见[日志](./logs)。
|
||||||
Loading…
Add table
Add a link
Reference in a new issue