@openclaw/feishu 2026.3.12 → 2026.5.1-beta.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/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1653 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +115 -22
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +798 -786
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +77 -25
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +76 -35
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +413 -87
- package/src/media.ts +488 -154
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +220 -313
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +194 -92
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +24 -36
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +297 -39
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +272 -0
- package/src/monitor.webhook-security.test.ts +125 -91
- package/src/monitor.webhook.test-helpers.ts +116 -0
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +627 -53
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +122 -118
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +23 -60
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +721 -168
- package/src/reply-dispatcher.ts +422 -172
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +127 -42
- package/src/send.test.ts +386 -4
- package/src/send.ts +486 -164
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +479 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/bitable.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import {
|
|
3
|
-
import type
|
|
2
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
|
+
import { Type, type TSchema } from "typebox";
|
|
4
|
+
import type { OpenClawPluginApi } from "../runtime-api.js";
|
|
4
5
|
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
5
6
|
import { createFeishuToolClient } from "./tool-account.js";
|
|
6
7
|
|
|
@@ -14,6 +15,16 @@ function json(data: unknown) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
type LarkResponse<T = unknown> = { code?: number; msg?: string; data?: T };
|
|
18
|
+
type BitableRecordCreatePayload = NonNullable<
|
|
19
|
+
Parameters<Lark.Client["bitable"]["appTableRecord"]["create"]>[0]
|
|
20
|
+
>;
|
|
21
|
+
type BitableRecordUpdatePayload = NonNullable<
|
|
22
|
+
Parameters<Lark.Client["bitable"]["appTableRecord"]["update"]>[0]
|
|
23
|
+
>;
|
|
24
|
+
type BitableRecordFields = NonNullable<NonNullable<BitableRecordCreatePayload["data"]>["fields"]>;
|
|
25
|
+
type BitableRecordUpdateFields = NonNullable<
|
|
26
|
+
NonNullable<BitableRecordUpdatePayload["data"]>["fields"]
|
|
27
|
+
>;
|
|
17
28
|
|
|
18
29
|
export class LarkApiError extends Error {
|
|
19
30
|
readonly code: number;
|
|
@@ -212,12 +223,11 @@ async function createRecord(
|
|
|
212
223
|
client: Lark.Client,
|
|
213
224
|
appToken: string,
|
|
214
225
|
tableId: string,
|
|
215
|
-
fields:
|
|
226
|
+
fields: BitableRecordFields,
|
|
216
227
|
) {
|
|
217
228
|
const res = await client.bitable.appTableRecord.create({
|
|
218
229
|
path: { app_token: appToken, table_id: tableId },
|
|
219
|
-
|
|
220
|
-
data: { fields: fields as any },
|
|
230
|
+
data: { fields },
|
|
221
231
|
});
|
|
222
232
|
ensureLarkSuccess(res, "bitable.appTableRecord.create", { appToken, tableId });
|
|
223
233
|
|
|
@@ -235,6 +245,35 @@ type CleanupLogger = {
|
|
|
235
245
|
/** Default field types created for new Bitable tables (to be cleaned up) */
|
|
236
246
|
const DEFAULT_CLEANUP_FIELD_TYPES = new Set([3, 5, 17]); // SingleSelect, DateTime, Attachment
|
|
237
247
|
|
|
248
|
+
function isDefaultEmptyBitableFieldValue(value: unknown): boolean {
|
|
249
|
+
if (value === undefined || value === null || value === "") {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(value)) {
|
|
253
|
+
return value.every(isDefaultEmptyBitableFieldValue);
|
|
254
|
+
}
|
|
255
|
+
if (typeof value === "object") {
|
|
256
|
+
const record = value as Record<string, unknown>;
|
|
257
|
+
const keys = Object.keys(record);
|
|
258
|
+
if (keys.length === 0) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if ("text" in record && keys.every((key) => key === "text" || key === "type")) {
|
|
262
|
+
return record.text === undefined || record.text === null || record.text === "";
|
|
263
|
+
}
|
|
264
|
+
return Object.values(record).every(isDefaultEmptyBitableFieldValue);
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isPlaceholderBitableRecord(fields: unknown): boolean {
|
|
270
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
const values = Object.values(fields);
|
|
274
|
+
return values.every(isDefaultEmptyBitableFieldValue);
|
|
275
|
+
}
|
|
276
|
+
|
|
238
277
|
/** Clean up default placeholder rows and fields in a newly created Bitable table */
|
|
239
278
|
async function cleanupNewBitable(
|
|
240
279
|
client: Lark.Client,
|
|
@@ -270,7 +309,7 @@ async function cleanupNewBitable(
|
|
|
270
309
|
});
|
|
271
310
|
cleanedFields++;
|
|
272
311
|
} catch (err) {
|
|
273
|
-
logger.debug(`Failed to rename primary field: ${err}`);
|
|
312
|
+
logger.debug(`Failed to rename primary field: ${String(err)}`);
|
|
274
313
|
}
|
|
275
314
|
}
|
|
276
315
|
|
|
@@ -291,7 +330,7 @@ async function cleanupNewBitable(
|
|
|
291
330
|
});
|
|
292
331
|
cleanedFields++;
|
|
293
332
|
} catch (err) {
|
|
294
|
-
logger.debug(`Failed to delete default field ${field.field_name}: ${err}`);
|
|
333
|
+
logger.debug(`Failed to delete default field ${field.field_name}: ${String(err)}`);
|
|
295
334
|
}
|
|
296
335
|
}
|
|
297
336
|
}
|
|
@@ -305,7 +344,7 @@ async function cleanupNewBitable(
|
|
|
305
344
|
|
|
306
345
|
if (recordsRes.code === 0 && recordsRes.data?.items) {
|
|
307
346
|
const emptyRecordIds = recordsRes.data.items
|
|
308
|
-
.filter((r) =>
|
|
347
|
+
.filter((r) => isPlaceholderBitableRecord(r.fields))
|
|
309
348
|
.map((r) => r.record_id)
|
|
310
349
|
.filter((id): id is string => Boolean(id));
|
|
311
350
|
|
|
@@ -325,7 +364,7 @@ async function cleanupNewBitable(
|
|
|
325
364
|
});
|
|
326
365
|
cleanedRows++;
|
|
327
366
|
} catch (err) {
|
|
328
|
-
logger.debug(`Failed to delete empty row ${recordId}: ${err}`);
|
|
367
|
+
logger.debug(`Failed to delete empty row ${recordId}: ${String(err)}`);
|
|
329
368
|
}
|
|
330
369
|
}
|
|
331
370
|
}
|
|
@@ -372,7 +411,7 @@ async function createApp(
|
|
|
372
411
|
}
|
|
373
412
|
}
|
|
374
413
|
} catch (err) {
|
|
375
|
-
log.debug(`Cleanup failed (non-critical): ${err}`);
|
|
414
|
+
log.debug(`Cleanup failed (non-critical): ${String(err)}`);
|
|
376
415
|
}
|
|
377
416
|
|
|
378
417
|
return {
|
|
@@ -424,12 +463,11 @@ async function updateRecord(
|
|
|
424
463
|
appToken: string,
|
|
425
464
|
tableId: string,
|
|
426
465
|
recordId: string,
|
|
427
|
-
fields:
|
|
466
|
+
fields: NonNullable<NonNullable<BitableRecordUpdatePayload["data"]>["fields"]>,
|
|
428
467
|
) {
|
|
429
468
|
const res = await client.bitable.appTableRecord.update({
|
|
430
469
|
path: { app_token: appToken, table_id: tableId, record_id: recordId },
|
|
431
|
-
|
|
432
|
-
data: { fields: fields as any },
|
|
470
|
+
data: { fields },
|
|
433
471
|
});
|
|
434
472
|
ensureLarkSuccess(res, "bitable.appTableRecord.update", { appToken, tableId, recordId });
|
|
435
473
|
|
|
@@ -534,13 +572,11 @@ const UpdateRecordSchema = Type.Object({
|
|
|
534
572
|
|
|
535
573
|
export function registerFeishuBitableTools(api: OpenClawPluginApi) {
|
|
536
574
|
if (!api.config) {
|
|
537
|
-
api.logger.debug?.("feishu_bitable: No config available, skipping bitable tools");
|
|
538
575
|
return;
|
|
539
576
|
}
|
|
540
577
|
|
|
541
578
|
const accounts = listEnabledFeishuAccounts(api.config);
|
|
542
579
|
if (accounts.length === 0) {
|
|
543
|
-
api.logger.debug?.("feishu_bitable: No Feishu accounts configured, skipping bitable tools");
|
|
544
580
|
return;
|
|
545
581
|
}
|
|
546
582
|
|
|
@@ -549,11 +585,14 @@ export function registerFeishuBitableTools(api: OpenClawPluginApi) {
|
|
|
549
585
|
const getClient = (params: AccountAwareParams | undefined, defaultAccountId?: string) =>
|
|
550
586
|
createFeishuToolClient({ api, executeParams: params, defaultAccountId });
|
|
551
587
|
|
|
552
|
-
const registerBitableTool = <
|
|
588
|
+
const registerBitableTool = <
|
|
589
|
+
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Tool params bind each schema-specific executor to its registered tool.
|
|
590
|
+
TParams extends AccountAwareParams,
|
|
591
|
+
>(params: {
|
|
553
592
|
name: string;
|
|
554
593
|
label: string;
|
|
555
594
|
description: string;
|
|
556
|
-
parameters:
|
|
595
|
+
parameters: TSchema;
|
|
557
596
|
execute: (args: { params: TParams; defaultAccountId?: string }) => Promise<unknown>;
|
|
558
597
|
}) => {
|
|
559
598
|
api.registerTool(
|
|
@@ -571,7 +610,7 @@ export function registerFeishuBitableTools(api: OpenClawPluginApi) {
|
|
|
571
610
|
}),
|
|
572
611
|
);
|
|
573
612
|
} catch (err) {
|
|
574
|
-
return json({ error:
|
|
613
|
+
return json({ error: formatErrorMessage(err) });
|
|
575
614
|
}
|
|
576
615
|
},
|
|
577
616
|
}),
|
|
@@ -645,7 +684,7 @@ export function registerFeishuBitableTools(api: OpenClawPluginApi) {
|
|
|
645
684
|
registerBitableTool<{
|
|
646
685
|
app_token: string;
|
|
647
686
|
table_id: string;
|
|
648
|
-
fields:
|
|
687
|
+
fields: BitableRecordFields;
|
|
649
688
|
accountId?: string;
|
|
650
689
|
}>({
|
|
651
690
|
name: "feishu_bitable_create_record",
|
|
@@ -666,7 +705,7 @@ export function registerFeishuBitableTools(api: OpenClawPluginApi) {
|
|
|
666
705
|
app_token: string;
|
|
667
706
|
table_id: string;
|
|
668
707
|
record_id: string;
|
|
669
|
-
fields:
|
|
708
|
+
fields: BitableRecordUpdateFields;
|
|
670
709
|
accountId?: string;
|
|
671
710
|
}>({
|
|
672
711
|
name: "feishu_bitable_update_record",
|
|
@@ -720,6 +759,4 @@ export function registerFeishuBitableTools(api: OpenClawPluginApi) {
|
|
|
720
759
|
);
|
|
721
760
|
},
|
|
722
761
|
});
|
|
723
|
-
|
|
724
|
-
api.logger.info?.("feishu_bitable: Registered bitable tools");
|
|
725
762
|
}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "../runtime-api.js";
|
|
2
|
+
import { buildFeishuConversationId } from "./conversation-id.js";
|
|
3
|
+
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
|
4
|
+
import { downloadMessageResourceFeishu } from "./media.js";
|
|
5
|
+
import { isFeishuBroadcastMention } from "./mention.js";
|
|
6
|
+
import { parsePostContent } from "./post.js";
|
|
7
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
8
|
+
import type { FeishuChatType, FeishuMediaInfo } from "./types.js";
|
|
9
|
+
|
|
10
|
+
type FeishuMention = {
|
|
11
|
+
key: string;
|
|
12
|
+
id: {
|
|
13
|
+
open_id?: string;
|
|
14
|
+
user_id?: string;
|
|
15
|
+
union_id?: string;
|
|
16
|
+
};
|
|
17
|
+
name: string;
|
|
18
|
+
tenant_key?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type FeishuMessageLike = {
|
|
22
|
+
message: {
|
|
23
|
+
content: string;
|
|
24
|
+
message_type: string;
|
|
25
|
+
mentions?: FeishuMention[];
|
|
26
|
+
chat_id: string;
|
|
27
|
+
root_id?: string;
|
|
28
|
+
parent_id?: string;
|
|
29
|
+
thread_id?: string;
|
|
30
|
+
message_id: string;
|
|
31
|
+
};
|
|
32
|
+
sender: {
|
|
33
|
+
sender_id: {
|
|
34
|
+
open_id?: string;
|
|
35
|
+
user_id?: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type GroupSessionScope = "group" | "group_sender" | "group_topic" | "group_topic_sender";
|
|
41
|
+
|
|
42
|
+
type FeishuLogger = (...args: unknown[]) => void;
|
|
43
|
+
|
|
44
|
+
type ResolvedFeishuGroupSession = {
|
|
45
|
+
peerId: string;
|
|
46
|
+
parentPeer: { kind: "group"; id: string } | null;
|
|
47
|
+
groupSessionScope: GroupSessionScope;
|
|
48
|
+
replyInThread: boolean;
|
|
49
|
+
threadReply: boolean;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function resolveFeishuGroupSession(params: {
|
|
53
|
+
chatId: string;
|
|
54
|
+
senderOpenId: string;
|
|
55
|
+
messageId: string;
|
|
56
|
+
rootId?: string;
|
|
57
|
+
threadId?: string;
|
|
58
|
+
chatType?: FeishuChatType;
|
|
59
|
+
groupConfig?: {
|
|
60
|
+
groupSessionScope?: GroupSessionScope;
|
|
61
|
+
topicSessionMode?: "enabled" | "disabled";
|
|
62
|
+
replyInThread?: "enabled" | "disabled";
|
|
63
|
+
};
|
|
64
|
+
feishuCfg?: {
|
|
65
|
+
groupSessionScope?: GroupSessionScope;
|
|
66
|
+
topicSessionMode?: "enabled" | "disabled";
|
|
67
|
+
replyInThread?: "enabled" | "disabled";
|
|
68
|
+
};
|
|
69
|
+
}): ResolvedFeishuGroupSession {
|
|
70
|
+
const { chatId, senderOpenId, messageId, rootId, threadId, chatType, groupConfig, feishuCfg } =
|
|
71
|
+
params;
|
|
72
|
+
const normalizedThreadId = threadId?.trim();
|
|
73
|
+
const normalizedRootId = rootId?.trim();
|
|
74
|
+
const threadReply = Boolean(normalizedThreadId || normalizedRootId);
|
|
75
|
+
const replyInThread =
|
|
76
|
+
(groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled" ||
|
|
77
|
+
threadReply;
|
|
78
|
+
const legacyTopicSessionMode =
|
|
79
|
+
groupConfig?.topicSessionMode ?? feishuCfg?.topicSessionMode ?? "disabled";
|
|
80
|
+
const groupSessionScope: GroupSessionScope =
|
|
81
|
+
groupConfig?.groupSessionScope ??
|
|
82
|
+
feishuCfg?.groupSessionScope ??
|
|
83
|
+
(legacyTopicSessionMode === "enabled" ? "group_topic" : "group");
|
|
84
|
+
const normalizedTopicGroupThreadId =
|
|
85
|
+
chatType === "topic_group" ? (normalizedThreadId ?? normalizedRootId) : undefined;
|
|
86
|
+
const topicScope =
|
|
87
|
+
groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender"
|
|
88
|
+
? (normalizedTopicGroupThreadId ??
|
|
89
|
+
normalizedRootId ??
|
|
90
|
+
normalizedThreadId ??
|
|
91
|
+
(replyInThread ? messageId : null))
|
|
92
|
+
: null;
|
|
93
|
+
|
|
94
|
+
let peerId = chatId;
|
|
95
|
+
switch (groupSessionScope) {
|
|
96
|
+
case "group_sender":
|
|
97
|
+
peerId = buildFeishuConversationId({ chatId, scope: "group_sender", senderOpenId });
|
|
98
|
+
break;
|
|
99
|
+
case "group_topic":
|
|
100
|
+
peerId = topicScope
|
|
101
|
+
? buildFeishuConversationId({ chatId, scope: "group_topic", topicId: topicScope })
|
|
102
|
+
: chatId;
|
|
103
|
+
break;
|
|
104
|
+
case "group_topic_sender":
|
|
105
|
+
peerId = topicScope
|
|
106
|
+
? buildFeishuConversationId({
|
|
107
|
+
chatId,
|
|
108
|
+
scope: "group_topic_sender",
|
|
109
|
+
topicId: topicScope,
|
|
110
|
+
senderOpenId,
|
|
111
|
+
})
|
|
112
|
+
: buildFeishuConversationId({ chatId, scope: "group_sender", senderOpenId });
|
|
113
|
+
break;
|
|
114
|
+
case "group":
|
|
115
|
+
default:
|
|
116
|
+
peerId = chatId;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
peerId,
|
|
122
|
+
parentPeer:
|
|
123
|
+
topicScope &&
|
|
124
|
+
(groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender")
|
|
125
|
+
? { kind: "group", id: chatId }
|
|
126
|
+
: null,
|
|
127
|
+
groupSessionScope,
|
|
128
|
+
replyInThread,
|
|
129
|
+
threadReply,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function parseMessageContent(content: string, messageType: string): string {
|
|
134
|
+
if (messageType === "post") {
|
|
135
|
+
return parsePostContent(content).textContent;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(content);
|
|
140
|
+
if (messageType === "text") {
|
|
141
|
+
return parsed.text || "";
|
|
142
|
+
}
|
|
143
|
+
if (["image", "file", "audio", "video", "media", "sticker"].includes(messageType)) {
|
|
144
|
+
if (messageType === "audio") {
|
|
145
|
+
const speechToText =
|
|
146
|
+
typeof parsed.speech_to_text === "string" ? parsed.speech_to_text.trim() : "";
|
|
147
|
+
if (speechToText) {
|
|
148
|
+
return speechToText;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const placeholder = inferPlaceholder(messageType);
|
|
152
|
+
const fileName = typeof parsed.file_name === "string" ? parsed.file_name.trim() : "";
|
|
153
|
+
return fileName ? `${placeholder} (${fileName})` : placeholder;
|
|
154
|
+
}
|
|
155
|
+
if (messageType === "share_chat") {
|
|
156
|
+
if (parsed && typeof parsed === "object") {
|
|
157
|
+
const share = parsed as { body?: unknown; summary?: unknown; share_chat_id?: unknown };
|
|
158
|
+
if (typeof share.body === "string" && share.body.trim()) {
|
|
159
|
+
return share.body.trim();
|
|
160
|
+
}
|
|
161
|
+
if (typeof share.summary === "string" && share.summary.trim()) {
|
|
162
|
+
return share.summary.trim();
|
|
163
|
+
}
|
|
164
|
+
if (typeof share.share_chat_id === "string" && share.share_chat_id.trim()) {
|
|
165
|
+
return `[Forwarded message: ${share.share_chat_id.trim()}]`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return "[Forwarded message]";
|
|
169
|
+
}
|
|
170
|
+
if (messageType === "merge_forward") {
|
|
171
|
+
return "[Merged and Forwarded Message - loading...]";
|
|
172
|
+
}
|
|
173
|
+
return content;
|
|
174
|
+
} catch {
|
|
175
|
+
return content;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function formatSubMessageContent(content: string, contentType: string): string {
|
|
180
|
+
try {
|
|
181
|
+
const parsed = JSON.parse(content);
|
|
182
|
+
switch (contentType) {
|
|
183
|
+
case "text":
|
|
184
|
+
return parsed.text || content;
|
|
185
|
+
case "post":
|
|
186
|
+
return parsePostContent(content).textContent;
|
|
187
|
+
case "image":
|
|
188
|
+
return "[Image]";
|
|
189
|
+
case "file":
|
|
190
|
+
return `[File: ${parsed.file_name || "unknown"}]`;
|
|
191
|
+
case "audio":
|
|
192
|
+
return "[Audio]";
|
|
193
|
+
case "video":
|
|
194
|
+
return "[Video]";
|
|
195
|
+
case "sticker":
|
|
196
|
+
return "[Sticker]";
|
|
197
|
+
case "merge_forward":
|
|
198
|
+
return "[Nested Merged Forward]";
|
|
199
|
+
default:
|
|
200
|
+
return `[${contentType}]`;
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
return content;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function parseMergeForwardContent(params: { content: string; log?: FeishuLogger }): string {
|
|
208
|
+
const { content, log } = params;
|
|
209
|
+
const maxMessages = 50;
|
|
210
|
+
log?.("feishu: parsing merge_forward sub-messages from API response");
|
|
211
|
+
|
|
212
|
+
let items: Array<{
|
|
213
|
+
message_id?: string;
|
|
214
|
+
msg_type?: string;
|
|
215
|
+
body?: { content?: string };
|
|
216
|
+
sender?: { id?: string };
|
|
217
|
+
upper_message_id?: string;
|
|
218
|
+
create_time?: string;
|
|
219
|
+
}>;
|
|
220
|
+
try {
|
|
221
|
+
items = JSON.parse(content);
|
|
222
|
+
} catch {
|
|
223
|
+
log?.("feishu: merge_forward items parse failed");
|
|
224
|
+
return "[Merged and Forwarded Message - parse error]";
|
|
225
|
+
}
|
|
226
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
227
|
+
return "[Merged and Forwarded Message - no sub-messages]";
|
|
228
|
+
}
|
|
229
|
+
const subMessages = items.filter((item) => item.upper_message_id);
|
|
230
|
+
if (subMessages.length === 0) {
|
|
231
|
+
return "[Merged and Forwarded Message - no sub-messages found]";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
log?.(`feishu: merge_forward contains ${subMessages.length} sub-messages`);
|
|
235
|
+
subMessages.sort(
|
|
236
|
+
(a, b) => Number.parseInt(a.create_time || "0", 10) - Number.parseInt(b.create_time || "0", 10),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const lines = ["[Merged and Forwarded Messages]"];
|
|
240
|
+
for (const item of subMessages.slice(0, maxMessages)) {
|
|
241
|
+
lines.push(`- ${formatSubMessageContent(item.body?.content || "", item.msg_type || "text")}`);
|
|
242
|
+
}
|
|
243
|
+
if (subMessages.length > maxMessages) {
|
|
244
|
+
lines.push(`... and ${subMessages.length - maxMessages} more messages`);
|
|
245
|
+
}
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function checkBotMentioned(event: FeishuMessageLike, botOpenId?: string): boolean {
|
|
250
|
+
if (!botOpenId) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const mentions = event.message.mentions ?? [];
|
|
254
|
+
if (mentions.length > 0) {
|
|
255
|
+
return mentions.some(
|
|
256
|
+
(mention) => !isFeishuBroadcastMention(mention) && mention.id.open_id === botOpenId,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (event.message.message_type === "post") {
|
|
260
|
+
return parsePostContent(event.message.content).mentionedOpenIds.some(
|
|
261
|
+
(id) => id.trim().toLowerCase() !== "all" && id === botOpenId,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function normalizeMentions(
|
|
268
|
+
text: string,
|
|
269
|
+
mentions?: FeishuMention[],
|
|
270
|
+
botStripId?: string,
|
|
271
|
+
): string {
|
|
272
|
+
if (!mentions || mentions.length === 0) {
|
|
273
|
+
return text;
|
|
274
|
+
}
|
|
275
|
+
const escaped = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
276
|
+
const escapeName = (value: string) => value.replace(/</g, "<").replace(/>/g, ">");
|
|
277
|
+
let result = text;
|
|
278
|
+
for (const mention of mentions) {
|
|
279
|
+
const mentionId = mention.id.open_id;
|
|
280
|
+
const replacement =
|
|
281
|
+
botStripId && mentionId === botStripId
|
|
282
|
+
? ""
|
|
283
|
+
: mentionId
|
|
284
|
+
? `<at user_id="${mentionId}">${escapeName(mention.name)}</at>`
|
|
285
|
+
: `@${mention.name}`;
|
|
286
|
+
result = result.replace(new RegExp(escaped(mention.key), "g"), () => replacement).trim();
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function normalizeFeishuCommandProbeBody(text: string): string {
|
|
292
|
+
if (!text) {
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
return text
|
|
296
|
+
.replace(/<at\b[^>]*>[^<]*<\/at>/giu, " ")
|
|
297
|
+
.replace(/(^|\s)@[^/\s]+(?=\s|$|\/)/gu, "$1")
|
|
298
|
+
.replace(/\s+/g, " ")
|
|
299
|
+
.trim();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseMediaKeys(
|
|
303
|
+
content: string,
|
|
304
|
+
messageType: string,
|
|
305
|
+
): { imageKey?: string; fileKey?: string; fileName?: string } {
|
|
306
|
+
try {
|
|
307
|
+
const parsed = JSON.parse(content);
|
|
308
|
+
const imageKey = normalizeFeishuExternalKey(parsed.image_key);
|
|
309
|
+
const fileKey = normalizeFeishuExternalKey(parsed.file_key);
|
|
310
|
+
switch (messageType) {
|
|
311
|
+
case "image":
|
|
312
|
+
return { imageKey, fileName: parsed.file_name };
|
|
313
|
+
case "file":
|
|
314
|
+
case "audio":
|
|
315
|
+
case "sticker":
|
|
316
|
+
return { fileKey, fileName: parsed.file_name };
|
|
317
|
+
case "video":
|
|
318
|
+
case "media":
|
|
319
|
+
return { fileKey, imageKey, fileName: parsed.file_name };
|
|
320
|
+
default:
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function toMessageResourceType(messageType: string): "image" | "file" {
|
|
329
|
+
return messageType === "image" ? "image" : "file";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function inferPlaceholder(messageType: string): string {
|
|
333
|
+
switch (messageType) {
|
|
334
|
+
case "image":
|
|
335
|
+
return "<media:image>";
|
|
336
|
+
case "file":
|
|
337
|
+
return "<media:document>";
|
|
338
|
+
case "audio":
|
|
339
|
+
return "<media:audio>";
|
|
340
|
+
case "video":
|
|
341
|
+
case "media":
|
|
342
|
+
return "<media:video>";
|
|
343
|
+
case "sticker":
|
|
344
|
+
return "<media:sticker>";
|
|
345
|
+
default:
|
|
346
|
+
return "<media:document>";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function resolveFeishuMediaList(params: {
|
|
351
|
+
cfg: ClawdbotConfig;
|
|
352
|
+
messageId: string;
|
|
353
|
+
messageType: string;
|
|
354
|
+
content: string;
|
|
355
|
+
maxBytes: number;
|
|
356
|
+
log?: (msg: string) => void;
|
|
357
|
+
accountId?: string;
|
|
358
|
+
}): Promise<FeishuMediaInfo[]> {
|
|
359
|
+
const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
|
|
360
|
+
const mediaTypes = ["image", "file", "audio", "video", "media", "sticker", "post"];
|
|
361
|
+
if (!mediaTypes.includes(messageType)) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const out: FeishuMediaInfo[] = [];
|
|
366
|
+
const core = getFeishuRuntime();
|
|
367
|
+
|
|
368
|
+
if (messageType === "post") {
|
|
369
|
+
const { imageKeys, mediaKeys } = parsePostContent(content);
|
|
370
|
+
if (imageKeys.length === 0 && mediaKeys.length === 0) {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
if (imageKeys.length > 0) {
|
|
374
|
+
log?.(`feishu: post message contains ${imageKeys.length} embedded image(s)`);
|
|
375
|
+
}
|
|
376
|
+
if (mediaKeys.length > 0) {
|
|
377
|
+
log?.(`feishu: post message contains ${mediaKeys.length} embedded media file(s)`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const imageKey of imageKeys) {
|
|
381
|
+
try {
|
|
382
|
+
const result = await downloadMessageResourceFeishu({
|
|
383
|
+
cfg,
|
|
384
|
+
messageId,
|
|
385
|
+
fileKey: imageKey,
|
|
386
|
+
type: "image",
|
|
387
|
+
accountId,
|
|
388
|
+
});
|
|
389
|
+
const contentType =
|
|
390
|
+
result.contentType ?? (await core.media.detectMime({ buffer: result.buffer }));
|
|
391
|
+
const saved = await core.channel.media.saveMediaBuffer(
|
|
392
|
+
result.buffer,
|
|
393
|
+
contentType,
|
|
394
|
+
"inbound",
|
|
395
|
+
maxBytes,
|
|
396
|
+
);
|
|
397
|
+
out.push({
|
|
398
|
+
path: saved.path,
|
|
399
|
+
contentType: saved.contentType,
|
|
400
|
+
placeholder: "<media:image>",
|
|
401
|
+
});
|
|
402
|
+
log?.(`feishu: downloaded embedded image ${imageKey}, saved to ${saved.path}`);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
log?.(`feishu: failed to download embedded image ${imageKey}: ${String(err)}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const media of mediaKeys) {
|
|
409
|
+
try {
|
|
410
|
+
const result = await downloadMessageResourceFeishu({
|
|
411
|
+
cfg,
|
|
412
|
+
messageId,
|
|
413
|
+
fileKey: media.fileKey,
|
|
414
|
+
type: "file",
|
|
415
|
+
accountId,
|
|
416
|
+
});
|
|
417
|
+
const contentType =
|
|
418
|
+
result.contentType ?? (await core.media.detectMime({ buffer: result.buffer }));
|
|
419
|
+
const saved = await core.channel.media.saveMediaBuffer(
|
|
420
|
+
result.buffer,
|
|
421
|
+
contentType,
|
|
422
|
+
"inbound",
|
|
423
|
+
maxBytes,
|
|
424
|
+
);
|
|
425
|
+
out.push({
|
|
426
|
+
path: saved.path,
|
|
427
|
+
contentType: saved.contentType,
|
|
428
|
+
placeholder: "<media:video>",
|
|
429
|
+
});
|
|
430
|
+
log?.(`feishu: downloaded embedded media ${media.fileKey}, saved to ${saved.path}`);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
log?.(`feishu: failed to download embedded media ${media.fileKey}: ${String(err)}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const mediaKeys = parseMediaKeys(content, messageType);
|
|
439
|
+
if (!mediaKeys.imageKey && !mediaKeys.fileKey) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
|
|
445
|
+
if (!fileKey) {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
const result = await downloadMessageResourceFeishu({
|
|
449
|
+
cfg,
|
|
450
|
+
messageId,
|
|
451
|
+
fileKey,
|
|
452
|
+
type: toMessageResourceType(messageType),
|
|
453
|
+
accountId,
|
|
454
|
+
});
|
|
455
|
+
const contentType =
|
|
456
|
+
result.contentType ?? (await core.media.detectMime({ buffer: result.buffer }));
|
|
457
|
+
const saved = await core.channel.media.saveMediaBuffer(
|
|
458
|
+
result.buffer,
|
|
459
|
+
contentType,
|
|
460
|
+
"inbound",
|
|
461
|
+
maxBytes,
|
|
462
|
+
result.fileName || mediaKeys.fileName,
|
|
463
|
+
);
|
|
464
|
+
out.push({
|
|
465
|
+
path: saved.path,
|
|
466
|
+
contentType: saved.contentType,
|
|
467
|
+
placeholder: inferPlaceholder(messageType),
|
|
468
|
+
});
|
|
469
|
+
log?.(`feishu: downloaded ${messageType} media, saved to ${saved.path}`);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
log?.(`feishu: failed to download ${messageType} media: ${String(err)}`);
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
}
|