feat(feishu): add Feishu inbound webhook, /gh commands and card actions

- add feishu_links D1 table and link store helpers
- implement X-Lark-Signature verification, url_verification, /gh login|logout|comment|merge|close and card.action.trigger Merge/Close
- render interactive cards with clickable title link, inline links and callback buttons (no whole-card card_link)
- bind Feishu account in OAuth callback
- document event subscription and required scopes
This commit is contained in:
RhenCloud 2026-08-27 00:04:49 +08:00
parent 3437ac5513
commit 4b99f6d33d
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
38 changed files with 2449 additions and 1412 deletions

View file

@ -24,6 +24,8 @@ WebHooker requires several secrets to function. For local development, store the
| `GITHUB_CLIENT_SECRET` | OAuth client secret from App settings |
| `DISCORD_TOKEN` | Discord bot token |
| `TELEGRAM_TOKEN` | Telegram bot token (from BotFather) — required for Telegram routes |
| `FEISHU_APP_ID` | Feishu app ID (from the app Credentials page) — required for Feishu routes |
| `FEISHU_APP_SECRET` | Feishu app secret — required for Feishu routes |
> [!NOTE]
> `GITHUB_APP_ID` and `GITHUB_PRIVATE_KEY` (PKCS#8 PEM) are used by the GitHub App

View file

@ -30,6 +30,8 @@ bunx wrangler secret put GITHUB_CLIENT_SECRET
bunx wrangler secret put DISCORD_TOKEN
bunx wrangler secret put DISCORD_PUBLIC_KEY # Discord app public key (Developer Portal) — required for interactions
bunx wrangler secret put TELEGRAM_TOKEN # Telegram bot token (BotFather) — required for Telegram routes
bunx wrangler secret put FEISHU_APP_ID # Feishu app ID — required for Feishu routes
bunx wrangler secret put FEISHU_APP_SECRET # Feishu app secret — required for Feishu routes
bunx wrangler secret put ADMIN_USER_IDS # comma-separated GitHub IDs/logins allowed into the Web UI
```
@ -196,6 +198,48 @@ In Telegram, `/gh` commands (`/gh login`, `/gh logout`, `/gh comment <text>`, `/
Avatars are rendered as a link-preview card using the built-in `GET /api/richheader` (overridable with `TELEGRAM_RICH_HEADER_HOST`).
## Feishu Bot Setup
1. Go to [Feishu Open Platform](https://open.feishu.cn/app) → **Create App** → choose **Custom App** → give it a name.
2. In **Credentials & Basic Info**, copy **App ID** and **App Secret** to `FEISHU_APP_ID` and `FEISHU_APP_SECRET`.
3. In **Permissions & Scopes**, add at least one of the following message scopes so the app can send (and read) messages:
- `im:message` (read and send direct messages and group chat messages)
- `im:message:send_as_bot` (send messages as an app)
- `im:message:send` (historical version)
4. In **Bot** tab, turn on the bot capability. Add the bot to the target group chat (or create a new group) and copy the **Chat ID** from the group settings.
5. In WebHooker `/admin`, create or edit a route and add a target with `platform: "feishu"`, `chatId` set to the Feishu **Chat ID**, and optional `topicId` for a topic inside the chat.
WebHooker uses the app-level credentials (`FEISHU_APP_ID` / `FEISHU_APP_SECRET`) to request a `tenant_access_token`, caches it until expiry, and sends messages as an interactive card (`interactive` message type). The same token is used to edit messages in place for `workflow_run` / `check_run` progress updates.
### Inbound: commands & buttons
WebHooker can receive Feishu events and let users act on PRs/Issues from chat, the same way as Discord and Telegram:
- `/gh login` — link your GitHub account (opens an OAuth page).
- `/gh logout` — unlink your GitHub account.
- `/gh comment <PR/Issue 链接> <内容>` — comment as your linked GitHub user.
- `/gh merge <PR 链接>` / `/gh close <PR 链接>` — merge / close the PR as your linked user.
- The **Merge** / **Close** buttons on a card trigger the same actions.
To enable inbound:
1. In the app **Events & Callbacks****Event Subscriptions**, set the **Request URL** to `https://<your-worker>/feishu/webhook` (Feishu will send a `url_verification` challenge, which WebHooker answers automatically).
2. Subscribe to the events:
- `im.message.receive_v1` — receive `/gh` commands (requires the `im:message` scope).
- `card.action.trigger` — receive button clicks on cards.
3. In **Credentials & Basic Info**, set the **App Secret** (already used for `FEISHU_APP_SECRET`) — it also signs the inbound callback via the `X-Lark-Signature` header, and WebHooker verifies it.
### Required permissions
| Permission | Purpose |
| ---------- | ------- |
| `im:message` | Read and send direct messages and group chat messages. |
| `im:message:send_as_bot` | Send messages as an app bot (alternative to `im:message`). |
| `im:message:send` | Send messages V2 (historical version, alternative). |
> [!NOTE]
> Custom bots (group-level webhook URL) are not supported. WebHooker uses an **app bot** so message editing, token caching, multi-group routing, and inbound commands work the same way as Discord and Telegram.
## Custom Domain (Optional)
To use a custom domain instead of `*.workers.dev`:

View file

@ -11,7 +11,7 @@ Every dispatch attempt is recorded in the D1 `send_logs` table and browsable in
| `event` | Event type (e.g. `push`, `pull_request`, `custom`) |
| `repo` | Repository full name (when present) |
| `target` | Target id the message was sent to |
| `platform` | `discord` or `telegram` |
| `platform` | `discord`, `telegram` or `feishu` |
| `ok` | Whether the send succeeded |
| `status` | HTTP status from the platform API (when applicable) |
| `error` | Error message (when failed) |

View file

@ -29,7 +29,7 @@ There are **no default routes** — each route must define its own target. If no
}
```
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.
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), `telegram` or `feishu`. For **Discord**, `target.channelId` is required (a thread in `target.threadId` is optional). For **Telegram** and **Feishu**, `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 |
| ---------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------- |