@openclaw/feishu 2026.3.2 → 2026.3.7

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 (70) hide show
  1. package/index.ts +2 -2
  2. package/package.json +1 -1
  3. package/src/accounts.test.ts +199 -13
  4. package/src/accounts.ts +45 -17
  5. package/src/bitable.ts +40 -28
  6. package/src/bot.checkBotMentioned.test.ts +8 -0
  7. package/src/bot.stripBotMention.test.ts +118 -22
  8. package/src/bot.test.ts +516 -9
  9. package/src/bot.ts +366 -109
  10. package/src/card-action.ts +1 -1
  11. package/src/channel.test.ts +1 -1
  12. package/src/channel.ts +52 -64
  13. package/src/chat.test.ts +2 -2
  14. package/src/chat.ts +1 -1
  15. package/src/client.test.ts +207 -4
  16. package/src/client.ts +70 -5
  17. package/src/config-schema.test.ts +14 -6
  18. package/src/config-schema.ts +5 -1
  19. package/src/dedup.ts +1 -1
  20. package/src/directory.test.ts +40 -0
  21. package/src/directory.ts +29 -50
  22. package/src/docx-batch-insert.test.ts +90 -0
  23. package/src/docx-batch-insert.ts +8 -11
  24. package/src/docx.account-selection.test.ts +3 -3
  25. package/src/docx.ts +1 -1
  26. package/src/drive.ts +13 -17
  27. package/src/dynamic-agent.ts +1 -1
  28. package/src/feishu-command-handler.ts +59 -0
  29. package/src/media.test.ts +60 -13
  30. package/src/media.ts +23 -9
  31. package/src/monitor.account.ts +19 -8
  32. package/src/monitor.reaction.test.ts +111 -105
  33. package/src/monitor.startup.test.ts +11 -10
  34. package/src/monitor.startup.ts +20 -7
  35. package/src/monitor.state.ts +4 -1
  36. package/src/monitor.test-mocks.ts +42 -9
  37. package/src/monitor.transport.ts +4 -1
  38. package/src/monitor.ts +4 -4
  39. package/src/monitor.webhook-security.test.ts +8 -23
  40. package/src/onboarding.status.test.ts +1 -1
  41. package/src/onboarding.test.ts +143 -0
  42. package/src/onboarding.ts +86 -71
  43. package/src/outbound.test.ts +178 -0
  44. package/src/outbound.ts +39 -6
  45. package/src/perm.ts +11 -15
  46. package/src/policy.test.ts +40 -0
  47. package/src/policy.ts +9 -10
  48. package/src/probe.test.ts +18 -18
  49. package/src/reactions.ts +1 -1
  50. package/src/reply-dispatcher.test.ts +175 -0
  51. package/src/reply-dispatcher.ts +69 -21
  52. package/src/runtime.ts +1 -1
  53. package/src/secret-input.ts +8 -14
  54. package/src/send-message.ts +71 -0
  55. package/src/send-target.test.ts +1 -1
  56. package/src/send-target.ts +1 -1
  57. package/src/send.reply-fallback.test.ts +74 -0
  58. package/src/send.test.ts +1 -1
  59. package/src/send.ts +88 -49
  60. package/src/streaming-card.test.ts +54 -0
  61. package/src/streaming-card.ts +96 -28
  62. package/src/targets.ts +5 -1
  63. package/src/tool-account-routing.test.ts +3 -3
  64. package/src/tool-account.ts +1 -1
  65. package/src/tool-factory-test-harness.ts +1 -1
  66. package/src/tool-result.test.ts +32 -0
  67. package/src/tool-result.ts +14 -0
  68. package/src/types.ts +2 -3
  69. package/src/typing.ts +1 -1
  70. package/src/wiki.ts +15 -19
package/src/send.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
2
2
  import { resolveFeishuAccount } from "./accounts.js";
3
3
  import { createFeishuClient } from "./client.js";
4
4
  import type { MentionTarget } from "./mention.js";
@@ -19,6 +19,61 @@ function shouldFallbackFromReplyTarget(response: { code?: number; msg?: string }
19
19
  return msg.includes("withdrawn") || msg.includes("not found");
20
20
  }
21
21
 
22
+ /** Check whether a thrown error indicates a withdrawn/not-found reply target. */
23
+ function isWithdrawnReplyError(err: unknown): boolean {
24
+ if (typeof err !== "object" || err === null) {
25
+ return false;
26
+ }
27
+ // SDK error shape: err.code
28
+ const code = (err as { code?: number }).code;
29
+ if (typeof code === "number" && WITHDRAWN_REPLY_ERROR_CODES.has(code)) {
30
+ return true;
31
+ }
32
+ // AxiosError shape: err.response.data.code
33
+ const response = (err as { response?: { data?: { code?: number; msg?: string } } }).response;
34
+ if (
35
+ typeof response?.data?.code === "number" &&
36
+ WITHDRAWN_REPLY_ERROR_CODES.has(response.data.code)
37
+ ) {
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ type FeishuCreateMessageClient = {
44
+ im: {
45
+ message: {
46
+ create: (opts: {
47
+ params: { receive_id_type: "chat_id" | "email" | "open_id" | "union_id" | "user_id" };
48
+ data: { receive_id: string; content: string; msg_type: string };
49
+ }) => Promise<{ code?: number; msg?: string; data?: { message_id?: string } }>;
50
+ };
51
+ };
52
+ };
53
+
54
+ /** Send a direct message as a fallback when a reply target is unavailable. */
55
+ async function sendFallbackDirect(
56
+ client: FeishuCreateMessageClient,
57
+ params: {
58
+ receiveId: string;
59
+ receiveIdType: "chat_id" | "email" | "open_id" | "union_id" | "user_id";
60
+ content: string;
61
+ msgType: string;
62
+ },
63
+ errorPrefix: string,
64
+ ): Promise<FeishuSendResult> {
65
+ const response = await client.im.message.create({
66
+ params: { receive_id_type: params.receiveIdType },
67
+ data: {
68
+ receive_id: params.receiveId,
69
+ content: params.content,
70
+ msg_type: params.msgType,
71
+ },
72
+ });
73
+ assertFeishuMessageApiSuccess(response, errorPrefix);
74
+ return toFeishuSendResult(response, params.receiveId);
75
+ }
76
+
22
77
  export type FeishuMessageInfo = {
23
78
  messageId: string;
24
79
  chatId: string;
@@ -239,41 +294,33 @@ export async function sendMessageFeishu(
239
294
 
240
295
  const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
241
296
 
297
+ const directParams = { receiveId, receiveIdType, content, msgType };
298
+
242
299
  if (replyToMessageId) {
243
- const response = await client.im.message.reply({
244
- path: { message_id: replyToMessageId },
245
- data: {
246
- content,
247
- msg_type: msgType,
248
- ...(replyInThread ? { reply_in_thread: true } : {}),
249
- },
250
- });
251
- if (shouldFallbackFromReplyTarget(response)) {
252
- const fallback = await client.im.message.create({
253
- params: { receive_id_type: receiveIdType },
300
+ let response: { code?: number; msg?: string; data?: { message_id?: string } };
301
+ try {
302
+ response = await client.im.message.reply({
303
+ path: { message_id: replyToMessageId },
254
304
  data: {
255
- receive_id: receiveId,
256
305
  content,
257
306
  msg_type: msgType,
307
+ ...(replyInThread ? { reply_in_thread: true } : {}),
258
308
  },
259
309
  });
260
- assertFeishuMessageApiSuccess(fallback, "Feishu send failed");
261
- return toFeishuSendResult(fallback, receiveId);
310
+ } catch (err) {
311
+ if (!isWithdrawnReplyError(err)) {
312
+ throw err;
313
+ }
314
+ return sendFallbackDirect(client, directParams, "Feishu send failed");
315
+ }
316
+ if (shouldFallbackFromReplyTarget(response)) {
317
+ return sendFallbackDirect(client, directParams, "Feishu send failed");
262
318
  }
263
319
  assertFeishuMessageApiSuccess(response, "Feishu reply failed");
264
320
  return toFeishuSendResult(response, receiveId);
265
321
  }
266
322
 
267
- const response = await client.im.message.create({
268
- params: { receive_id_type: receiveIdType },
269
- data: {
270
- receive_id: receiveId,
271
- content,
272
- msg_type: msgType,
273
- },
274
- });
275
- assertFeishuMessageApiSuccess(response, "Feishu send failed");
276
- return toFeishuSendResult(response, receiveId);
323
+ return sendFallbackDirect(client, directParams, "Feishu send failed");
277
324
  }
278
325
 
279
326
  export type SendFeishuCardParams = {
@@ -291,41 +338,33 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
291
338
  const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId });
292
339
  const content = JSON.stringify(card);
293
340
 
341
+ const directParams = { receiveId, receiveIdType, content, msgType: "interactive" };
342
+
294
343
  if (replyToMessageId) {
295
- const response = await client.im.message.reply({
296
- path: { message_id: replyToMessageId },
297
- data: {
298
- content,
299
- msg_type: "interactive",
300
- ...(replyInThread ? { reply_in_thread: true } : {}),
301
- },
302
- });
303
- if (shouldFallbackFromReplyTarget(response)) {
304
- const fallback = await client.im.message.create({
305
- params: { receive_id_type: receiveIdType },
344
+ let response: { code?: number; msg?: string; data?: { message_id?: string } };
345
+ try {
346
+ response = await client.im.message.reply({
347
+ path: { message_id: replyToMessageId },
306
348
  data: {
307
- receive_id: receiveId,
308
349
  content,
309
350
  msg_type: "interactive",
351
+ ...(replyInThread ? { reply_in_thread: true } : {}),
310
352
  },
311
353
  });
312
- assertFeishuMessageApiSuccess(fallback, "Feishu card send failed");
313
- return toFeishuSendResult(fallback, receiveId);
354
+ } catch (err) {
355
+ if (!isWithdrawnReplyError(err)) {
356
+ throw err;
357
+ }
358
+ return sendFallbackDirect(client, directParams, "Feishu card send failed");
359
+ }
360
+ if (shouldFallbackFromReplyTarget(response)) {
361
+ return sendFallbackDirect(client, directParams, "Feishu card send failed");
314
362
  }
315
363
  assertFeishuMessageApiSuccess(response, "Feishu card reply failed");
316
364
  return toFeishuSendResult(response, receiveId);
317
365
  }
318
366
 
319
- const response = await client.im.message.create({
320
- params: { receive_id_type: receiveIdType },
321
- data: {
322
- receive_id: receiveId,
323
- content,
324
- msg_type: "interactive",
325
- },
326
- });
327
- assertFeishuMessageApiSuccess(response, "Feishu card send failed");
328
- return toFeishuSendResult(response, receiveId);
367
+ return sendFallbackDirect(client, directParams, "Feishu card send failed");
329
368
  }
330
369
 
331
370
  export async function updateCardFeishu(params: {
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mergeStreamingText, resolveStreamingCardSendMode } from "./streaming-card.js";
3
+
4
+ describe("mergeStreamingText", () => {
5
+ it("prefers the latest full text when it already includes prior text", () => {
6
+ expect(mergeStreamingText("hello", "hello world")).toBe("hello world");
7
+ });
8
+
9
+ it("keeps previous text when the next partial is empty or redundant", () => {
10
+ expect(mergeStreamingText("hello", "")).toBe("hello");
11
+ expect(mergeStreamingText("hello world", "hello")).toBe("hello world");
12
+ });
13
+
14
+ it("appends fragmented chunks without injecting newlines", () => {
15
+ expect(mergeStreamingText("hello wor", "ld")).toBe("hello world");
16
+ expect(mergeStreamingText("line1", "line2")).toBe("line1line2");
17
+ });
18
+
19
+ it("merges overlap between adjacent partial snapshots", () => {
20
+ expect(mergeStreamingText("好的,让我", "让我再读取一遍")).toBe("好的,让我再读取一遍");
21
+ expect(mergeStreamingText("revision_id: 552", "2,一点变化都没有")).toBe(
22
+ "revision_id: 552,一点变化都没有",
23
+ );
24
+ expect(mergeStreamingText("abc", "cabc")).toBe("cabc");
25
+ });
26
+ });
27
+
28
+ describe("resolveStreamingCardSendMode", () => {
29
+ it("prefers message.reply when reply target and root id both exist", () => {
30
+ expect(
31
+ resolveStreamingCardSendMode({
32
+ replyToMessageId: "om_parent",
33
+ rootId: "om_topic_root",
34
+ }),
35
+ ).toBe("reply");
36
+ });
37
+
38
+ it("falls back to root create when reply target is absent", () => {
39
+ expect(
40
+ resolveStreamingCardSendMode({
41
+ rootId: "om_topic_root",
42
+ }),
43
+ ).toBe("root_create");
44
+ });
45
+
46
+ it("uses create mode when no reply routing fields are provided", () => {
47
+ expect(resolveStreamingCardSendMode()).toBe("create");
48
+ expect(
49
+ resolveStreamingCardSendMode({
50
+ replyInThread: true,
51
+ }),
52
+ ).toBe("create");
53
+ });
54
+ });
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { Client } from "@larksuiteoapi/node-sdk";
6
- import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
6
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/feishu";
7
7
  import type { FeishuDomain } from "./types.js";
8
8
 
9
9
  type Credentials = { appId: string; appSecret: string; domain?: FeishuDomain };
@@ -16,6 +16,13 @@ export type StreamingCardHeader = {
16
16
  template?: string;
17
17
  };
18
18
 
19
+ type StreamingStartOptions = {
20
+ replyToMessageId?: string;
21
+ replyInThread?: boolean;
22
+ rootId?: string;
23
+ header?: StreamingCardHeader;
24
+ };
25
+
19
26
  // Token cache (keyed by domain + appId)
20
27
  const tokenCache = new Map<string, { token: string; expiresAt: number }>();
21
28
 
@@ -60,6 +67,10 @@ async function getToken(creds: Credentials): Promise<string> {
60
67
  policy: { allowedHostnames: resolveAllowedHostnames(creds.domain) },
61
68
  auditContext: "feishu.streaming-card.token",
62
69
  });
70
+ if (!response.ok) {
71
+ await release();
72
+ throw new Error(`Token request failed with HTTP ${response.status}`);
73
+ }
63
74
  const data = (await response.json()) as {
64
75
  code: number;
65
76
  msg: string;
@@ -85,6 +96,52 @@ function truncateSummary(text: string, max = 50): string {
85
96
  return clean.length <= max ? clean : clean.slice(0, max - 3) + "...";
86
97
  }
87
98
 
99
+ export function mergeStreamingText(
100
+ previousText: string | undefined,
101
+ nextText: string | undefined,
102
+ ): string {
103
+ const previous = typeof previousText === "string" ? previousText : "";
104
+ const next = typeof nextText === "string" ? nextText : "";
105
+ if (!next) {
106
+ return previous;
107
+ }
108
+ if (!previous || next === previous) {
109
+ return next;
110
+ }
111
+ if (next.startsWith(previous)) {
112
+ return next;
113
+ }
114
+ if (previous.startsWith(next)) {
115
+ return previous;
116
+ }
117
+ if (next.includes(previous)) {
118
+ return next;
119
+ }
120
+ if (previous.includes(next)) {
121
+ return previous;
122
+ }
123
+
124
+ // Merge partial overlaps, e.g. "这" + "这是" => "这是".
125
+ const maxOverlap = Math.min(previous.length, next.length);
126
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
127
+ if (previous.slice(-overlap) === next.slice(0, overlap)) {
128
+ return `${previous}${next.slice(overlap)}`;
129
+ }
130
+ }
131
+ // Fallback for fragmented partial chunks: append as-is to avoid losing tokens.
132
+ return `${previous}${next}`;
133
+ }
134
+
135
+ export function resolveStreamingCardSendMode(options?: StreamingStartOptions) {
136
+ if (options?.replyToMessageId) {
137
+ return "reply";
138
+ }
139
+ if (options?.rootId) {
140
+ return "root_create";
141
+ }
142
+ return "create";
143
+ }
144
+
88
145
  /** Streaming card session manager */
89
146
  export class FeishuStreamingSession {
90
147
  private client: Client;
@@ -106,12 +163,7 @@ export class FeishuStreamingSession {
106
163
  async start(
107
164
  receiveId: string,
108
165
  receiveIdType: "open_id" | "user_id" | "union_id" | "email" | "chat_id" = "chat_id",
109
- options?: {
110
- replyToMessageId?: string;
111
- replyInThread?: boolean;
112
- rootId?: string;
113
- header?: StreamingCardHeader;
114
- },
166
+ options?: StreamingStartOptions,
115
167
  ): Promise<void> {
116
168
  if (this.state) {
117
169
  return;
@@ -123,7 +175,7 @@ export class FeishuStreamingSession {
123
175
  config: {
124
176
  streaming_mode: true,
125
177
  summary: { content: "[Generating...]" },
126
- streaming_config: { print_frequency_ms: { default: 50 }, print_step: { default: 2 } },
178
+ streaming_config: { print_frequency_ms: { default: 50 }, print_step: { default: 1 } },
127
179
  },
128
180
  body: {
129
181
  elements: [{ tag: "markdown", content: "⏳ Thinking...", element_id: "content" }],
@@ -150,6 +202,10 @@ export class FeishuStreamingSession {
150
202
  policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
151
203
  auditContext: "feishu.streaming-card.create",
152
204
  });
205
+ if (!createRes.ok) {
206
+ await releaseCreate();
207
+ throw new Error(`Create card request failed with HTTP ${createRes.status}`);
208
+ }
153
209
  const createData = (await createRes.json()) as {
154
210
  code: number;
155
211
  msg: string;
@@ -162,28 +218,31 @@ export class FeishuStreamingSession {
162
218
  const cardId = createData.data.card_id;
163
219
  const cardContent = JSON.stringify({ type: "card", data: { card_id: cardId } });
164
220
 
165
- // Topic-group replies require root_id routing. Prefer create+root_id when available.
221
+ // Prefer message.reply when we have a reply target — reply_in_thread
222
+ // reliably routes streaming cards into Feishu topics, whereas
223
+ // message.create with root_id may silently ignore root_id for card
224
+ // references (card_id format).
166
225
  let sendRes;
167
- if (options?.rootId) {
168
- const createData = {
169
- receive_id: receiveId,
170
- msg_type: "interactive",
171
- content: cardContent,
172
- root_id: options.rootId,
173
- };
174
- sendRes = await this.client.im.message.create({
175
- params: { receive_id_type: receiveIdType },
176
- data: createData,
177
- });
178
- } else if (options?.replyToMessageId) {
226
+ const sendOptions = options ?? {};
227
+ const sendMode = resolveStreamingCardSendMode(sendOptions);
228
+ if (sendMode === "reply") {
179
229
  sendRes = await this.client.im.message.reply({
180
- path: { message_id: options.replyToMessageId },
230
+ path: { message_id: sendOptions.replyToMessageId! },
181
231
  data: {
182
232
  msg_type: "interactive",
183
233
  content: cardContent,
184
- ...(options.replyInThread ? { reply_in_thread: true } : {}),
234
+ ...(sendOptions.replyInThread ? { reply_in_thread: true } : {}),
185
235
  },
186
236
  });
237
+ } else if (sendMode === "root_create") {
238
+ // root_id is undeclared in the SDK types but accepted at runtime
239
+ sendRes = await this.client.im.message.create({
240
+ params: { receive_id_type: receiveIdType },
241
+ data: Object.assign(
242
+ { receive_id: receiveId, msg_type: "interactive", content: cardContent },
243
+ { root_id: sendOptions.rootId },
244
+ ),
245
+ });
187
246
  } else {
188
247
  sendRes = await this.client.im.message.create({
189
248
  params: { receive_id_type: receiveIdType },
@@ -235,10 +294,15 @@ export class FeishuStreamingSession {
235
294
  if (!this.state || this.closed) {
236
295
  return;
237
296
  }
297
+ const mergedInput = mergeStreamingText(this.pendingText ?? this.state.currentText, text);
298
+ if (!mergedInput || mergedInput === this.state.currentText) {
299
+ return;
300
+ }
301
+
238
302
  // Throttle: skip if updated recently, but remember pending text
239
303
  const now = Date.now();
240
304
  if (now - this.lastUpdateTime < this.updateThrottleMs) {
241
- this.pendingText = text;
305
+ this.pendingText = mergedInput;
242
306
  return;
243
307
  }
244
308
  this.pendingText = null;
@@ -248,8 +312,12 @@ export class FeishuStreamingSession {
248
312
  if (!this.state || this.closed) {
249
313
  return;
250
314
  }
251
- this.state.currentText = text;
252
- await this.updateCardContent(text, (e) => this.log?.(`Update failed: ${String(e)}`));
315
+ const mergedText = mergeStreamingText(this.state.currentText, mergedInput);
316
+ if (!mergedText || mergedText === this.state.currentText) {
317
+ return;
318
+ }
319
+ this.state.currentText = mergedText;
320
+ await this.updateCardContent(mergedText, (e) => this.log?.(`Update failed: ${String(e)}`));
253
321
  });
254
322
  await this.queue;
255
323
  }
@@ -261,8 +329,8 @@ export class FeishuStreamingSession {
261
329
  this.closed = true;
262
330
  await this.queue;
263
331
 
264
- // Use finalText, or pending throttled text, or current text
265
- const text = finalText ?? this.pendingText ?? this.state.currentText;
332
+ const pendingMerged = mergeStreamingText(this.state.currentText, this.pendingText ?? undefined);
333
+ const text = finalText ? mergeStreamingText(pendingMerged, finalText) : pendingMerged;
266
334
  const apiBase = resolveApiBase(this.creds.domain);
267
335
 
268
336
  // Only send final update if content differs from what's already displayed
package/src/targets.ts CHANGED
@@ -66,7 +66,11 @@ export function formatFeishuTarget(id: string, type?: FeishuIdType): string {
66
66
  export function resolveReceiveIdType(id: string): "chat_id" | "open_id" | "user_id" {
67
67
  const trimmed = id.trim();
68
68
  const lowered = trimmed.toLowerCase();
69
- if (lowered.startsWith("chat:") || lowered.startsWith("group:")) {
69
+ if (
70
+ lowered.startsWith("chat:") ||
71
+ lowered.startsWith("group:") ||
72
+ lowered.startsWith("channel:")
73
+ ) {
70
74
  return "chat_id";
71
75
  }
72
76
  if (lowered.startsWith("open_id:")) {
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
2
2
  import { beforeEach, describe, expect, test, vi } from "vitest";
3
3
  import { registerFeishuBitableTools } from "./bitable.js";
4
4
  import { registerFeishuDriveTools } from "./drive.js";
@@ -35,12 +35,12 @@ function createConfig(params: {
35
35
  accounts: {
36
36
  a: {
37
37
  appId: "app-a",
38
- appSecret: "sec-a",
38
+ appSecret: "sec-a", // pragma: allowlist secret
39
39
  tools: params.toolsA,
40
40
  },
41
41
  b: {
42
42
  appId: "app-b",
43
- appSecret: "sec-b",
43
+ appSecret: "sec-b", // pragma: allowlist secret
44
44
  tools: params.toolsB,
45
45
  },
46
46
  },
@@ -1,5 +1,5 @@
1
1
  import type * as Lark from "@larksuiteoapi/node-sdk";
2
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
3
3
  import { resolveFeishuAccount } from "./accounts.js";
4
4
  import { createFeishuClient } from "./client.js";
5
5
  import { resolveToolsConfig } from "./tools-config.js";
@@ -1,4 +1,4 @@
1
- import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
2
2
 
3
3
  type ToolContextLike = {
4
4
  agentAccountId?: string;
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ jsonToolResult,
4
+ toolExecutionErrorResult,
5
+ unknownToolActionResult,
6
+ } from "./tool-result.js";
7
+
8
+ describe("jsonToolResult", () => {
9
+ it("formats tool result with text content and details", () => {
10
+ const payload = { ok: true, id: "abc" };
11
+ expect(jsonToolResult(payload)).toEqual({
12
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
13
+ details: payload,
14
+ });
15
+ });
16
+
17
+ it("formats unknown action errors", () => {
18
+ expect(unknownToolActionResult("create")).toEqual({
19
+ content: [
20
+ { type: "text", text: JSON.stringify({ error: "Unknown action: create" }, null, 2) },
21
+ ],
22
+ details: { error: "Unknown action: create" },
23
+ });
24
+ });
25
+
26
+ it("formats execution errors", () => {
27
+ expect(toolExecutionErrorResult(new Error("boom"))).toEqual({
28
+ content: [{ type: "text", text: JSON.stringify({ error: "boom" }, null, 2) }],
29
+ details: { error: "boom" },
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,14 @@
1
+ export function jsonToolResult(data: unknown) {
2
+ return {
3
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
4
+ details: data,
5
+ };
6
+ }
7
+
8
+ export function unknownToolActionResult(action: unknown) {
9
+ return jsonToolResult({ error: `Unknown action: ${String(action)}` });
10
+ }
11
+
12
+ export function toolExecutionErrorResult(error: unknown) {
13
+ return jsonToolResult({ error: error instanceof Error ? error.message : String(error) });
14
+ }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BaseProbeResult } from "openclaw/plugin-sdk";
1
+ import type { BaseProbeResult } from "openclaw/plugin-sdk/feishu";
2
2
  import type {
3
3
  FeishuConfigSchema,
4
4
  FeishuGroupSchema,
@@ -45,6 +45,7 @@ export type FeishuMessageContext = {
45
45
  senderName?: string;
46
46
  chatType: "p2p" | "group" | "private";
47
47
  mentionedBot: boolean;
48
+ hasAnyMention?: boolean;
48
49
  rootId?: string;
49
50
  parentId?: string;
50
51
  threadId?: string;
@@ -52,8 +53,6 @@ export type FeishuMessageContext = {
52
53
  contentType: string;
53
54
  /** Mention forward targets (excluding the bot itself) */
54
55
  mentionTargets?: MentionTarget[];
55
- /** Extracted message body (after removing @ placeholders) */
56
- mentionMessageBody?: string;
57
56
  };
58
57
 
59
58
  export type FeishuSendResult = {
package/src/typing.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
2
2
  import { resolveFeishuAccount } from "./accounts.js";
3
3
  import { createFeishuClient } from "./client.js";
4
4
  import { getFeishuRuntime } from "./runtime.js";
package/src/wiki.ts CHANGED
@@ -1,18 +1,14 @@
1
1
  import type * as Lark from "@larksuiteoapi/node-sdk";
2
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
3
3
  import { listEnabledFeishuAccounts } from "./accounts.js";
4
4
  import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
5
+ import {
6
+ jsonToolResult,
7
+ toolExecutionErrorResult,
8
+ unknownToolActionResult,
9
+ } from "./tool-result.js";
5
10
  import { FeishuWikiSchema, type FeishuWikiParams } from "./wiki-schema.js";
6
11
 
7
- // ============ Helpers ============
8
-
9
- function json(data: unknown) {
10
- return {
11
- content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
12
- details: data,
13
- };
14
- }
15
-
16
12
  type ObjType = "doc" | "sheet" | "mindnote" | "bitable" | "file" | "docx" | "slides";
17
13
 
18
14
  // ============ Actions ============
@@ -194,22 +190,22 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
194
190
  });
195
191
  switch (p.action) {
196
192
  case "spaces":
197
- return json(await listSpaces(client));
193
+ return jsonToolResult(await listSpaces(client));
198
194
  case "nodes":
199
- return json(await listNodes(client, p.space_id, p.parent_node_token));
195
+ return jsonToolResult(await listNodes(client, p.space_id, p.parent_node_token));
200
196
  case "get":
201
- return json(await getNode(client, p.token));
197
+ return jsonToolResult(await getNode(client, p.token));
202
198
  case "search":
203
- return json({
199
+ return jsonToolResult({
204
200
  error:
205
201
  "Search is not available. Use feishu_wiki with action: 'nodes' to browse or action: 'get' to lookup by token.",
206
202
  });
207
203
  case "create":
208
- return json(
204
+ return jsonToolResult(
209
205
  await createNode(client, p.space_id, p.title, p.obj_type, p.parent_node_token),
210
206
  );
211
207
  case "move":
212
- return json(
208
+ return jsonToolResult(
213
209
  await moveNode(
214
210
  client,
215
211
  p.space_id,
@@ -219,13 +215,13 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
219
215
  ),
220
216
  );
221
217
  case "rename":
222
- return json(await renameNode(client, p.space_id, p.node_token, p.title));
218
+ return jsonToolResult(await renameNode(client, p.space_id, p.node_token, p.title));
223
219
  default:
224
220
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
225
- return json({ error: `Unknown action: ${(p as any).action}` });
221
+ return unknownToolActionResult((p as { action?: unknown }).action);
226
222
  }
227
223
  } catch (err) {
228
- return json({ error: err instanceof Error ? err.message : String(err) });
224
+ return toolExecutionErrorResult(err);
229
225
  }
230
226
  },
231
227
  };