@openclaw/feishu 2026.2.14 → 2026.2.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/feishu",
3
- "version": "2026.2.14",
3
+ "version": "2026.2.17",
4
4
  "description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/bitable.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
1
  import { Type } from "@sinclair/typebox";
3
- import type { FeishuConfig } from "./types.js";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
4
3
  import { createFeishuClient } from "./client.js";
4
+ import type { FeishuConfig } from "./types.js";
5
5
 
6
6
  // ============ Helpers ============
7
7
 
@@ -224,6 +224,198 @@ async function createRecord(
224
224
  };
225
225
  }
226
226
 
227
+ /** Logger interface for cleanup operations */
228
+ type CleanupLogger = {
229
+ debug: (msg: string) => void;
230
+ warn: (msg: string) => void;
231
+ };
232
+
233
+ /** Default field types created for new Bitable tables (to be cleaned up) */
234
+ const DEFAULT_CLEANUP_FIELD_TYPES = new Set([3, 5, 17]); // SingleSelect, DateTime, Attachment
235
+
236
+ /** Clean up default placeholder rows and fields in a newly created Bitable table */
237
+ async function cleanupNewBitable(
238
+ client: ReturnType<typeof createFeishuClient>,
239
+ appToken: string,
240
+ tableId: string,
241
+ tableName: string,
242
+ logger: CleanupLogger,
243
+ ): Promise<{ cleanedRows: number; cleanedFields: number }> {
244
+ let cleanedRows = 0;
245
+ let cleanedFields = 0;
246
+
247
+ // Step 1: Clean up default fields
248
+ const fieldsRes = await client.bitable.appTableField.list({
249
+ path: { app_token: appToken, table_id: tableId },
250
+ });
251
+
252
+ if (fieldsRes.code === 0 && fieldsRes.data?.items) {
253
+ // Step 1a: Rename primary field to the table name (works for both Feishu and Lark)
254
+ const primaryField = fieldsRes.data.items.find((f) => f.is_primary);
255
+ if (primaryField?.field_id) {
256
+ try {
257
+ const newFieldName = tableName.length <= 20 ? tableName : "Name";
258
+ await client.bitable.appTableField.update({
259
+ path: {
260
+ app_token: appToken,
261
+ table_id: tableId,
262
+ field_id: primaryField.field_id,
263
+ },
264
+ data: {
265
+ field_name: newFieldName,
266
+ type: 1,
267
+ },
268
+ });
269
+ cleanedFields++;
270
+ } catch (err) {
271
+ logger.debug(`Failed to rename primary field: ${err}`);
272
+ }
273
+ }
274
+
275
+ // Step 1b: Delete default placeholder fields by type (works for both Feishu and Lark)
276
+ const defaultFieldsToDelete = fieldsRes.data.items.filter(
277
+ (f) => !f.is_primary && DEFAULT_CLEANUP_FIELD_TYPES.has(f.type ?? 0),
278
+ );
279
+
280
+ for (const field of defaultFieldsToDelete) {
281
+ if (field.field_id) {
282
+ try {
283
+ await client.bitable.appTableField.delete({
284
+ path: {
285
+ app_token: appToken,
286
+ table_id: tableId,
287
+ field_id: field.field_id,
288
+ },
289
+ });
290
+ cleanedFields++;
291
+ } catch (err) {
292
+ logger.debug(`Failed to delete default field ${field.field_name}: ${err}`);
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ // Step 2: Delete empty placeholder rows (batch when possible)
299
+ const recordsRes = await client.bitable.appTableRecord.list({
300
+ path: { app_token: appToken, table_id: tableId },
301
+ params: { page_size: 100 },
302
+ });
303
+
304
+ if (recordsRes.code === 0 && recordsRes.data?.items) {
305
+ const emptyRecordIds = recordsRes.data.items
306
+ .filter((r) => !r.fields || Object.keys(r.fields).length === 0)
307
+ .map((r) => r.record_id)
308
+ .filter((id): id is string => Boolean(id));
309
+
310
+ if (emptyRecordIds.length > 0) {
311
+ try {
312
+ await client.bitable.appTableRecord.batchDelete({
313
+ path: { app_token: appToken, table_id: tableId },
314
+ data: { records: emptyRecordIds },
315
+ });
316
+ cleanedRows = emptyRecordIds.length;
317
+ } catch {
318
+ // Fallback: delete one by one if batch API is unavailable
319
+ for (const recordId of emptyRecordIds) {
320
+ try {
321
+ await client.bitable.appTableRecord.delete({
322
+ path: { app_token: appToken, table_id: tableId, record_id: recordId },
323
+ });
324
+ cleanedRows++;
325
+ } catch (err) {
326
+ logger.debug(`Failed to delete empty row ${recordId}: ${err}`);
327
+ }
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ return { cleanedRows, cleanedFields };
334
+ }
335
+
336
+ async function createApp(
337
+ client: ReturnType<typeof createFeishuClient>,
338
+ name: string,
339
+ folderToken?: string,
340
+ logger?: CleanupLogger,
341
+ ) {
342
+ const res = await client.bitable.app.create({
343
+ data: {
344
+ name,
345
+ ...(folderToken && { folder_token: folderToken }),
346
+ },
347
+ });
348
+ if (res.code !== 0) {
349
+ throw new Error(res.msg);
350
+ }
351
+
352
+ const appToken = res.data?.app?.app_token;
353
+ if (!appToken) {
354
+ throw new Error("Failed to create Bitable: no app_token returned");
355
+ }
356
+
357
+ const log: CleanupLogger = logger ?? { debug: () => {}, warn: () => {} };
358
+ let tableId: string | undefined;
359
+ let cleanedRows = 0;
360
+ let cleanedFields = 0;
361
+
362
+ try {
363
+ const tablesRes = await client.bitable.appTable.list({
364
+ path: { app_token: appToken },
365
+ });
366
+ if (tablesRes.code === 0 && tablesRes.data?.items && tablesRes.data.items.length > 0) {
367
+ tableId = tablesRes.data.items[0].table_id ?? undefined;
368
+ if (tableId) {
369
+ const cleanup = await cleanupNewBitable(client, appToken, tableId, name, log);
370
+ cleanedRows = cleanup.cleanedRows;
371
+ cleanedFields = cleanup.cleanedFields;
372
+ }
373
+ }
374
+ } catch (err) {
375
+ log.debug(`Cleanup failed (non-critical): ${err}`);
376
+ }
377
+
378
+ return {
379
+ app_token: appToken,
380
+ table_id: tableId,
381
+ name: res.data?.app?.name,
382
+ url: res.data?.app?.url,
383
+ cleaned_placeholder_rows: cleanedRows,
384
+ cleaned_default_fields: cleanedFields,
385
+ hint: tableId
386
+ ? `Table created. Use app_token="${appToken}" and table_id="${tableId}" for other bitable tools.`
387
+ : "Table created. Use feishu_bitable_get_meta to get table_id and field details.",
388
+ };
389
+ }
390
+
391
+ async function createField(
392
+ client: ReturnType<typeof createFeishuClient>,
393
+ appToken: string,
394
+ tableId: string,
395
+ fieldName: string,
396
+ fieldType: number,
397
+ property?: Record<string, unknown>,
398
+ ) {
399
+ const res = await client.bitable.appTableField.create({
400
+ path: { app_token: appToken, table_id: tableId },
401
+ data: {
402
+ field_name: fieldName,
403
+ type: fieldType,
404
+ ...(property && { property }),
405
+ },
406
+ });
407
+ if (res.code !== 0) {
408
+ throw new Error(res.msg);
409
+ }
410
+
411
+ return {
412
+ field_id: res.data?.field?.field_id,
413
+ field_name: res.data?.field?.field_name,
414
+ type: res.data?.field?.type,
415
+ type_name: FIELD_TYPE_NAMES[res.data?.field?.type ?? 0] || `type_${res.data?.field?.type}`,
416
+ };
417
+ }
418
+
227
419
  async function updateRecord(
228
420
  client: ReturnType<typeof createFeishuClient>,
229
421
  appToken: string,
@@ -296,6 +488,36 @@ const CreateRecordSchema = Type.Object({
296
488
  }),
297
489
  });
298
490
 
491
+ const CreateAppSchema = Type.Object({
492
+ name: Type.String({
493
+ description: "Name for the new Bitable application",
494
+ }),
495
+ folder_token: Type.Optional(
496
+ Type.String({
497
+ description: "Optional folder token to place the Bitable in a specific folder",
498
+ }),
499
+ ),
500
+ });
501
+
502
+ const CreateFieldSchema = Type.Object({
503
+ app_token: Type.String({
504
+ description:
505
+ "Bitable app token (use feishu_bitable_get_meta to get from URL, or feishu_bitable_create_app to create new)",
506
+ }),
507
+ table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
508
+ field_name: Type.String({ description: "Name for the new field" }),
509
+ field_type: Type.Number({
510
+ description:
511
+ "Field type ID: 1=Text, 2=Number, 3=SingleSelect, 4=MultiSelect, 5=DateTime, 7=Checkbox, 11=User, 13=Phone, 15=URL, 17=Attachment, 18=SingleLink, 19=Lookup, 20=Formula, 21=DuplexLink, 22=Location, 23=GroupChat, 1001=CreatedTime, 1002=ModifiedTime, 1003=CreatedUser, 1004=ModifiedUser, 1005=AutoNumber",
512
+ minimum: 1,
513
+ }),
514
+ property: Type.Optional(
515
+ Type.Record(Type.String(), Type.Any(), {
516
+ description: "Field-specific properties (e.g., options for SingleSelect, format for Number)",
517
+ }),
518
+ ),
519
+ });
520
+
299
521
  const UpdateRecordSchema = Type.Object({
300
522
  app_token: Type.String({
301
523
  description: "Bitable app token (use feishu_bitable_get_meta to get from URL)",
@@ -457,5 +679,61 @@ export function registerFeishuBitableTools(api: OpenClawPluginApi) {
457
679
  { name: "feishu_bitable_update_record" },
458
680
  );
459
681
 
460
- api.logger.info?.(`feishu_bitable: Registered 6 bitable tools`);
682
+ // Tool 6: feishu_bitable_create_app
683
+ api.registerTool(
684
+ {
685
+ name: "feishu_bitable_create_app",
686
+ label: "Feishu Bitable Create App",
687
+ description: "Create a new Bitable (multidimensional table) application",
688
+ parameters: CreateAppSchema,
689
+ async execute(_toolCallId, params) {
690
+ const { name, folder_token } = params as { name: string; folder_token?: string };
691
+ try {
692
+ const result = await createApp(getClient(), name, folder_token, {
693
+ debug: (msg) => api.logger.debug?.(msg),
694
+ warn: (msg) => api.logger.warn?.(msg),
695
+ });
696
+ return json(result);
697
+ } catch (err) {
698
+ return json({ error: err instanceof Error ? err.message : String(err) });
699
+ }
700
+ },
701
+ },
702
+ { name: "feishu_bitable_create_app" },
703
+ );
704
+
705
+ // Tool 7: feishu_bitable_create_field
706
+ api.registerTool(
707
+ {
708
+ name: "feishu_bitable_create_field",
709
+ label: "Feishu Bitable Create Field",
710
+ description: "Create a new field (column) in a Bitable table",
711
+ parameters: CreateFieldSchema,
712
+ async execute(_toolCallId, params) {
713
+ const { app_token, table_id, field_name, field_type, property } = params as {
714
+ app_token: string;
715
+ table_id: string;
716
+ field_name: string;
717
+ field_type: number;
718
+ property?: Record<string, unknown>;
719
+ };
720
+ try {
721
+ const result = await createField(
722
+ getClient(),
723
+ app_token,
724
+ table_id,
725
+ field_name,
726
+ field_type,
727
+ property,
728
+ );
729
+ return json(result);
730
+ } catch (err) {
731
+ return json({ error: err instanceof Error ? err.message : String(err) });
732
+ }
733
+ },
734
+ },
735
+ { name: "feishu_bitable_create_field" },
736
+ );
737
+
738
+ api.logger.info?.("feishu_bitable: Registered bitable tools");
461
739
  }
@@ -61,4 +61,68 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
61
61
  const ctx = parseFeishuMessageEvent(event as any, "");
62
62
  expect(ctx.mentionedBot).toBe(false);
63
63
  });
64
+
65
+ it("returns mentionedBot=true for post message with at (no top-level mentions)", () => {
66
+ const BOT_OPEN_ID = "ou_bot_123";
67
+ const postContent = JSON.stringify({
68
+ content: [
69
+ [{ tag: "at", user_id: BOT_OPEN_ID, user_name: "claw" }],
70
+ [{ tag: "text", text: "What does this document say" }],
71
+ ],
72
+ });
73
+ const event = {
74
+ sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
75
+ message: {
76
+ message_id: "msg_1",
77
+ chat_id: "oc_chat1",
78
+ chat_type: "group",
79
+ message_type: "post",
80
+ content: postContent,
81
+ mentions: [],
82
+ },
83
+ };
84
+ const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
85
+ expect(ctx.mentionedBot).toBe(true);
86
+ });
87
+
88
+ it("returns mentionedBot=false for post message with no at", () => {
89
+ const postContent = JSON.stringify({
90
+ content: [[{ tag: "text", text: "hello" }]],
91
+ });
92
+ const event = {
93
+ sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
94
+ message: {
95
+ message_id: "msg_1",
96
+ chat_id: "oc_chat1",
97
+ chat_type: "group",
98
+ message_type: "post",
99
+ content: postContent,
100
+ mentions: [],
101
+ },
102
+ };
103
+ const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
104
+ expect(ctx.mentionedBot).toBe(false);
105
+ });
106
+
107
+ it("returns mentionedBot=false for post message with at for another user", () => {
108
+ const postContent = JSON.stringify({
109
+ content: [
110
+ [{ tag: "at", user_id: "ou_other", user_name: "other" }],
111
+ [{ tag: "text", text: "hello" }],
112
+ ],
113
+ });
114
+ const event = {
115
+ sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
116
+ message: {
117
+ message_id: "msg_1",
118
+ chat_id: "oc_chat1",
119
+ chat_type: "group",
120
+ message_type: "post",
121
+ content: postContent,
122
+ mentions: [],
123
+ },
124
+ };
125
+ const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
126
+ expect(ctx.mentionedBot).toBe(false);
127
+ });
64
128
  });
package/src/bot.test.ts CHANGED
@@ -99,7 +99,13 @@ describe("handleFeishuMessage command authorization", () => {
99
99
  await handleFeishuMessage({
100
100
  cfg,
101
101
  event,
102
- runtime: { log: vi.fn(), error: vi.fn() } as RuntimeEnv,
102
+ runtime: {
103
+ log: vi.fn(),
104
+ error: vi.fn(),
105
+ exit: vi.fn((code: number): never => {
106
+ throw new Error(`exit ${code}`);
107
+ }),
108
+ } as RuntimeEnv,
103
109
  });
104
110
 
105
111
  expect(mockResolveCommandAuthorizedFromAuthorizers).toHaveBeenCalledWith({
@@ -148,7 +154,13 @@ describe("handleFeishuMessage command authorization", () => {
148
154
  await handleFeishuMessage({
149
155
  cfg,
150
156
  event,
151
- runtime: { log: vi.fn(), error: vi.fn() } as RuntimeEnv,
157
+ runtime: {
158
+ log: vi.fn(),
159
+ error: vi.fn(),
160
+ exit: vi.fn((code: number): never => {
161
+ throw new Error(`exit ${code}`);
162
+ }),
163
+ } as RuntimeEnv,
152
164
  });
153
165
 
154
166
  expect(mockReadAllowFromStore).toHaveBeenCalledWith("feishu");
@@ -189,7 +201,13 @@ describe("handleFeishuMessage command authorization", () => {
189
201
  await handleFeishuMessage({
190
202
  cfg,
191
203
  event,
192
- runtime: { log: vi.fn(), error: vi.fn() } as RuntimeEnv,
204
+ runtime: {
205
+ log: vi.fn(),
206
+ error: vi.fn(),
207
+ exit: vi.fn((code: number): never => {
208
+ throw new Error(`exit ${code}`);
209
+ }),
210
+ } as RuntimeEnv,
193
211
  });
194
212
 
195
213
  expect(mockUpsertPairingRequest).toHaveBeenCalledWith({
@@ -247,7 +265,13 @@ describe("handleFeishuMessage command authorization", () => {
247
265
  await handleFeishuMessage({
248
266
  cfg,
249
267
  event,
250
- runtime: { log: vi.fn(), error: vi.fn() } as RuntimeEnv,
268
+ runtime: {
269
+ log: vi.fn(),
270
+ error: vi.fn(),
271
+ exit: vi.fn((code: number): never => {
272
+ throw new Error(`exit ${code}`);
273
+ }),
274
+ } as RuntimeEnv,
251
275
  });
252
276
 
253
277
  expect(mockResolveCommandAuthorizedFromAuthorizers).toHaveBeenCalledWith({
package/src/bot.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
2
  import {
3
+ buildAgentMediaPayload,
3
4
  buildPendingHistoryContextFromMap,
4
5
  recordPendingHistoryEntryIfEnabled,
5
6
  clearHistoryEntriesIfEnabled,
6
7
  DEFAULT_GROUP_HISTORY_LIMIT,
7
8
  type HistoryEntry,
8
9
  } from "openclaw/plugin-sdk";
9
- import type { FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
10
- import type { DynamicAgentCreationConfig } from "./types.js";
11
10
  import { resolveFeishuAccount } from "./accounts.js";
12
11
  import { createFeishuClient } from "./client.js";
13
12
  import { tryRecordMessage } from "./dedup.js";
@@ -23,6 +22,8 @@ import {
23
22
  import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
24
23
  import { getFeishuRuntime } from "./runtime.js";
25
24
  import { getMessageFeishu, sendMessageFeishu } from "./send.js";
25
+ import type { FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
26
+ import type { DynamicAgentCreationConfig } from "./types.js";
26
27
 
27
28
  // --- Permission error extraction ---
28
29
  // Extract permission grant URL from Feishu API error response.
@@ -184,10 +185,17 @@ function parseMessageContent(content: string, messageType: string): string {
184
185
  }
185
186
 
186
187
  function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string): boolean {
187
- const mentions = event.message.mentions ?? [];
188
- if (mentions.length === 0) return false;
189
188
  if (!botOpenId) return false;
190
- return mentions.some((m) => m.id.open_id === botOpenId);
189
+ const mentions = event.message.mentions ?? [];
190
+ if (mentions.length > 0) {
191
+ return mentions.some((m) => m.id.open_id === botOpenId);
192
+ }
193
+ // Post (rich text) messages may have empty message.mentions when they contain docs/paste
194
+ if (event.message.message_type === "post") {
195
+ const { mentionedOpenIds } = parsePostContent(event.message.content);
196
+ return mentionedOpenIds.some((id) => id === botOpenId);
197
+ }
198
+ return false;
191
199
  }
192
200
 
193
201
  function stripBotMention(
@@ -243,6 +251,7 @@ function parseMediaKeys(
243
251
  function parsePostContent(content: string): {
244
252
  textContent: string;
245
253
  imageKeys: string[];
254
+ mentionedOpenIds: string[];
246
255
  } {
247
256
  try {
248
257
  const parsed = JSON.parse(content);
@@ -250,6 +259,7 @@ function parsePostContent(content: string): {
250
259
  const contentBlocks = parsed.content || [];
251
260
  let textContent = title ? `${title}\n\n` : "";
252
261
  const imageKeys: string[] = [];
262
+ const mentionedOpenIds: string[] = [];
253
263
 
254
264
  for (const paragraph of contentBlocks) {
255
265
  if (Array.isArray(paragraph)) {
@@ -262,6 +272,9 @@ function parsePostContent(content: string): {
262
272
  } else if (element.tag === "at") {
263
273
  // Mention: @username
264
274
  textContent += `@${element.user_name || element.user_id || ""}`;
275
+ if (element.user_id) {
276
+ mentionedOpenIds.push(element.user_id);
277
+ }
265
278
  } else if (element.tag === "img" && element.image_key) {
266
279
  // Embedded image
267
280
  imageKeys.push(element.image_key);
@@ -272,11 +285,12 @@ function parsePostContent(content: string): {
272
285
  }
273
286
 
274
287
  return {
275
- textContent: textContent.trim() || "[富文本消息]",
288
+ textContent: textContent.trim() || "[Rich text message]",
276
289
  imageKeys,
290
+ mentionedOpenIds,
277
291
  };
278
292
  } catch {
279
- return { textContent: "[富文本消息]", imageKeys: [] };
293
+ return { textContent: "[Rich text message]", imageKeys: [], mentionedOpenIds: [] };
280
294
  }
281
295
  }
282
296
 
@@ -433,27 +447,6 @@ async function resolveFeishuMediaList(params: {
433
447
  * Build media payload for inbound context.
434
448
  * Similar to Discord's buildDiscordMediaPayload().
435
449
  */
436
- function buildFeishuMediaPayload(mediaList: FeishuMediaInfo[]): {
437
- MediaPath?: string;
438
- MediaType?: string;
439
- MediaUrl?: string;
440
- MediaPaths?: string[];
441
- MediaUrls?: string[];
442
- MediaTypes?: string[];
443
- } {
444
- const first = mediaList[0];
445
- const mediaPaths = mediaList.map((media) => media.path);
446
- const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean) as string[];
447
- return {
448
- MediaPath: first?.path,
449
- MediaType: first?.contentType,
450
- MediaUrl: first?.path,
451
- MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
452
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
453
- MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
454
- };
455
- }
456
-
457
450
  export function parseFeishuMessageEvent(
458
451
  event: FeishuMessageEvent,
459
452
  botOpenId?: string,
@@ -766,7 +759,7 @@ export async function handleFeishuMessage(params: {
766
759
  log,
767
760
  accountId: account.accountId,
768
761
  });
769
- const mediaPayload = buildFeishuMediaPayload(mediaList);
762
+ const mediaPayload = buildAgentMediaPayload(mediaList);
770
763
 
771
764
  // Fetch quoted/replied message content if parentId exists
772
765
  let quotedContent: string | undefined;
package/src/channel.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
3
- import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
2
+ import {
3
+ buildBaseChannelStatusSummary,
4
+ createDefaultChannelRuntimeState,
5
+ DEFAULT_ACCOUNT_ID,
6
+ PAIRING_APPROVED_MESSAGE,
7
+ } from "openclaw/plugin-sdk";
4
8
  import {
5
9
  resolveFeishuAccount,
6
10
  resolveFeishuCredentials,
@@ -19,6 +23,7 @@ import { resolveFeishuGroupToolPolicy } from "./policy.js";
19
23
  import { probeFeishu } from "./probe.js";
20
24
  import { sendMessageFeishu } from "./send.js";
21
25
  import { normalizeFeishuTarget, looksLikeFeishuId, formatFeishuTarget } from "./targets.js";
26
+ import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
22
27
 
23
28
  const meta: ChannelMeta = {
24
29
  id: "feishu",
@@ -303,20 +308,9 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
303
308
  },
304
309
  outbound: feishuOutbound,
305
310
  status: {
306
- defaultRuntime: {
307
- accountId: DEFAULT_ACCOUNT_ID,
308
- running: false,
309
- lastStartAt: null,
310
- lastStopAt: null,
311
- lastError: null,
312
- port: null,
313
- },
311
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
314
312
  buildChannelSummary: ({ snapshot }) => ({
315
- configured: snapshot.configured ?? false,
316
- running: snapshot.running ?? false,
317
- lastStartAt: snapshot.lastStartAt ?? null,
318
- lastStopAt: snapshot.lastStopAt ?? null,
319
- lastError: snapshot.lastError ?? null,
313
+ ...buildBaseChannelStatusSummary(snapshot),
320
314
  port: snapshot.port ?? null,
321
315
  probe: snapshot.probe,
322
316
  lastProbeAt: snapshot.lastProbeAt ?? null,
package/src/docx.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import { Readable } from "stream";
1
2
  import type * as Lark from "@larksuiteoapi/node-sdk";
2
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
3
  import { Type } from "@sinclair/typebox";
4
- import { Readable } from "stream";
4
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
5
5
  import { listEnabledFeishuAccounts } from "./accounts.js";
6
6
  import { createFeishuClient } from "./client.js";
7
7
  import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
@@ -1,7 +1,7 @@
1
- import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
1
  import fs from "node:fs";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
4
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
5
5
  import type { DynamicAgentCreationConfig } from "./types.js";
6
6
 
7
7
  export type MaybeCreateDynamicAgentResult = {
package/src/media.ts CHANGED
@@ -1,11 +1,12 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
1
  import fs from "fs";
3
2
  import os from "os";
4
3
  import path from "path";
5
4
  import { Readable } from "stream";
5
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
6
6
  import { resolveFeishuAccount } from "./accounts.js";
7
7
  import { createFeishuClient } from "./client.js";
8
8
  import { getFeishuRuntime } from "./runtime.js";
9
+ import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
9
10
  import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
10
11
 
11
12
  export type DownloadImageResult = {
@@ -283,15 +284,8 @@ export async function sendImageFeishu(params: {
283
284
  msg_type: "image",
284
285
  },
285
286
  });
286
-
287
- if (response.code !== 0) {
288
- throw new Error(`Feishu image reply failed: ${response.msg || `code ${response.code}`}`);
289
- }
290
-
291
- return {
292
- messageId: response.data?.message_id ?? "unknown",
293
- chatId: receiveId,
294
- };
287
+ assertFeishuMessageApiSuccess(response, "Feishu image reply failed");
288
+ return toFeishuSendResult(response, receiveId);
295
289
  }
296
290
 
297
291
  const response = await client.im.message.create({
@@ -302,15 +296,8 @@ export async function sendImageFeishu(params: {
302
296
  msg_type: "image",
303
297
  },
304
298
  });
305
-
306
- if (response.code !== 0) {
307
- throw new Error(`Feishu image send failed: ${response.msg || `code ${response.code}`}`);
308
- }
309
-
310
- return {
311
- messageId: response.data?.message_id ?? "unknown",
312
- chatId: receiveId,
313
- };
299
+ assertFeishuMessageApiSuccess(response, "Feishu image send failed");
300
+ return toFeishuSendResult(response, receiveId);
314
301
  }
315
302
 
316
303
  /**
@@ -349,15 +336,8 @@ export async function sendFileFeishu(params: {
349
336
  msg_type: msgType,
350
337
  },
351
338
  });
352
-
353
- if (response.code !== 0) {
354
- throw new Error(`Feishu file reply failed: ${response.msg || `code ${response.code}`}`);
355
- }
356
-
357
- return {
358
- messageId: response.data?.message_id ?? "unknown",
359
- chatId: receiveId,
360
- };
339
+ assertFeishuMessageApiSuccess(response, "Feishu file reply failed");
340
+ return toFeishuSendResult(response, receiveId);
361
341
  }
362
342
 
363
343
  const response = await client.im.message.create({
@@ -368,15 +348,8 @@ export async function sendFileFeishu(params: {
368
348
  msg_type: msgType,
369
349
  },
370
350
  });
371
-
372
- if (response.code !== 0) {
373
- throw new Error(`Feishu file send failed: ${response.msg || `code ${response.code}`}`);
374
- }
375
-
376
- return {
377
- messageId: response.data?.message_id ?? "unknown",
378
- chatId: receiveId,
379
- };
351
+ assertFeishuMessageApiSuccess(response, "Feishu file send failed");
352
+ return toFeishuSendResult(response, receiveId);
380
353
  }
381
354
 
382
355
  /**
package/src/monitor.ts CHANGED
@@ -1,16 +1,16 @@
1
- import * as Lark from "@larksuiteoapi/node-sdk";
2
1
  import * as http from "http";
2
+ import * as Lark from "@larksuiteoapi/node-sdk";
3
3
  import {
4
4
  type ClawdbotConfig,
5
5
  type RuntimeEnv,
6
6
  type HistoryEntry,
7
7
  installRequestBodyLimitGuard,
8
8
  } from "openclaw/plugin-sdk";
9
- import type { ResolvedFeishuAccount } from "./types.js";
10
9
  import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
11
10
  import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
12
11
  import { createFeishuWSClient, createEventDispatcher } from "./client.js";
13
12
  import { probeFeishu } from "./probe.js";
13
+ import type { ResolvedFeishuAccount } from "./types.js";
14
14
 
15
15
  export type MonitorFeishuOpts = {
16
16
  config?: ClawdbotConfig;
package/src/onboarding.ts CHANGED
@@ -6,9 +6,9 @@ import type {
6
6
  WizardPrompter,
7
7
  } from "openclaw/plugin-sdk";
8
8
  import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "openclaw/plugin-sdk";
9
- import type { FeishuConfig } from "./types.js";
10
9
  import { resolveFeishuCredentials } from "./accounts.js";
11
10
  import { probeFeishu } from "./probe.js";
11
+ import type { FeishuConfig } from "./types.js";
12
12
 
13
13
  const channel = "feishu" as const;
14
14
 
package/src/policy.ts CHANGED
@@ -1,39 +1,19 @@
1
- import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
1
+ import type {
2
+ AllowlistMatch,
3
+ ChannelGroupContext,
4
+ GroupToolPolicyConfig,
5
+ } from "openclaw/plugin-sdk";
6
+ import { resolveAllowlistMatchSimple } from "openclaw/plugin-sdk";
2
7
  import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
3
8
 
4
- export type FeishuAllowlistMatch = {
5
- allowed: boolean;
6
- matchKey?: string;
7
- matchSource?: "wildcard" | "id" | "name";
8
- };
9
+ export type FeishuAllowlistMatch = AllowlistMatch<"wildcard" | "id" | "name">;
9
10
 
10
11
  export function resolveFeishuAllowlistMatch(params: {
11
12
  allowFrom: Array<string | number>;
12
13
  senderId: string;
13
14
  senderName?: string | null;
14
15
  }): FeishuAllowlistMatch {
15
- const allowFrom = params.allowFrom
16
- .map((entry) => String(entry).trim().toLowerCase())
17
- .filter(Boolean);
18
-
19
- if (allowFrom.length === 0) {
20
- return { allowed: false };
21
- }
22
- if (allowFrom.includes("*")) {
23
- return { allowed: true, matchKey: "*", matchSource: "wildcard" };
24
- }
25
-
26
- const senderId = params.senderId.toLowerCase();
27
- if (allowFrom.includes(senderId)) {
28
- return { allowed: true, matchKey: senderId, matchSource: "id" };
29
- }
30
-
31
- const senderName = params.senderName?.toLowerCase();
32
- if (senderName && allowFrom.includes(senderName)) {
33
- return { allowed: true, matchKey: senderName, matchSource: "name" };
34
- }
35
-
36
- return { allowed: false };
16
+ return resolveAllowlistMatchSimple(params);
37
17
  }
38
18
 
39
19
  export function resolveFeishuGroupConfig(params: {
package/src/probe.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { FeishuProbeResult } from "./types.js";
2
1
  import { createFeishuClient, type FeishuClientCredentials } from "./client.js";
2
+ import type { FeishuProbeResult } from "./types.js";
3
3
 
4
4
  export async function probeFeishu(creds?: FeishuClientCredentials): Promise<FeishuProbeResult> {
5
5
  if (!creds?.appId || !creds?.appSecret) {
@@ -6,9 +6,9 @@ import {
6
6
  type ReplyPayload,
7
7
  type RuntimeEnv,
8
8
  } from "openclaw/plugin-sdk";
9
- import type { MentionTarget } from "./mention.js";
10
9
  import { resolveFeishuAccount } from "./accounts.js";
11
10
  import { createFeishuClient } from "./client.js";
11
+ import type { MentionTarget } from "./mention.js";
12
12
  import { buildMentionedCardContent } from "./mention.js";
13
13
  import { getFeishuRuntime } from "./runtime.js";
14
14
  import { sendMarkdownCardFeishu, sendMessageFeishu } from "./send.js";
@@ -0,0 +1,29 @@
1
+ export type FeishuMessageApiResponse = {
2
+ code?: number;
3
+ msg?: string;
4
+ data?: {
5
+ message_id?: string;
6
+ };
7
+ };
8
+
9
+ export function assertFeishuMessageApiSuccess(
10
+ response: FeishuMessageApiResponse,
11
+ errorPrefix: string,
12
+ ) {
13
+ if (response.code !== 0) {
14
+ throw new Error(`${errorPrefix}: ${response.msg || `code ${response.code}`}`);
15
+ }
16
+ }
17
+
18
+ export function toFeishuSendResult(
19
+ response: FeishuMessageApiResponse,
20
+ chatId: string,
21
+ ): {
22
+ messageId: string;
23
+ chatId: string;
24
+ } {
25
+ return {
26
+ messageId: response.data?.message_id ?? "unknown",
27
+ chatId,
28
+ };
29
+ }
package/src/send.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import type { MentionTarget } from "./mention.js";
3
- import type { FeishuSendResult, ResolvedFeishuAccount } from "./types.js";
4
2
  import { resolveFeishuAccount } from "./accounts.js";
5
3
  import { createFeishuClient } from "./client.js";
4
+ import type { MentionTarget } from "./mention.js";
6
5
  import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
7
6
  import { getFeishuRuntime } from "./runtime.js";
7
+ import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
8
8
  import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
9
+ import type { FeishuSendResult, ResolvedFeishuAccount } from "./types.js";
9
10
 
10
11
  export type FeishuMessageInfo = {
11
12
  messageId: string;
@@ -161,15 +162,8 @@ export async function sendMessageFeishu(
161
162
  msg_type: msgType,
162
163
  },
163
164
  });
164
-
165
- if (response.code !== 0) {
166
- throw new Error(`Feishu reply failed: ${response.msg || `code ${response.code}`}`);
167
- }
168
-
169
- return {
170
- messageId: response.data?.message_id ?? "unknown",
171
- chatId: receiveId,
172
- };
165
+ assertFeishuMessageApiSuccess(response, "Feishu reply failed");
166
+ return toFeishuSendResult(response, receiveId);
173
167
  }
174
168
 
175
169
  const response = await client.im.message.create({
@@ -180,15 +174,8 @@ export async function sendMessageFeishu(
180
174
  msg_type: msgType,
181
175
  },
182
176
  });
183
-
184
- if (response.code !== 0) {
185
- throw new Error(`Feishu send failed: ${response.msg || `code ${response.code}`}`);
186
- }
187
-
188
- return {
189
- messageId: response.data?.message_id ?? "unknown",
190
- chatId: receiveId,
191
- };
177
+ assertFeishuMessageApiSuccess(response, "Feishu send failed");
178
+ return toFeishuSendResult(response, receiveId);
192
179
  }
193
180
 
194
181
  export type SendFeishuCardParams = {
@@ -223,15 +210,8 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
223
210
  msg_type: "interactive",
224
211
  },
225
212
  });
226
-
227
- if (response.code !== 0) {
228
- throw new Error(`Feishu card reply failed: ${response.msg || `code ${response.code}`}`);
229
- }
230
-
231
- return {
232
- messageId: response.data?.message_id ?? "unknown",
233
- chatId: receiveId,
234
- };
213
+ assertFeishuMessageApiSuccess(response, "Feishu card reply failed");
214
+ return toFeishuSendResult(response, receiveId);
235
215
  }
236
216
 
237
217
  const response = await client.im.message.create({
@@ -242,15 +222,8 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
242
222
  msg_type: "interactive",
243
223
  },
244
224
  });
245
-
246
- if (response.code !== 0) {
247
- throw new Error(`Feishu card send failed: ${response.msg || `code ${response.code}`}`);
248
- }
249
-
250
- return {
251
- messageId: response.data?.message_id ?? "unknown",
252
- chatId: receiveId,
253
- };
225
+ assertFeishuMessageApiSuccess(response, "Feishu card send failed");
226
+ return toFeishuSendResult(response, receiveId);
254
227
  }
255
228
 
256
229
  export async function updateCardFeishu(params: {
package/src/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { BaseProbeResult } from "openclaw/plugin-sdk";
1
2
  import type {
2
3
  FeishuConfigSchema,
3
4
  FeishuGroupSchema,
@@ -52,9 +53,7 @@ export type FeishuSendResult = {
52
53
  chatId: string;
53
54
  };
54
55
 
55
- export type FeishuProbeResult = {
56
- ok: boolean;
57
- error?: string;
56
+ export type FeishuProbeResult = BaseProbeResult<string> & {
58
57
  appId?: string;
59
58
  botName?: string;
60
59
  botOpenId?: string;