






















@@ -15,6 +15,8 @@ import { resolveFeishuSendTarget } from "./send-target.js";
1515import type { FeishuChatType, FeishuMessageInfo, FeishuSendResult } from "./types.js";
16161717const WITHDRAWN_REPLY_ERROR_CODES = new Set([230011, 231003]);
18+const INTERACTIVE_CARD_FALLBACK_TEXT = "[Interactive Card]";
19+const POST_FALLBACK_TEXT = "[Rich text message]";
1820const FEISHU_CARD_TEMPLATES = new Set([
1921"blue",
2022"green",
@@ -60,6 +62,10 @@ function isWithdrawnReplyError(err: unknown): boolean {
6062return false;
6163}
626465+function isRecord(value: unknown): value is Record<string, unknown> {
66+return Boolean(value && typeof value === "object" && !Array.isArray(value));
67+}
68+6369type FeishuCreateMessageClient = {
6470im: {
6571message: {
@@ -179,41 +185,121 @@ async function sendReplyOrFallbackDirect(
179185return toFeishuSendResult(response, params.directParams.receiveId);
180186}
181187182-function parseInteractiveCardContent(parsed: unknown): string {
183-if (!parsed || typeof parsed !== "object") {
184-return "[Interactive Card]";
188+function normalizeCardTemplateVariable(value: unknown): string | undefined {
189+if (typeof value === "string") {
190+return value;
185191}
192+if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
193+return String(value);
194+}
195+return undefined;
196+}
197+198+function readCardTemplateVariables(parsed: Record<string, unknown>): Map<string, string> {
199+const variables = new Map<string, string>();
200+for (const source of [parsed.template_variable, parsed.template_variables]) {
201+if (!isRecord(source)) {
202+continue;
203+}
204+for (const [key, value] of Object.entries(source)) {
205+const normalized = normalizeCardTemplateVariable(value);
206+if (normalized !== undefined) {
207+variables.set(key, normalized);
208+}
209+}
210+}
211+return variables;
212+}
186213187-// Support both schema 1.0 (top-level `elements`) and 2.0 (`body.elements`).
188-const candidate = parsed as { elements?: unknown; body?: { elements?: unknown } };
189-const elements = Array.isArray(candidate.elements)
190- ? candidate.elements
191- : Array.isArray(candidate.body?.elements)
192- ? candidate.body.elements
193- : null;
194-if (!elements) {
195-return "[Interactive Card]";
214+function applyCardTemplateVariables(text: string, variables: Map<string, string>): string {
215+if (variables.size === 0) {
216+return text;
196217}
218+return text.replace(/\$\{([A-Za-z0-9_.-]+)\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (match, a, b) => {
219+const variableName = typeof a === "string" ? a : b;
220+return variables.get(variableName) ?? match;
221+});
222+}
223+224+function extractInteractiveElementText(
225+element: unknown,
226+variables: Map<string, string>,
227+): string | undefined {
228+if (!isRecord(element)) {
229+return undefined;
230+}
231+const tag = typeof element.tag === "string" ? element.tag : "";
232+const text = isRecord(element.text) ? element.text : undefined;
233+234+if (tag === "div" && typeof text?.content === "string") {
235+return applyCardTemplateVariables(text.content, variables);
236+}
237+if ((tag === "markdown" || tag === "lark_md") && typeof element.content === "string") {
238+return applyCardTemplateVariables(element.content, variables);
239+}
240+if (tag === "plain_text" && typeof element.content === "string") {
241+return applyCardTemplateVariables(element.content, variables);
242+}
243+return undefined;
244+}
197245246+function extractInteractiveElementsText(
247+elements: unknown[],
248+variables: Map<string, string>,
249+): string {
198250const texts: string[] = [];
199251for (const element of elements) {
200-if (!element || typeof element !== "object") {
201-continue;
252+const text = extractInteractiveElementText(element, variables);
253+if (text !== undefined) {
254+texts.push(text);
202255}
203-const item = element as {
204-tag?: string;
205-content?: string;
206-text?: { content?: string };
207-};
208-if (item.tag === "div" && typeof item.text?.content === "string") {
209-texts.push(item.text.content);
256+}
257+return texts.join("\n").trim();
258+}
259+260+function readInteractiveElementArrays(parsed: Record<string, unknown>): unknown[][] {
261+const body = isRecord(parsed.body) ? parsed.body : undefined;
262+const elementArrays: unknown[][] = [];
263+264+for (const candidate of [parsed.elements, body?.elements]) {
265+if (Array.isArray(candidate)) {
266+elementArrays.push(candidate);
267+}
268+}
269+270+for (const candidate of [parsed.i18n_elements, body?.i18n_elements]) {
271+if (!isRecord(candidate)) {
210272continue;
211273}
212-if (item.tag === "markdown" && typeof item.content === "string") {
213-texts.push(item.content);
274+for (const localeElements of Object.values(candidate)) {
275+if (Array.isArray(localeElements)) {
276+elementArrays.push(localeElements);
277+}
278+}
279+}
280+281+return elementArrays;
282+}
283+284+function parseInteractivePostFallback(parsed: unknown): string | undefined {
285+const textContent = parsePostContent(JSON.stringify(parsed)).textContent.trim();
286+return textContent && textContent !== POST_FALLBACK_TEXT ? textContent : undefined;
287+}
288+289+function parseInteractiveCardContent(parsed: unknown): string {
290+if (!isRecord(parsed)) {
291+return INTERACTIVE_CARD_FALLBACK_TEXT;
292+}
293+294+const variables = readCardTemplateVariables(parsed);
295+for (const elements of readInteractiveElementArrays(parsed)) {
296+const text = extractInteractiveElementsText(elements, variables);
297+if (text) {
298+return text;
214299}
215300}
216-return texts.join("\n").trim() || "[Interactive Card]";
301+302+return parseInteractivePostFallback(parsed) ?? INTERACTIVE_CARD_FALLBACK_TEXT;
217303}
218304219305function parseFeishuMessageContent(rawContent: string, msgType: string): string {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。