WebHooker/server/lib/drivers/feishu/render.ts
RhenCloud 4b99f6d33d
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
2026-08-27 00:07:05 +08:00

153 lines
4.2 KiB
TypeScript

import type { NeutralMessage } from "../../types";
import { cap, splitMessageTitle } from "../../formatters/helpers";
const MAX_HEADER_TITLE = 100;
const MAX_MARKDOWN = 4000;
const MAX_NOTE = 500;
type FeishuTemplate =
| "blue"
| "green"
| "red"
| "yellow"
| "orange"
| "purple"
| "indigo"
| "wathet"
| "lime"
| "grey";
function colorToTemplate(color?: number): FeishuTemplate {
if (color == null) return "blue";
const r = (color >> 16) & 0xff;
const g = (color >> 8) & 0xff;
const b = color & 0xff;
const max = Math.max(r, g, b);
if (max < 80) return "grey";
if (r > g + b && g < 100) return "red";
if (g > r + b && g > 150) return "green";
if (b > r + g) return "indigo";
if (r > 180 && g > 120 && b < 80) return "orange";
if (r > 200 && g > 180 && b < 120) return "yellow";
if (r > 120 && g < 80 && b > 120) return "purple";
if (r > 160 && g > 180 && b > 200) return "wathet";
if (g > 150 && r > 150 && b < 80) return "lime";
return "blue";
}
function mdText(content: string): { tag: "lark_md"; content: string } {
return { tag: "lark_md", content: cap(content, MAX_MARKDOWN) };
}
function divMarkdown(content: string): { tag: "div"; text: { tag: "lark_md"; content: string } } {
return { tag: "div", text: mdText(content) };
}
function formatTimestamp(ts?: string): string {
if (!ts) return "";
const d = new Date(ts);
if (Number.isNaN(d.getTime())) return ts;
const pad = (n: number): string => String(n).padStart(2, "0");
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
}
export function renderNeutralMessage(message: NeutralMessage): Record<string, unknown> {
const { head, subject } = splitMessageTitle(message.title);
const elements: Record<string, unknown>[] = [];
if (message.url) {
elements.push(divMarkdown(`**[${head}](${message.url})**`));
}
if (message.author?.name) {
const author = message.author.url
? `[${message.author.name}](${message.author.url})`
: message.author.name;
elements.push(divMarkdown(`**${author}**`));
}
if (subject || message.url || message.description) {
const parts: string[] = [];
if (subject) {
parts.push(`**${subject}**`);
} else if (message.url) {
parts.push(`**${message.title}**`);
}
if (message.description) {
parts.push(message.description);
}
if (parts.length) {
elements.push(divMarkdown(parts.join("\n\n")));
}
}
if (message.fields?.length) {
const lines = message.fields.map((f) => `**${f.name}**: ${f.value}`);
elements.push(divMarkdown(lines.join("\n\n")));
}
const meta: string[] = [];
if (message.forge?.name) {
meta.push(message.forge.url ? `[${message.forge.name}](${message.forge.url})` : message.forge.name);
}
if (message.footer) meta.push(message.footer);
const ts = formatTimestamp(message.timestamp);
if (ts) meta.push(ts);
if (meta.length) {
if (elements.length) elements.push({ tag: "hr" });
elements.push({
tag: "note",
elements: [{ tag: "lark_md", content: cap(meta.join(" · "), MAX_NOTE) }],
});
}
if (message.url && !message.actions?.length) {
elements.push({
tag: "action",
actions: [
{
tag: "button",
text: { tag: "plain_text", content: "Open" },
type: "primary",
multi_url: {
url: message.url,
pc_url: message.url,
android_url: message.url,
ios_url: message.url,
},
},
],
});
}
if (message.actions?.length) {
elements.push({
tag: "action",
actions: message.actions.map((action) => ({
tag: "button",
text: { tag: "plain_text", content: action.label },
type: action.style === "danger" ? "danger" : action.style === "primary" ? "primary" : "default",
action_id: action.id,
value: { v: action.id },
})),
});
}
return {
config: {
wide_screen_mode: true,
enable_forward: true,
update_multi: true,
},
header: {
title: {
tag: "plain_text",
content: cap(head, MAX_HEADER_TITLE),
},
template: colorToTemplate(message.color),
},
elements,
};
}