openclaw-home-pc/openclaw/extensions/openclaw-lark/src/card/cardkit.js
2026-03-21 15:31:06 +08:00

183 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* CardKit streaming APIs for Lark/Feishu.
*/
import { LarkClient } from '../core/lark-client';
import { larkLogger } from '../core/lark-logger';
import { normalizeFeishuTarget, normalizeMessageId, resolveReceiveIdType } from '../core/targets';
import { runWithMessageUnavailableGuard } from '../core/message-unavailable';
const log = larkLogger('card/cardkit');
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* 记录 CardKit API 响应日志,检测错误码并抛出异常。
*
* 默认 fail-fastbody-level 非零 code 视为业务错误,立即抛出,
* 由调用方streaming-card-controller 等)统一走 catch → guard 处理。
*/
function logCardKitResponse(params) {
const { resp, api, context } = params;
const { code, msg } = resp;
log.info(`cardkit ${api} response`, { code, msg, context });
if (code && code !== 0) {
log.warn(`cardkit ${api} FAILED`, { code, msg, context, fullResponse: resp });
throw new Error(`cardkit ${api} FAILED: code=${code}, msg=${msg ?? ''}, ${context}`);
}
}
// ---------------------------------------------------------------------------
// CardKit streaming APIs
// ---------------------------------------------------------------------------
/**
* Create a card entity via the CardKit API.
*
* Returns the card_id directly, bypassing the idConvert step.
* The card can then be sent via IM API and streamed via CardKit.
*/
export async function createCardEntity(params) {
const { cfg, card, accountId } = params;
const client = LarkClient.fromCfg(cfg, accountId).sdk;
// SDK 返回类型不完整,运行时包含 code/msg/data 字段
const response = (await client.cardkit.v1.card.create({
data: {
type: 'card_json',
data: JSON.stringify(card),
},
}));
// 兼容不同 SDK 包装层:优先 data.card_id回退顶层 card_id
const cardId = (response.data?.card_id ?? response.card_id) ?? null;
logCardKitResponse({ resp: response, api: 'card.create', context: `cardId=${cardId}` });
return cardId;
}
/**
* Stream text content to a specific card element using the CardKit API.
*
* The card automatically diffs the new content against the previous
* content and renders incremental changes with a typewriter animation.
*
* @param params.cardId - CardKit card ID (from `convertMessageToCardId`).
* @param params.elementId - The element ID to update (e.g. `STREAMING_ELEMENT_ID`).
* @param params.content - The full cumulative text (not a delta).
* @param params.sequence - Monotonically increasing sequence number.
*/
export async function streamCardContent(params) {
const { cfg, cardId, elementId, content, sequence, accountId } = params;
const client = LarkClient.fromCfg(cfg, accountId).sdk;
// SDK 返回类型不完整,运行时包含 code/msg 字段
const resp = (await client.cardkit.v1.cardElement.content({
data: { content, sequence },
path: { card_id: cardId, element_id: elementId },
}));
logCardKitResponse({
resp,
api: 'cardElement.content',
context: `seq=${sequence}, contentLen=${content.length}`,
});
}
/**
* Fully replace a card using the CardKit API.
*
* Used for the final "complete" state update (with action buttons, green
* header, etc.) after streaming finishes.
*
* @param params.cardId - CardKit card ID.
* @param params.card - The new card JSON content.
* @param params.sequence - Monotonically increasing sequence number.
*/
export async function updateCardKitCard(params) {
const { cfg, cardId, card, sequence, accountId } = params;
const client = LarkClient.fromCfg(cfg, accountId).sdk;
// SDK 返回类型不完整,运行时包含 code/msg 字段
const resp = (await client.cardkit.v1.card.update({
data: {
card: { type: 'card_json', data: JSON.stringify(card) },
sequence,
},
path: { card_id: cardId },
}));
logCardKitResponse({
resp,
api: 'card.update',
context: `seq=${sequence}, cardId=${cardId}`,
});
}
export async function updateCardKitCardForAuth(params) {
return updateCardKitCard(params);
}
/**
* Send an interactive card message by referencing a CardKit card_id.
*
* The content format is: {"type":"card","data":{"card_id":"xxx"}}
* This links the IM message to the CardKit card entity, enabling
* streaming updates via cardElement.content().
*/
export async function sendCardByCardId(params) {
const { cfg, to, cardId, replyToMessageId, replyInThread, accountId } = params;
const client = LarkClient.fromCfg(cfg, accountId).sdk;
const contentPayload = JSON.stringify({
type: 'card',
data: { card_id: cardId },
});
if (replyToMessageId) {
// 规范化 message_id处理合成 ID如 "om_xxx:auth-complete"
const normalizedId = normalizeMessageId(replyToMessageId);
const response = await runWithMessageUnavailableGuard({
messageId: normalizedId,
operation: 'im.message.reply(interactive.cardkit)',
fn: () => client.im.message.reply({
path: { message_id: normalizedId },
data: { content: contentPayload, msg_type: 'interactive', reply_in_thread: replyInThread },
}),
});
return {
messageId: response?.data?.message_id ?? '',
chatId: response?.data?.chat_id ?? '',
};
}
const target = normalizeFeishuTarget(to);
if (!target) {
throw new Error(`[feishu-send] Invalid target: "${to}"`);
}
const receiveIdType = resolveReceiveIdType(target);
const response = await client.im.message.create({
// SDK 类型将 receive_id_type 限定为字面量联合,但运行时接受动态值
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params: { receive_id_type: receiveIdType },
data: {
receive_id: target,
msg_type: 'interactive',
content: contentPayload,
},
});
return {
messageId: response?.data?.message_id ?? '',
chatId: response?.data?.chat_id ?? '',
};
}
/**
* Close (or open) the streaming mode on a CardKit card.
*
* Must be called after streaming is complete to restore normal card
* behaviour (forwarding, interaction callbacks, etc.).
*/
export async function setCardStreamingMode(params) {
const { cfg, cardId, streamingMode, sequence, accountId } = params;
const client = LarkClient.fromCfg(cfg, accountId).sdk;
// SDK 返回类型不完整,运行时包含 code/msg 字段
const resp = (await client.cardkit.v1.card.settings({
data: {
settings: JSON.stringify({ streaming_mode: streamingMode }),
sequence,
},
path: { card_id: cardId },
}));
logCardKitResponse({
resp,
api: 'card.settings',
context: `seq=${sequence}, streaming_mode=${streamingMode}`,
});
}