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

@ -13,7 +13,7 @@ import {
handleOAuthCallback as handleGithubOAuthCallback,
getInstallationAccount,
} from "../github/oauth";
import { removeToken, saveDiscordLink, saveTelegramLink } from "../github/store";
import { removeToken, saveDiscordLink, saveTelegramLink, saveFeishuLink } from "../github/store";
import { createAdminSession, adminCookie, getAdminSession } from "./session";
import {
loadGroups,
@ -27,6 +27,7 @@ import {
import { clientIp } from "./auth";
import { recordAudit } from "../lib/audit";
import { sendMessage } from "../drivers/telegram/rest";
import { getTenantAccessToken, sendText as sendFeishuText } from "../drivers/feishu/rest";
import { cfEnv } from "../cf";
import { initConfigStore } from "../config";
import type { Env, Group } from "../types";
@ -37,6 +38,8 @@ interface PendingState {
discordUserId?: string;
telegramUserId?: string;
telegramChatId?: string;
feishuUserId?: string;
feishuChatId?: string;
}
function linkedPage(login: string): string {
@ -337,6 +340,22 @@ export async function handleOAuthCallback(event: H3Event): Promise<unknown> {
return { ok: true, telegramUserId: pending.telegramUserId, login: result.login };
}
// Feishu account-linking flow: bind the Feishu user to this GitHub account.
if (pending.feishuUserId) {
await saveFeishuLink(env.DB, pending.feishuUserId, result.userId);
if (pending.feishuChatId) {
const tokenRes = await getTenantAccessToken(env);
if (tokenRes.ok && tokenRes.token) {
await sendFeishuText(
tokenRes.token,
pending.feishuChatId,
`✅ GitHub 账号已绑定:**@${result.login}**。现在可以用 /gh comment 评论了。`,
).catch(() => undefined);
}
}
return { ok: true, feishuUserId: pending.feishuUserId, login: result.login };
}
const isBrowser = (getHeader(event, "accept") ?? "").includes("text/html");
if (isBrowser) {
// Invite accept flow: the redirect target is the invite page, which