@openclaw/feishu 2026.2.25 → 2026.3.2

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 (73) hide show
  1. package/index.ts +2 -0
  2. package/package.json +2 -1
  3. package/skills/feishu-doc/SKILL.md +109 -3
  4. package/src/accounts.test.ts +161 -0
  5. package/src/accounts.ts +76 -8
  6. package/src/async.ts +62 -0
  7. package/src/bitable.ts +189 -215
  8. package/src/bot.card-action.test.ts +63 -0
  9. package/src/bot.checkBotMentioned.test.ts +56 -1
  10. package/src/bot.test.ts +1271 -56
  11. package/src/bot.ts +499 -215
  12. package/src/card-action.ts +79 -0
  13. package/src/channel.ts +26 -4
  14. package/src/chat-schema.ts +24 -0
  15. package/src/chat.test.ts +89 -0
  16. package/src/chat.ts +130 -0
  17. package/src/client.test.ts +121 -0
  18. package/src/client.ts +13 -0
  19. package/src/config-schema.test.ts +101 -1
  20. package/src/config-schema.ts +66 -11
  21. package/src/dedup.ts +47 -1
  22. package/src/doc-schema.ts +135 -0
  23. package/src/docx-batch-insert.ts +190 -0
  24. package/src/docx-color-text.ts +149 -0
  25. package/src/docx-table-ops.ts +298 -0
  26. package/src/docx.account-selection.test.ts +70 -0
  27. package/src/docx.test.ts +331 -9
  28. package/src/docx.ts +996 -72
  29. package/src/drive.ts +38 -33
  30. package/src/media.test.ts +227 -7
  31. package/src/media.ts +52 -11
  32. package/src/mention.ts +1 -1
  33. package/src/monitor.account.ts +534 -0
  34. package/src/monitor.reaction.test.ts +578 -0
  35. package/src/monitor.startup.test.ts +203 -0
  36. package/src/monitor.startup.ts +51 -0
  37. package/src/monitor.state.defaults.test.ts +46 -0
  38. package/src/monitor.state.ts +152 -0
  39. package/src/monitor.test-mocks.ts +12 -0
  40. package/src/monitor.transport.ts +163 -0
  41. package/src/monitor.ts +44 -346
  42. package/src/monitor.webhook-security.test.ts +53 -10
  43. package/src/onboarding.status.test.ts +25 -0
  44. package/src/onboarding.ts +144 -52
  45. package/src/outbound.test.ts +181 -0
  46. package/src/outbound.ts +94 -7
  47. package/src/perm.ts +37 -30
  48. package/src/policy.test.ts +56 -1
  49. package/src/policy.ts +5 -1
  50. package/src/post.test.ts +105 -0
  51. package/src/post.ts +274 -0
  52. package/src/probe.test.ts +271 -0
  53. package/src/probe.ts +131 -19
  54. package/src/reply-dispatcher.test.ts +300 -0
  55. package/src/reply-dispatcher.ts +159 -46
  56. package/src/secret-input.ts +19 -0
  57. package/src/send-target.test.ts +74 -0
  58. package/src/send-target.ts +6 -2
  59. package/src/send.reply-fallback.test.ts +105 -0
  60. package/src/send.test.ts +168 -0
  61. package/src/send.ts +143 -18
  62. package/src/streaming-card.ts +131 -43
  63. package/src/targets.test.ts +55 -1
  64. package/src/targets.ts +32 -7
  65. package/src/tool-account-routing.test.ts +129 -0
  66. package/src/tool-account.ts +70 -0
  67. package/src/tool-factory-test-harness.ts +76 -0
  68. package/src/tools-config.test.ts +21 -0
  69. package/src/tools-config.ts +2 -1
  70. package/src/types.ts +10 -1
  71. package/src/typing.test.ts +144 -0
  72. package/src/typing.ts +140 -10
  73. package/src/wiki.ts +55 -50
package/src/bitable.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import type * as Lark from "@larksuiteoapi/node-sdk";
1
2
  import { Type } from "@sinclair/typebox";
2
3
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
- import { createFeishuClient } from "./client.js";
4
- import type { FeishuConfig } from "./types.js";
4
+ import { listEnabledFeishuAccounts } from "./accounts.js";
5
+ import { createFeishuToolClient } from "./tool-account.js";
5
6
 
6
7
  // ============ Helpers ============
7
8
 
@@ -64,10 +65,7 @@ function parseBitableUrl(url: string): { token: string; tableId?: string; isWiki
64
65
  }
65
66
 
66
67
  /** Get app_token from wiki node_token */
67
- async function getAppTokenFromWiki(
68
- client: ReturnType<typeof createFeishuClient>,
69
- nodeToken: string,
70
- ): Promise<string> {
68
+ async function getAppTokenFromWiki(client: Lark.Client, nodeToken: string): Promise<string> {
71
69
  const res = await client.wiki.space.getNode({
72
70
  params: { token: nodeToken },
73
71
  });
@@ -87,7 +85,7 @@ async function getAppTokenFromWiki(
87
85
  }
88
86
 
89
87
  /** Get bitable metadata from URL (handles both /base/ and /wiki/ URLs) */
90
- async function getBitableMeta(client: ReturnType<typeof createFeishuClient>, url: string) {
88
+ async function getBitableMeta(client: Lark.Client, url: string) {
91
89
  const parsed = parseBitableUrl(url);
92
90
  if (!parsed) {
93
91
  throw new Error("Invalid URL format. Expected /base/XXX or /wiki/XXX URL");
@@ -134,11 +132,7 @@ async function getBitableMeta(client: ReturnType<typeof createFeishuClient>, url
134
132
  };
135
133
  }
136
134
 
137
- async function listFields(
138
- client: ReturnType<typeof createFeishuClient>,
139
- appToken: string,
140
- tableId: string,
141
- ) {
135
+ async function listFields(client: Lark.Client, appToken: string, tableId: string) {
142
136
  const res = await client.bitable.appTableField.list({
143
137
  path: { app_token: appToken, table_id: tableId },
144
138
  });
@@ -161,7 +155,7 @@ async function listFields(
161
155
  }
162
156
 
163
157
  async function listRecords(
164
- client: ReturnType<typeof createFeishuClient>,
158
+ client: Lark.Client,
165
159
  appToken: string,
166
160
  tableId: string,
167
161
  pageSize?: number,
@@ -186,12 +180,7 @@ async function listRecords(
186
180
  };
187
181
  }
188
182
 
189
- async function getRecord(
190
- client: ReturnType<typeof createFeishuClient>,
191
- appToken: string,
192
- tableId: string,
193
- recordId: string,
194
- ) {
183
+ async function getRecord(client: Lark.Client, appToken: string, tableId: string, recordId: string) {
195
184
  const res = await client.bitable.appTableRecord.get({
196
185
  path: { app_token: appToken, table_id: tableId, record_id: recordId },
197
186
  });
@@ -205,7 +194,7 @@ async function getRecord(
205
194
  }
206
195
 
207
196
  async function createRecord(
208
- client: ReturnType<typeof createFeishuClient>,
197
+ client: Lark.Client,
209
198
  appToken: string,
210
199
  tableId: string,
211
200
  fields: Record<string, unknown>,
@@ -235,7 +224,7 @@ const DEFAULT_CLEANUP_FIELD_TYPES = new Set([3, 5, 17]); // SingleSelect, DateTi
235
224
 
236
225
  /** Clean up default placeholder rows and fields in a newly created Bitable table */
237
226
  async function cleanupNewBitable(
238
- client: ReturnType<typeof createFeishuClient>,
227
+ client: Lark.Client,
239
228
  appToken: string,
240
229
  tableId: string,
241
230
  tableName: string,
@@ -334,7 +323,7 @@ async function cleanupNewBitable(
334
323
  }
335
324
 
336
325
  async function createApp(
337
- client: ReturnType<typeof createFeishuClient>,
326
+ client: Lark.Client,
338
327
  name: string,
339
328
  folderToken?: string,
340
329
  logger?: CleanupLogger,
@@ -389,7 +378,7 @@ async function createApp(
389
378
  }
390
379
 
391
380
  async function createField(
392
- client: ReturnType<typeof createFeishuClient>,
381
+ client: Lark.Client,
393
382
  appToken: string,
394
383
  tableId: string,
395
384
  fieldName: string,
@@ -417,7 +406,7 @@ async function createField(
417
406
  }
418
407
 
419
408
  async function updateRecord(
420
- client: ReturnType<typeof createFeishuClient>,
409
+ client: Lark.Client,
421
410
  appToken: string,
422
411
  tableId: string,
423
412
  recordId: string,
@@ -532,208 +521,193 @@ const UpdateRecordSchema = Type.Object({
532
521
  // ============ Tool Registration ============
533
522
 
534
523
  export function registerFeishuBitableTools(api: OpenClawPluginApi) {
535
- const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
536
- if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
537
- api.logger.debug?.("feishu_bitable: Feishu credentials not configured, skipping bitable tools");
524
+ if (!api.config) {
525
+ api.logger.debug?.("feishu_bitable: No config available, skipping bitable tools");
538
526
  return;
539
527
  }
540
528
 
541
- const getClient = () => createFeishuClient(feishuCfg);
542
-
543
- // Tool 0: feishu_bitable_get_meta (helper to parse URLs)
544
- api.registerTool(
545
- {
546
- name: "feishu_bitable_get_meta",
547
- label: "Feishu Bitable Get Meta",
548
- description:
549
- "Parse a Bitable URL and get app_token, table_id, and table list. Use this first when given a /wiki/ or /base/ URL.",
550
- parameters: GetMetaSchema,
551
- async execute(_toolCallId, params) {
552
- const { url } = params as { url: string };
553
- try {
554
- const result = await getBitableMeta(getClient(), url);
555
- return json(result);
556
- } catch (err) {
557
- return json({ error: err instanceof Error ? err.message : String(err) });
558
- }
559
- },
529
+ const accounts = listEnabledFeishuAccounts(api.config);
530
+ if (accounts.length === 0) {
531
+ api.logger.debug?.("feishu_bitable: No Feishu accounts configured, skipping bitable tools");
532
+ return;
533
+ }
534
+
535
+ type AccountAwareParams = { accountId?: string };
536
+
537
+ const getClient = (params: AccountAwareParams | undefined, defaultAccountId?: string) =>
538
+ createFeishuToolClient({ api, executeParams: params, defaultAccountId });
539
+
540
+ const registerBitableTool = <TParams extends AccountAwareParams>(params: {
541
+ name: string;
542
+ label: string;
543
+ description: string;
544
+ parameters: unknown;
545
+ execute: (args: { params: TParams; defaultAccountId?: string }) => Promise<unknown>;
546
+ }) => {
547
+ api.registerTool(
548
+ (ctx) => ({
549
+ name: params.name,
550
+ label: params.label,
551
+ description: params.description,
552
+ parameters: params.parameters,
553
+ async execute(_toolCallId, rawParams) {
554
+ try {
555
+ return json(
556
+ await params.execute({
557
+ params: rawParams as TParams,
558
+ defaultAccountId: ctx.agentAccountId,
559
+ }),
560
+ );
561
+ } catch (err) {
562
+ return json({ error: err instanceof Error ? err.message : String(err) });
563
+ }
564
+ },
565
+ }),
566
+ { name: params.name },
567
+ );
568
+ };
569
+
570
+ registerBitableTool<{ url: string; accountId?: string }>({
571
+ name: "feishu_bitable_get_meta",
572
+ label: "Feishu Bitable Get Meta",
573
+ description:
574
+ "Parse a Bitable URL and get app_token, table_id, and table list. Use this first when given a /wiki/ or /base/ URL.",
575
+ parameters: GetMetaSchema,
576
+ async execute({ params, defaultAccountId }) {
577
+ return getBitableMeta(getClient(params, defaultAccountId), params.url);
560
578
  },
561
- { name: "feishu_bitable_get_meta" },
562
- );
563
-
564
- // Tool 1: feishu_bitable_list_fields
565
- api.registerTool(
566
- {
567
- name: "feishu_bitable_list_fields",
568
- label: "Feishu Bitable List Fields",
569
- description: "List all fields (columns) in a Bitable table with their types and properties",
570
- parameters: ListFieldsSchema,
571
- async execute(_toolCallId, params) {
572
- const { app_token, table_id } = params as { app_token: string; table_id: string };
573
- try {
574
- const result = await listFields(getClient(), app_token, table_id);
575
- return json(result);
576
- } catch (err) {
577
- return json({ error: err instanceof Error ? err.message : String(err) });
578
- }
579
- },
579
+ });
580
+
581
+ registerBitableTool<{ app_token: string; table_id: string; accountId?: string }>({
582
+ name: "feishu_bitable_list_fields",
583
+ label: "Feishu Bitable List Fields",
584
+ description: "List all fields (columns) in a Bitable table with their types and properties",
585
+ parameters: ListFieldsSchema,
586
+ async execute({ params, defaultAccountId }) {
587
+ return listFields(getClient(params, defaultAccountId), params.app_token, params.table_id);
580
588
  },
581
- { name: "feishu_bitable_list_fields" },
582
- );
583
-
584
- // Tool 2: feishu_bitable_list_records
585
- api.registerTool(
586
- {
587
- name: "feishu_bitable_list_records",
588
- label: "Feishu Bitable List Records",
589
- description: "List records (rows) from a Bitable table with pagination support",
590
- parameters: ListRecordsSchema,
591
- async execute(_toolCallId, params) {
592
- const { app_token, table_id, page_size, page_token } = params as {
593
- app_token: string;
594
- table_id: string;
595
- page_size?: number;
596
- page_token?: string;
597
- };
598
- try {
599
- const result = await listRecords(getClient(), app_token, table_id, page_size, page_token);
600
- return json(result);
601
- } catch (err) {
602
- return json({ error: err instanceof Error ? err.message : String(err) });
603
- }
604
- },
589
+ });
590
+
591
+ registerBitableTool<{
592
+ app_token: string;
593
+ table_id: string;
594
+ page_size?: number;
595
+ page_token?: string;
596
+ accountId?: string;
597
+ }>({
598
+ name: "feishu_bitable_list_records",
599
+ label: "Feishu Bitable List Records",
600
+ description: "List records (rows) from a Bitable table with pagination support",
601
+ parameters: ListRecordsSchema,
602
+ async execute({ params, defaultAccountId }) {
603
+ return listRecords(
604
+ getClient(params, defaultAccountId),
605
+ params.app_token,
606
+ params.table_id,
607
+ params.page_size,
608
+ params.page_token,
609
+ );
605
610
  },
606
- { name: "feishu_bitable_list_records" },
607
- );
608
-
609
- // Tool 3: feishu_bitable_get_record
610
- api.registerTool(
611
- {
612
- name: "feishu_bitable_get_record",
613
- label: "Feishu Bitable Get Record",
614
- description: "Get a single record by ID from a Bitable table",
615
- parameters: GetRecordSchema,
616
- async execute(_toolCallId, params) {
617
- const { app_token, table_id, record_id } = params as {
618
- app_token: string;
619
- table_id: string;
620
- record_id: string;
621
- };
622
- try {
623
- const result = await getRecord(getClient(), app_token, table_id, record_id);
624
- return json(result);
625
- } catch (err) {
626
- return json({ error: err instanceof Error ? err.message : String(err) });
627
- }
628
- },
611
+ });
612
+
613
+ registerBitableTool<{
614
+ app_token: string;
615
+ table_id: string;
616
+ record_id: string;
617
+ accountId?: string;
618
+ }>({
619
+ name: "feishu_bitable_get_record",
620
+ label: "Feishu Bitable Get Record",
621
+ description: "Get a single record by ID from a Bitable table",
622
+ parameters: GetRecordSchema,
623
+ async execute({ params, defaultAccountId }) {
624
+ return getRecord(
625
+ getClient(params, defaultAccountId),
626
+ params.app_token,
627
+ params.table_id,
628
+ params.record_id,
629
+ );
629
630
  },
630
- { name: "feishu_bitable_get_record" },
631
- );
632
-
633
- // Tool 4: feishu_bitable_create_record
634
- api.registerTool(
635
- {
636
- name: "feishu_bitable_create_record",
637
- label: "Feishu Bitable Create Record",
638
- description: "Create a new record (row) in a Bitable table",
639
- parameters: CreateRecordSchema,
640
- async execute(_toolCallId, params) {
641
- const { app_token, table_id, fields } = params as {
642
- app_token: string;
643
- table_id: string;
644
- fields: Record<string, unknown>;
645
- };
646
- try {
647
- const result = await createRecord(getClient(), app_token, table_id, fields);
648
- return json(result);
649
- } catch (err) {
650
- return json({ error: err instanceof Error ? err.message : String(err) });
651
- }
652
- },
631
+ });
632
+
633
+ registerBitableTool<{
634
+ app_token: string;
635
+ table_id: string;
636
+ fields: Record<string, unknown>;
637
+ accountId?: string;
638
+ }>({
639
+ name: "feishu_bitable_create_record",
640
+ label: "Feishu Bitable Create Record",
641
+ description: "Create a new record (row) in a Bitable table",
642
+ parameters: CreateRecordSchema,
643
+ async execute({ params, defaultAccountId }) {
644
+ return createRecord(
645
+ getClient(params, defaultAccountId),
646
+ params.app_token,
647
+ params.table_id,
648
+ params.fields,
649
+ );
653
650
  },
654
- { name: "feishu_bitable_create_record" },
655
- );
656
-
657
- // Tool 5: feishu_bitable_update_record
658
- api.registerTool(
659
- {
660
- name: "feishu_bitable_update_record",
661
- label: "Feishu Bitable Update Record",
662
- description: "Update an existing record (row) in a Bitable table",
663
- parameters: UpdateRecordSchema,
664
- async execute(_toolCallId, params) {
665
- const { app_token, table_id, record_id, fields } = params as {
666
- app_token: string;
667
- table_id: string;
668
- record_id: string;
669
- fields: Record<string, unknown>;
670
- };
671
- try {
672
- const result = await updateRecord(getClient(), app_token, table_id, record_id, fields);
673
- return json(result);
674
- } catch (err) {
675
- return json({ error: err instanceof Error ? err.message : String(err) });
676
- }
677
- },
651
+ });
652
+
653
+ registerBitableTool<{
654
+ app_token: string;
655
+ table_id: string;
656
+ record_id: string;
657
+ fields: Record<string, unknown>;
658
+ accountId?: string;
659
+ }>({
660
+ name: "feishu_bitable_update_record",
661
+ label: "Feishu Bitable Update Record",
662
+ description: "Update an existing record (row) in a Bitable table",
663
+ parameters: UpdateRecordSchema,
664
+ async execute({ params, defaultAccountId }) {
665
+ return updateRecord(
666
+ getClient(params, defaultAccountId),
667
+ params.app_token,
668
+ params.table_id,
669
+ params.record_id,
670
+ params.fields,
671
+ );
678
672
  },
679
- { name: "feishu_bitable_update_record" },
680
- );
681
-
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
- },
673
+ });
674
+
675
+ registerBitableTool<{ name: string; folder_token?: string; accountId?: string }>({
676
+ name: "feishu_bitable_create_app",
677
+ label: "Feishu Bitable Create App",
678
+ description: "Create a new Bitable (multidimensional table) application",
679
+ parameters: CreateAppSchema,
680
+ async execute({ params, defaultAccountId }) {
681
+ return createApp(getClient(params, defaultAccountId), params.name, params.folder_token, {
682
+ debug: (msg) => api.logger.debug?.(msg),
683
+ warn: (msg) => api.logger.warn?.(msg),
684
+ });
701
685
  },
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
- },
686
+ });
687
+
688
+ registerBitableTool<{
689
+ app_token: string;
690
+ table_id: string;
691
+ field_name: string;
692
+ field_type: number;
693
+ property?: Record<string, unknown>;
694
+ accountId?: string;
695
+ }>({
696
+ name: "feishu_bitable_create_field",
697
+ label: "Feishu Bitable Create Field",
698
+ description: "Create a new field (column) in a Bitable table",
699
+ parameters: CreateFieldSchema,
700
+ async execute({ params, defaultAccountId }) {
701
+ return createField(
702
+ getClient(params, defaultAccountId),
703
+ params.app_token,
704
+ params.table_id,
705
+ params.field_name,
706
+ params.field_type,
707
+ params.property,
708
+ );
734
709
  },
735
- { name: "feishu_bitable_create_field" },
736
- );
710
+ });
737
711
 
738
712
  api.logger.info?.("feishu_bitable: Registered bitable tools");
739
713
  }
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
3
+
4
+ // Mock resolveFeishuAccount
5
+ vi.mock("./accounts.js", () => ({
6
+ resolveFeishuAccount: vi.fn().mockReturnValue({ accountId: "mock-account" }),
7
+ }));
8
+
9
+ // Mock bot.js to verify handleFeishuMessage call
10
+ vi.mock("./bot.js", () => ({
11
+ handleFeishuMessage: vi.fn(),
12
+ }));
13
+
14
+ import { handleFeishuMessage } from "./bot.js";
15
+
16
+ describe("Feishu Card Action Handler", () => {
17
+ const cfg = {} as any; // Minimal mock
18
+ const runtime = { log: vi.fn(), error: vi.fn() } as any;
19
+
20
+ it("handles card action with text payload", async () => {
21
+ const event: FeishuCardActionEvent = {
22
+ operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
23
+ token: "tok1",
24
+ action: { value: { text: "/ping" }, tag: "button" },
25
+ context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
26
+ };
27
+
28
+ await handleFeishuCardAction({ cfg, event, runtime });
29
+
30
+ expect(handleFeishuMessage).toHaveBeenCalledWith(
31
+ expect.objectContaining({
32
+ event: expect.objectContaining({
33
+ message: expect.objectContaining({
34
+ content: '{"text":"/ping"}',
35
+ chat_id: "chat1",
36
+ }),
37
+ }),
38
+ }),
39
+ );
40
+ });
41
+
42
+ it("handles card action with JSON object payload", async () => {
43
+ const event: FeishuCardActionEvent = {
44
+ operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
45
+ token: "tok2",
46
+ action: { value: { key: "val" }, tag: "button" },
47
+ context: { open_id: "u123", user_id: "uid1", chat_id: "" },
48
+ };
49
+
50
+ await handleFeishuCardAction({ cfg, event, runtime });
51
+
52
+ expect(handleFeishuMessage).toHaveBeenCalledWith(
53
+ expect.objectContaining({
54
+ event: expect.objectContaining({
55
+ message: expect.objectContaining({
56
+ content: '{"text":"{\\"key\\":\\"val\\"}"}',
57
+ chat_id: "u123", // Fallback to open_id
58
+ }),
59
+ }),
60
+ }),
61
+ );
62
+ });
63
+ });
@@ -3,7 +3,7 @@ import { parseFeishuMessageEvent } from "./bot.js";
3
3
 
4
4
  // Helper to build a minimal FeishuMessageEvent for testing
5
5
  function makeEvent(
6
- chatType: "p2p" | "group",
6
+ chatType: "p2p" | "group" | "private",
7
7
  mentions?: Array<{ key: string; name: string; id: { open_id?: string } }>,
8
8
  text = "hello",
9
9
  ) {
@@ -36,6 +36,20 @@ function makePostEvent(content: unknown) {
36
36
  };
37
37
  }
38
38
 
39
+ function makeShareChatEvent(content: unknown) {
40
+ return {
41
+ sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
42
+ message: {
43
+ message_id: "msg_1",
44
+ chat_id: "oc_chat1",
45
+ chat_type: "group",
46
+ message_type: "share_chat",
47
+ content: JSON.stringify(content),
48
+ mentions: [],
49
+ },
50
+ };
51
+ }
52
+
39
53
  describe("parseFeishuMessageEvent – mentionedBot", () => {
40
54
  const BOT_OPEN_ID = "ou_bot_123";
41
55
 
@@ -45,6 +59,15 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
45
59
  expect(ctx.mentionedBot).toBe(false);
46
60
  });
47
61
 
62
+ it("falls back to sender user_id when open_id is missing", () => {
63
+ const event = makeEvent("p2p", []);
64
+ (event as any).sender.sender_id = { user_id: "u_mobile_only" };
65
+
66
+ const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
67
+ expect(ctx.senderOpenId).toBe("u_mobile_only");
68
+ expect(ctx.senderId).toBe("u_mobile_only");
69
+ });
70
+
48
71
  it("returns mentionedBot=true when bot is mentioned", () => {
49
72
  const event = makeEvent("group", [
50
73
  { key: "@_user_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },
@@ -127,4 +150,36 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
127
150
  const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
128
151
  expect(ctx.mentionedBot).toBe(false);
129
152
  });
153
+
154
+ it("preserves post code and code_block content", () => {
155
+ const event = makePostEvent({
156
+ content: [
157
+ [
158
+ { tag: "text", text: "before " },
159
+ { tag: "code", text: "inline()" },
160
+ ],
161
+ [{ tag: "code_block", language: "ts", text: "const x = 1;" }],
162
+ ],
163
+ });
164
+ const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
165
+ expect(ctx.content).toContain("before `inline()`");
166
+ expect(ctx.content).toContain("```ts\nconst x = 1;\n```");
167
+ });
168
+
169
+ it("uses share_chat body when available", () => {
170
+ const event = makeShareChatEvent({
171
+ body: "Merged and Forwarded Message",
172
+ share_chat_id: "sc_abc123",
173
+ });
174
+ const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
175
+ expect(ctx.content).toBe("Merged and Forwarded Message");
176
+ });
177
+
178
+ it("falls back to share_chat identifier when body is unavailable", () => {
179
+ const event = makeShareChatEvent({
180
+ share_chat_id: "sc_abc123",
181
+ });
182
+ const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
183
+ expect(ctx.content).toBe("[Forwarded message: sc_abc123]");
184
+ });
130
185
  });