mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
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:
parent
3437ac5513
commit
4b99f6d33d
38 changed files with 2449 additions and 1412 deletions
153
server/lib/drivers/feishu/render.ts
Normal file
153
server/lib/drivers/feishu/render.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue