@milerliu/feishu 0.1.16 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/send.ts +129 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milerliu/feishu",
3
- "version": "0.1.16",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Feishu/Lark channel plugin",
6
6
  "license": "MIT",
package/src/send.ts CHANGED
@@ -18,8 +18,8 @@ export type FeishuMessageInfo = {
18
18
 
19
19
  /**
20
20
  * Send text with streaming effect using Feishu streaming update card.
21
- * This creates an initial card and then streams updates to it.
22
21
  * Uses the official Feishu streaming update card API.
22
+ * Reference: https://open.feishu.cn/document/cardkit-v1/streaming-updates-openapi-overview
23
23
  */
24
24
  export async function sendStreamingMessageFeishu(params: {
25
25
  cfg: ClawdbotConfig;
@@ -48,34 +48,76 @@ export async function sendStreamingMessageFeishu(params: {
48
48
  rawText = buildMentionedMessage(mentions, rawText);
49
49
  }
50
50
 
51
- // Create interactive card with streaming_update enabled
52
- const card = {
53
- header: {
54
- title: {
55
- tag: "plain_text",
56
- content: "AI 回复",
51
+ // Create streaming card according to official docs
52
+ // Reference: https://open.feishu.cn/document/cardkit-v1/streaming-updates-openapi-overview
53
+ const streamingCard = {
54
+ schema: "2.0",
55
+ config: {
56
+ streaming_mode: true,
57
+ streaming_config: {
58
+ print_frequency_ms: {
59
+ default: 30,
60
+ },
61
+ print_step: {
62
+ default: 2,
63
+ },
64
+ print_strategy: "fast",
65
+ },
66
+ summary: {
67
+ content: "AI 正在生成内容...",
57
68
  },
58
69
  },
59
- config: {
60
- wide_screen_mode: true,
61
- streaming_update: true,
70
+ body: {
71
+ elements: [
72
+ {
73
+ tag: "markdown",
74
+ content: "",
75
+ element_id: "ai_response",
76
+ },
77
+ ],
62
78
  },
63
- elements: [
64
- {
65
- tag: "markdown",
66
- content: rawText,
67
- },
68
- ],
69
79
  };
70
80
 
71
- const cardContent = JSON.stringify(card);
81
+ const cardContent = JSON.stringify(streamingCard);
82
+
83
+ // Step 1: Create card instance
84
+ let cardResponse;
85
+ try {
86
+ cardResponse = await (client as any).cardkit.v1.card.create({
87
+ data: {
88
+ type: "adaptive",
89
+ data: cardContent,
90
+ },
91
+ });
92
+ } catch (error) {
93
+ console.error("Failed to create streaming card:", error);
94
+ // Fallback to regular card
95
+ const fallbackCard = buildMarkdownCard(rawText);
96
+ return sendCardFeishu({ cfg, to, card: fallbackCard });
97
+ }
98
+
99
+ if (cardResponse.code !== 0) {
100
+ console.error(`Card creation failed: ${cardResponse.msg}`);
101
+ // Fallback to regular card
102
+ const fallbackCard = buildMarkdownCard(rawText);
103
+ return sendCardFeishu({ cfg, to, card: fallbackCard });
104
+ }
105
+
106
+ const cardId = cardResponse.data?.card_id;
107
+ if (!cardId) {
108
+ console.error("No card_id returned");
109
+ // Fallback to regular card
110
+ const fallbackCard = buildMarkdownCard(rawText);
111
+ return sendCardFeishu({ cfg, to, card: fallbackCard });
112
+ }
72
113
 
73
- // Create the card message
114
+ // Step 2: Send card to chat
115
+ const messageContent = JSON.stringify({ card_id: cardId });
74
116
  const messageResponse = await client.im.message.create({
75
117
  params: { receive_id_type: receiveIdType },
76
118
  data: {
77
119
  receive_id: receiveId,
78
- content: cardContent,
120
+ content: messageContent,
79
121
  msg_type: "interactive",
80
122
  },
81
123
  });
@@ -86,17 +128,56 @@ export async function sendStreamingMessageFeishu(params: {
86
128
 
87
129
  const messageId = messageResponse.data?.message_id ?? "unknown";
88
130
 
89
- // Now update the message multiple times for streaming effect
90
- // Use im.v1.message.update API
91
- const chunkSize = 500;
131
+ // Step 3: Stream updates using batch_update
132
+ // Split text into chunks for streaming effect
133
+ const chunkSize = 10; // Small chunks for streaming effect
92
134
  const chunks: string[] = [];
93
135
  for (let i = 0; i < rawText.length; i += chunkSize) {
94
136
  chunks.push(rawText.slice(i, i + chunkSize));
95
137
  }
96
138
 
97
- // Send updates with delay
139
+ let cumulativeContent = "";
140
+ let sequence = 0;
141
+
98
142
  for (let i = 0; i < chunks.length; i++) {
99
- const updateCard = {
143
+ cumulativeContent += chunks[i];
144
+ sequence = i;
145
+
146
+ const actions = JSON.stringify([
147
+ {
148
+ action_name: "update_content",
149
+ action_param: {
150
+ tag: "markdown",
151
+ content: cumulativeContent,
152
+ element_id: "ai_response",
153
+ },
154
+ },
155
+ ]);
156
+
157
+ try {
158
+ await (client as any).cardkit.v1.card.batch_update({
159
+ path: { card_id: cardId },
160
+ data: {
161
+ uuid: `stream-${Date.now()}-${i}`,
162
+ sequence,
163
+ actions,
164
+ },
165
+ });
166
+
167
+ // Small delay between updates for streaming effect
168
+ if (i < chunks.length - 1) {
169
+ await new Promise((resolve) => setTimeout(resolve, 50));
170
+ }
171
+ } catch (error) {
172
+ console.error(`Streaming update failed at chunk ${i}: ${error}`);
173
+ }
174
+ }
175
+
176
+ return {
177
+ messageId,
178
+ chatId: receiveId,
179
+ };
180
+ }
100
181
  header: {
101
182
  title: {
102
183
  tag: "plain_text",
@@ -309,19 +390,38 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
309
390
  }
310
391
 
311
392
  const receiveIdType = resolveReceiveIdType(receiveId);
312
- const content = JSON.stringify(card);
393
+
394
+ // Extract content from card for post message
395
+ const markdownContent = card.elements?.[0]?.content || "";
396
+
397
+ // Build post-type rich text message
398
+ const postContent = {
399
+ zh_cn: {
400
+ title: "AI 回复",
401
+ content: [
402
+ [
403
+ {
404
+ tag: "text",
405
+ text: markdownContent,
406
+ },
407
+ ],
408
+ ],
409
+ },
410
+ };
411
+
412
+ const content = JSON.stringify(postContent);
313
413
 
314
414
  if (replyToMessageId) {
315
415
  const response = await client.im.message.reply({
316
416
  path: { message_id: replyToMessageId },
317
417
  data: {
318
418
  content,
319
- msg_type: "interactive",
419
+ msg_type: "post",
320
420
  },
321
421
  });
322
422
 
323
423
  if (response.code !== 0) {
324
- throw new Error(`Feishu card reply failed: ${response.msg || `code ${response.code}`}`);
424
+ throw new Error(`Feishu post reply failed: ${response.msg || `code ${response.code}`}`);
325
425
  }
326
426
 
327
427
  return {
@@ -335,12 +435,12 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
335
435
  data: {
336
436
  receive_id: receiveId,
337
437
  content,
338
- msg_type: "interactive",
438
+ msg_type: "post",
339
439
  },
340
440
  });
341
441
 
342
442
  if (response.code !== 0) {
343
- throw new Error(`Feishu card send failed: ${response.msg || `code ${response.code}`}`);
443
+ throw new Error(`Feishu post send failed: ${response.msg || `code ${response.code}`}`);
344
444
  }
345
445
 
346
446
  return {