@openclaw/feishu 2026.2.25 → 2026.3.1
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/index.ts +2 -0
- package/package.json +2 -1
- package/skills/feishu-doc/SKILL.md +109 -3
- package/src/accounts.test.ts +90 -0
- package/src/accounts.ts +11 -2
- package/src/async.ts +62 -0
- package/src/bitable.ts +189 -215
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +55 -0
- package/src/bot.test.ts +863 -9
- package/src/bot.ts +414 -200
- package/src/card-action.ts +79 -0
- package/src/channel.ts +6 -0
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +107 -0
- package/src/client.ts +13 -0
- package/src/config-schema.test.ts +82 -1
- package/src/config-schema.ts +54 -3
- package/src/doc-schema.ts +141 -0
- package/src/docx-batch-insert.ts +190 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +76 -0
- package/src/docx.test.ts +470 -0
- package/src/docx.ts +996 -72
- package/src/drive.ts +38 -33
- package/src/media.test.ts +123 -6
- package/src/media.ts +31 -10
- package/src/monitor.account.ts +286 -0
- package/src/monitor.reaction.test.ts +235 -0
- package/src/monitor.startup.test.ts +187 -0
- package/src/monitor.startup.ts +51 -0
- package/src/monitor.state.ts +76 -0
- package/src/monitor.transport.ts +163 -0
- package/src/monitor.ts +44 -346
- package/src/monitor.webhook-security.test.ts +27 -1
- package/src/outbound.test.ts +181 -0
- package/src/outbound.ts +94 -7
- package/src/perm.ts +37 -30
- package/src/policy.test.ts +56 -1
- package/src/policy.ts +5 -1
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +253 -0
- package/src/probe.ts +99 -7
- package/src/reply-dispatcher.test.ts +259 -0
- package/src/reply-dispatcher.ts +139 -45
- package/src/send.reply-fallback.test.ts +105 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +143 -18
- package/src/streaming-card.ts +131 -43
- package/src/targets.test.ts +26 -1
- package/src/targets.ts +11 -6
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +140 -10
- 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 {
|
|
4
|
-
import
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
536
|
-
|
|
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
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -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
|
});
|