feat(ringcentral): add Adaptive Cards support

- Add sendRingCentralAdaptiveCard, updateRingCentralAdaptiveCard, deleteRingCentralAdaptiveCard APIs
- Add RingCentralAdaptiveCard, RingCentralAdaptiveCardElement, RingCentralAdaptiveCardAction types
- Add hasCodeBlocks() and markdownToAdaptiveCard() helpers for code block formatting
- Add useAdaptiveCards config option (default: false)
- Add 6 new tests for Adaptive Cards functionality
This commit is contained in:
John Lin 2026-01-28 23:32:39 +08:00
parent c84cd60b6d
commit de00b3e335
No known key found for this signature in database
5 changed files with 226 additions and 1 deletions

View File

@ -6,6 +6,7 @@ import type {
RingCentralPost,
RingCentralUser,
RingCentralAttachment,
RingCentralAdaptiveCard,
} from "./types.js";
// Team Messaging API endpoints
@ -164,6 +165,60 @@ export async function downloadRingCentralAttachment(params: {
};
}
// Adaptive Cards API
export async function sendRingCentralAdaptiveCard(params: {
account: ResolvedRingCentralAccount;
chatId: string;
card: RingCentralAdaptiveCard;
fallbackText?: string;
}): Promise<{ cardId?: string } | null> {
const { account, chatId, card, fallbackText } = params;
const platform = await getRingCentralPlatform(account);
const body = {
...card,
type: "AdaptiveCard",
$schema: card.$schema ?? "http://adaptivecards.io/schemas/adaptive-card.json",
version: card.version ?? "1.3",
...(fallbackText ? { fallbackText } : {}),
};
const response = await platform.post(`${TM_API_BASE}/chats/${chatId}/adaptive-cards`, body);
const result = (await response.json()) as { id?: string };
return result ? { cardId: result.id } : null;
}
export async function updateRingCentralAdaptiveCard(params: {
account: ResolvedRingCentralAccount;
cardId: string;
card: RingCentralAdaptiveCard;
fallbackText?: string;
}): Promise<{ cardId?: string }> {
const { account, cardId, card, fallbackText } = params;
const platform = await getRingCentralPlatform(account);
const body = {
...card,
type: "AdaptiveCard",
$schema: card.$schema ?? "http://adaptivecards.io/schemas/adaptive-card.json",
version: card.version ?? "1.3",
...(fallbackText ? { fallbackText } : {}),
};
const response = await platform.put(`${TM_API_BASE}/adaptive-cards/${cardId}`, body);
const result = (await response.json()) as { id?: string };
return { cardId: result.id };
}
export async function deleteRingCentralAdaptiveCard(params: {
account: ResolvedRingCentralAccount;
cardId: string;
}): Promise<void> {
const { account, cardId } = params;
const platform = await getRingCentralPlatform(account);
await platform.delete(`${TM_API_BASE}/adaptive-cards/${cardId}`);
}
export async function probeRingCentral(
account: ResolvedRingCentralAccount,
): Promise<{ ok: boolean; error?: string; elapsedMs: number }> {

View File

@ -42,6 +42,7 @@ const RingCentralAccountSchemaBase = z
allowBots: z.boolean().optional(),
botExtensionId: z.string().optional(),
selfOnly: z.boolean().optional(),
useAdaptiveCards: z.boolean().optional(), // Use Adaptive Cards for code blocks (default: false)
})
.strict();

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { toRingCentralMarkdown, needsMarkdownConversion } from "./markdown.js";
import { toRingCentralMarkdown, needsMarkdownConversion, hasCodeBlocks, markdownToAdaptiveCard } from "./markdown.js";
describe("toRingCentralMarkdown", () => {
it("converts single underscore italic to asterisk", () => {
@ -119,3 +119,50 @@ describe("needsMarkdownConversion", () => {
expect(needsMarkdownConversion("*italic* and **bold**")).toBe(false);
});
});
describe("hasCodeBlocks", () => {
it("returns true for text with code blocks", () => {
expect(hasCodeBlocks("```\ncode\n```")).toBe(true);
expect(hasCodeBlocks("```js\nconst x = 1;\n```")).toBe(true);
});
it("returns false for text without code blocks", () => {
expect(hasCodeBlocks("plain text")).toBe(false);
expect(hasCodeBlocks("`inline code`")).toBe(false);
});
});
describe("markdownToAdaptiveCard", () => {
it("converts simple text to adaptive card", () => {
const result = markdownToAdaptiveCard("Hello world");
expect(result.type).toBe("AdaptiveCard");
expect(result.version).toBe("1.3");
expect(result.body).toHaveLength(1);
expect(result.body[0].type).toBe("TextBlock");
expect(result.body[0].text).toBe("Hello world");
});
it("converts code blocks with monospace font", () => {
const result = markdownToAdaptiveCard("```js\nconst x = 1;\n```");
expect(result.body).toHaveLength(1);
expect(result.body[0].fontType).toBe("Monospace");
expect(result.body[0].text).toBe("const x = 1;");
});
it("handles mixed text and code blocks", () => {
const result = markdownToAdaptiveCard("Some text\n\n```\ncode\n```\n\nMore text");
expect(result.body).toHaveLength(3);
expect(result.body[0].text).toBe("Some text");
expect(result.body[1].fontType).toBe("Monospace");
expect(result.body[1].text).toBe("code");
expect(result.body[2].text).toBe("More text");
});
it("converts headings with bold styling", () => {
const result = markdownToAdaptiveCard("# Title");
expect(result.body).toHaveLength(1);
expect(result.body[0].weight).toBe("Bolder");
expect(result.body[0].size).toBe("Medium");
expect(result.body[0].text).toBe("Title");
});
});

View File

@ -85,3 +85,74 @@ export function needsMarkdownConversion(text: string): boolean {
/^#{1,6}\s+.+$/m.test(text)
);
}
/**
* Check if text contains code blocks that would benefit from Adaptive Card formatting
*/
export function hasCodeBlocks(text: string): boolean {
return /```[\s\S]*?```/.test(text);
}
/**
* Convert markdown text to an Adaptive Card structure.
* This preserves code blocks with proper formatting.
*/
export function markdownToAdaptiveCard(text: string): {
type: "AdaptiveCard";
$schema: string;
version: string;
body: Array<{ type: string; text?: string; wrap?: boolean; fontType?: string; size?: string; weight?: string }>;
} {
const body: Array<{ type: string; text?: string; wrap?: boolean; fontType?: string; size?: string; weight?: string }> = [];
// Split by code blocks
const parts = text.split(/(```[\s\S]*?```)/g);
for (const part of parts) {
if (!part.trim()) continue;
if (part.startsWith("```")) {
// Extract language and code
const match = part.match(/```(\w*)\n?([\s\S]*?)\n?```/);
if (match) {
const [, , code] = match;
// Add code block with monospace font
body.push({
type: "TextBlock",
text: code.trim(),
wrap: true,
fontType: "Monospace",
});
}
} else {
// Regular text - convert markdown
const converted = toRingCentralMarkdown(part.trim());
if (converted) {
// Check if it's a heading (starts with **)
const headingMatch = converted.match(/^\*\*(.+)\*\*$/);
if (headingMatch && !converted.includes("\n")) {
body.push({
type: "TextBlock",
text: headingMatch[1],
wrap: true,
size: "Medium",
weight: "Bolder",
});
} else {
body.push({
type: "TextBlock",
text: converted,
wrap: true,
});
}
}
}
}
return {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.3",
body,
};
}

View File

@ -53,6 +53,56 @@ export type RingCentralMention = {
name?: string;
};
// Adaptive Cards types (Microsoft Adaptive Card schema v1.3)
export type RingCentralAdaptiveCardElement = {
type: string;
text?: string;
size?: "Small" | "Default" | "Medium" | "Large" | "ExtraLarge";
weight?: "Lighter" | "Default" | "Bolder";
color?: "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention";
wrap?: boolean;
url?: string;
altText?: string;
columns?: RingCentralAdaptiveCardElement[];
items?: RingCentralAdaptiveCardElement[];
width?: string | number;
style?: string;
isVisible?: boolean;
id?: string;
// Input elements
label?: string;
placeholder?: string;
value?: string;
isRequired?: boolean;
// Action elements
title?: string;
data?: Record<string, unknown>;
[key: string]: unknown;
};
export type RingCentralAdaptiveCardAction = {
type: "Action.Submit" | "Action.OpenUrl" | "Action.ShowCard" | "Action.ToggleVisibility";
title?: string;
url?: string;
data?: Record<string, unknown>;
card?: RingCentralAdaptiveCard;
targetElements?: Array<string | { elementId: string; isVisible?: boolean }>;
};
export type RingCentralAdaptiveCard = {
type?: "AdaptiveCard";
$schema?: string;
version?: string;
body?: RingCentralAdaptiveCardElement[];
actions?: RingCentralAdaptiveCardAction[];
fallbackText?: string;
speak?: string;
lang?: string;
verticalContentAlignment?: "Top" | "Center" | "Bottom";
backgroundImage?: string | { url: string; fillMode?: string };
minHeight?: string;
};
export type RingCentralWebhookEvent = {
uuid?: string;
event?: string;
@ -113,6 +163,7 @@ export type RingCentralAccountConfig = {
botExtensionId?: string;
replyToMode?: "off" | "all";
selfOnly?: boolean; // JWT mode: only accept messages from the JWT user in Personal chat (default: true)
useAdaptiveCards?: boolean; // Use Adaptive Cards for messages with code blocks (default: false)
};
export type RingCentralConfig = RingCentralAccountConfig & {