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

@ -12,7 +12,7 @@ import {
} from "~/composables/useFilterNode";
interface TargetForm {
platform: "discord" | "telegram";
platform: "discord" | "telegram" | "feishu";
channelId: string;
threadId: string;
chatId: string;
@ -129,6 +129,10 @@ function collect(): Route | null {
const chatId = tg.chatId.trim();
if (!chatId) continue;
targets.push({ platform: "telegram", chatId, topicId: tg.topicId.trim() || undefined });
} else if (tg.platform === "feishu") {
const chatId = tg.chatId.trim();
if (!chatId) continue;
targets.push({ platform: "feishu", chatId });
} else {
const channelId = tg.channelId.trim();
if (!channelId) continue;
@ -170,7 +174,7 @@ function save(): void {
return;
}
form.targets.forEach((tg, i) => {
if (tg.platform === "telegram" && !tg.chatId.trim()) {
if ((tg.platform === "telegram" || tg.platform === "feishu") && !tg.chatId.trim()) {
targetError.value = t("routeEditor.errChat", { n: i + 1 });
} else if (tg.platform === "discord" && !tg.channelId.trim()) {
targetError.value = t("routeEditor.errChannel", { n: i + 1 });
@ -283,7 +287,7 @@ watch(
? r.targets.map((tg) => ({
...blankTarget(),
...tg,
platform: tg.platform === "telegram" ? "telegram" : "discord",
platform: tg.platform === "telegram" ? "telegram" : tg.platform === "feishu" ? "feishu" : "discord",
}))
: [blankTarget()];
if (r?.ast) {
@ -481,6 +485,7 @@ watch(
<select v-model="tg.platform" class="tg-select">
<option value="discord">Discord</option>
<option value="telegram">Telegram</option>
<option value="feishu">{{ t("routeEditor.platformFeishu") }}</option>
</select>
<template v-if="tg.platform === 'discord'">
<input
@ -494,7 +499,7 @@ watch(
:placeholder="t('routeEditor.threadPlaceholder')"
/>
</template>
<template v-else>
<template v-else-if="tg.platform === 'telegram'">
<input
v-model="tg.chatId"
class="input tg-in1"
@ -506,6 +511,13 @@ watch(
:placeholder="t('routeEditor.topicPlaceholder')"
/>
</template>
<template v-else>
<input
v-model="tg.chatId"
class="input tg-in1"
:placeholder="t('routeEditor.feishuChatPlaceholder')"
/>
</template>
<button
type="button"
class="icon-btn danger tg-del"