feat(feishu): support optional header in streaming cards (openclaw#22826)

Add an optional `header` parameter to `FeishuStreamingSession.start()`
so that streaming cards can display a colored title bar, matching the
appearance of non-streaming interactive cards.

The Card Kit API already supports `header` alongside `streaming_mode`,
but the current implementation omits it, producing headerless cards.

This change is fully backward-compatible: when `header` is not provided,
behavior is identical to before.

Closes #13267 (partial)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
songlei 2026-02-28 13:21:22 +08:00 committed by GitHub
parent 0a23739c37
commit 8a2273e210
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 20 additions and 2 deletions

View File

@ -9,6 +9,13 @@ import type { FeishuDomain } from "./types.js";
type Credentials = { appId: string; appSecret: string; domain?: FeishuDomain };
type CardState = { cardId: string; messageId: string; sequence: number; currentText: string };
/** Optional header for streaming cards (title bar with color template) */
export type StreamingCardHeader = {
title: string;
/** Color template: blue, green, red, orange, purple, indigo, wathet, turquoise, yellow, grey, carmine, violet, lime */
template?: string;
};
// Token cache (keyed by domain + appId)
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
@ -99,14 +106,19 @@ export class FeishuStreamingSession {
async start(
receiveId: string,
receiveIdType: "open_id" | "user_id" | "union_id" | "email" | "chat_id" = "chat_id",
options?: { replyToMessageId?: string; replyInThread?: boolean; rootId?: string },
options?: {
replyToMessageId?: string;
replyInThread?: boolean;
rootId?: string;
header?: StreamingCardHeader;
},
): Promise<void> {
if (this.state) {
return;
}
const apiBase = resolveApiBase(this.creds.domain);
const cardJson = {
const cardJson: Record<string, unknown> = {
schema: "2.0",
config: {
streaming_mode: true,
@ -117,6 +129,12 @@ export class FeishuStreamingSession {
elements: [{ tag: "markdown", content: "⏳ Thinking...", element_id: "content" }],
},
};
if (options?.header) {
cardJson.header = {
title: { tag: "plain_text", content: options.header.title },
template: options.header.template ?? "blue",
};
}
// Create card entity
const { response: createRes, release: releaseCreate } = await fetchWithSsrFGuard({