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

@ -0,0 +1,37 @@
import type { RouteTarget, Env, NeutralMessage } from "../../types";
import type { PlatformDriver, SendResult } from "../types";
import { getTenantAccessToken, sendMessage, updateMessage } from "./rest";
import { renderNeutralMessage } from "./render";
export class FeishuDriver implements PlatformDriver {
readonly id = "feishu";
async send(message: NeutralMessage, target: RouteTarget, env: Env): Promise<SendResult> {
const chatId = target.chatId ?? "";
if (!chatId) {
return { ok: false, error: "target.chatId is required", errorCode: "NO_TARGET" };
}
const tokenRes = await getTenantAccessToken(env);
if (!tokenRes.ok || !tokenRes.token) {
return { ok: false, error: tokenRes.error ?? "No token", errorCode: tokenRes.errorCode ?? "NO_TOKEN" };
}
return sendMessage(tokenRes.token, chatId, renderNeutralMessage(message));
}
async edit(
message: NeutralMessage,
target: RouteTarget,
env: Env,
messageId: string,
): Promise<SendResult> {
const chatId = target.chatId ?? "";
if (!chatId) {
return { ok: false, error: "target.chatId is required", errorCode: "NO_TARGET" };
}
const tokenRes = await getTenantAccessToken(env);
if (!tokenRes.ok || !tokenRes.token) {
return { ok: false, error: tokenRes.error ?? "No token", errorCode: tokenRes.errorCode ?? "NO_TOKEN" };
}
return updateMessage(tokenRes.token, messageId, renderNeutralMessage(message));
}
}