@m1heng-clawd/feishu 0.1.7 → 0.1.9
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/README.md +180 -8
- package/package.json +3 -3
- package/src/accounts.ts +100 -13
- package/src/bot.ts +124 -28
- package/src/channel.ts +163 -53
- package/src/client.ts +83 -35
- package/src/config-schema.ts +73 -1
- package/src/directory.ts +15 -9
- package/src/docx.ts +16 -6
- package/src/drive.ts +12 -6
- package/src/dynamic-agent.ts +131 -0
- package/src/media.ts +43 -35
- package/src/monitor.ts +215 -70
- package/src/outbound.ts +7 -7
- package/src/perm.ts +12 -6
- package/src/probe.ts +6 -9
- package/src/reactions.ts +19 -16
- package/src/reply-dispatcher.ts +19 -11
- package/src/send.ts +49 -43
- package/src/types.ts +16 -2
- package/src/typing.ts +11 -9
- package/src/wiki.ts +12 -6
package/src/bot.ts
CHANGED
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
7
7
|
type HistoryEntry,
|
|
8
8
|
} from "openclaw/plugin-sdk";
|
|
9
|
-
import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo } from "./types.js";
|
|
9
|
+
import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
|
|
10
10
|
import { getFeishuRuntime } from "./runtime.js";
|
|
11
11
|
import { createFeishuClient } from "./client.js";
|
|
12
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
12
13
|
import {
|
|
13
14
|
resolveFeishuGroupConfig,
|
|
14
15
|
resolveFeishuReplyPolicy,
|
|
@@ -23,6 +24,39 @@ import {
|
|
|
23
24
|
extractMessageBody,
|
|
24
25
|
isMentionForwardRequest,
|
|
25
26
|
} from "./mention.js";
|
|
27
|
+
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
|
28
|
+
import type { DynamicAgentCreationConfig } from "./types.js";
|
|
29
|
+
|
|
30
|
+
// --- Message deduplication ---
|
|
31
|
+
// Prevent duplicate processing when WebSocket reconnects or Feishu redelivers messages.
|
|
32
|
+
const DEDUP_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
33
|
+
const DEDUP_MAX_SIZE = 1_000;
|
|
34
|
+
const DEDUP_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // cleanup every 5 minutes
|
|
35
|
+
const processedMessageIds = new Map<string, number>(); // messageId -> timestamp
|
|
36
|
+
let lastCleanupTime = Date.now();
|
|
37
|
+
|
|
38
|
+
function tryRecordMessage(messageId: string): boolean {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
|
|
41
|
+
// Throttled cleanup: evict expired entries at most once per interval
|
|
42
|
+
if (now - lastCleanupTime > DEDUP_CLEANUP_INTERVAL_MS) {
|
|
43
|
+
for (const [id, ts] of processedMessageIds) {
|
|
44
|
+
if (now - ts > DEDUP_TTL_MS) processedMessageIds.delete(id);
|
|
45
|
+
}
|
|
46
|
+
lastCleanupTime = now;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (processedMessageIds.has(messageId)) return false;
|
|
50
|
+
|
|
51
|
+
// Evict oldest entries if cache is full
|
|
52
|
+
if (processedMessageIds.size >= DEDUP_MAX_SIZE) {
|
|
53
|
+
const first = processedMessageIds.keys().next().value!;
|
|
54
|
+
processedMessageIds.delete(first);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
processedMessageIds.set(messageId, now);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
26
60
|
|
|
27
61
|
// --- Permission error extraction ---
|
|
28
62
|
// Extract permission grant URL from Feishu API error response.
|
|
@@ -51,7 +85,7 @@ function extractPermissionError(err: unknown): PermissionError | null {
|
|
|
51
85
|
|
|
52
86
|
// Extract the grant URL from the error message (contains the direct link)
|
|
53
87
|
const msg = feishuErr.msg ?? "";
|
|
54
|
-
const urlMatch = msg.match(/https:\/\/
|
|
88
|
+
const urlMatch = msg.match(/https:\/\/[^\s,]+\/app\/[^\s,]+/);
|
|
55
89
|
const grantUrl = urlMatch?.[0];
|
|
56
90
|
|
|
57
91
|
return {
|
|
@@ -77,12 +111,12 @@ type SenderNameResult = {
|
|
|
77
111
|
};
|
|
78
112
|
|
|
79
113
|
async function resolveFeishuSenderName(params: {
|
|
80
|
-
|
|
114
|
+
account: ResolvedFeishuAccount;
|
|
81
115
|
senderOpenId: string;
|
|
82
116
|
log: (...args: any[]) => void;
|
|
83
117
|
}): Promise<SenderNameResult> {
|
|
84
|
-
const {
|
|
85
|
-
if (!
|
|
118
|
+
const { account, senderOpenId, log } = params;
|
|
119
|
+
if (!account.configured) return {};
|
|
86
120
|
if (!senderOpenId) return {};
|
|
87
121
|
|
|
88
122
|
const cached = senderNameCache.get(senderOpenId);
|
|
@@ -90,7 +124,7 @@ async function resolveFeishuSenderName(params: {
|
|
|
90
124
|
if (cached && cached.expireAt > now) return { name: cached.name };
|
|
91
125
|
|
|
92
126
|
try {
|
|
93
|
-
const client = createFeishuClient(
|
|
127
|
+
const client = createFeishuClient(account);
|
|
94
128
|
|
|
95
129
|
// contact/v3/users/:user_id?user_id_type=open_id
|
|
96
130
|
const res: any = await client.contact.user.get({
|
|
@@ -308,8 +342,9 @@ async function resolveFeishuMediaList(params: {
|
|
|
308
342
|
content: string;
|
|
309
343
|
maxBytes: number;
|
|
310
344
|
log?: (msg: string) => void;
|
|
345
|
+
accountId?: string;
|
|
311
346
|
}): Promise<FeishuMediaInfo[]> {
|
|
312
|
-
const { cfg, messageId, messageType, content, maxBytes, log } = params;
|
|
347
|
+
const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
|
|
313
348
|
|
|
314
349
|
// Only process media message types (including post for embedded images)
|
|
315
350
|
const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
|
|
@@ -337,6 +372,7 @@ async function resolveFeishuMediaList(params: {
|
|
|
337
372
|
messageId,
|
|
338
373
|
fileKey: imageKey,
|
|
339
374
|
type: "image",
|
|
375
|
+
accountId,
|
|
340
376
|
});
|
|
341
377
|
|
|
342
378
|
let contentType = result.contentType;
|
|
@@ -390,6 +426,7 @@ async function resolveFeishuMediaList(params: {
|
|
|
390
426
|
messageId,
|
|
391
427
|
fileKey,
|
|
392
428
|
type: resourceType,
|
|
429
|
+
accountId,
|
|
393
430
|
});
|
|
394
431
|
buffer = result.buffer;
|
|
395
432
|
contentType = result.contentType;
|
|
@@ -491,18 +528,30 @@ export async function handleFeishuMessage(params: {
|
|
|
491
528
|
botOpenId?: string;
|
|
492
529
|
runtime?: RuntimeEnv;
|
|
493
530
|
chatHistories?: Map<string, HistoryEntry[]>;
|
|
531
|
+
accountId?: string;
|
|
494
532
|
}): Promise<void> {
|
|
495
|
-
const { cfg, event, botOpenId, runtime, chatHistories } = params;
|
|
496
|
-
|
|
533
|
+
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
|
|
534
|
+
|
|
535
|
+
// Resolve account with merged config
|
|
536
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
537
|
+
const feishuCfg = account.config;
|
|
538
|
+
|
|
497
539
|
const log = runtime?.log ?? console.log;
|
|
498
540
|
const error = runtime?.error ?? console.error;
|
|
499
541
|
|
|
542
|
+
// Dedup check: skip if this message was already processed
|
|
543
|
+
const messageId = event.message.message_id;
|
|
544
|
+
if (!tryRecordMessage(messageId)) {
|
|
545
|
+
log(`feishu: skipping duplicate message ${messageId}`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
500
549
|
let ctx = parseFeishuMessageEvent(event, botOpenId);
|
|
501
550
|
const isGroup = ctx.chatType === "group";
|
|
502
551
|
|
|
503
552
|
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
|
|
504
553
|
const senderResult = await resolveFeishuSenderName({
|
|
505
|
-
|
|
554
|
+
account,
|
|
506
555
|
senderOpenId: ctx.senderOpenId,
|
|
507
556
|
log,
|
|
508
557
|
});
|
|
@@ -511,7 +560,7 @@ export async function handleFeishuMessage(params: {
|
|
|
511
560
|
// Track permission error to inform agent later (with cooldown to avoid repetition)
|
|
512
561
|
let permissionErrorForAgent: PermissionError | undefined;
|
|
513
562
|
if (senderResult.permissionError) {
|
|
514
|
-
const appKey =
|
|
563
|
+
const appKey = account.appId ?? "default";
|
|
515
564
|
const now = Date.now();
|
|
516
565
|
const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
|
|
517
566
|
|
|
@@ -521,12 +570,12 @@ export async function handleFeishuMessage(params: {
|
|
|
521
570
|
}
|
|
522
571
|
}
|
|
523
572
|
|
|
524
|
-
log(`feishu: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
|
|
573
|
+
log(`feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
|
|
525
574
|
|
|
526
575
|
// Log mention targets if detected
|
|
527
576
|
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
|
528
577
|
const names = ctx.mentionTargets.map((t) => t.name).join(", ");
|
|
529
|
-
log(`feishu: detected @ forward request, targets: [${names}]`);
|
|
578
|
+
log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
|
|
530
579
|
}
|
|
531
580
|
|
|
532
581
|
const historyLimit = Math.max(
|
|
@@ -537,6 +586,7 @@ export async function handleFeishuMessage(params: {
|
|
|
537
586
|
if (isGroup) {
|
|
538
587
|
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
|
539
588
|
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
|
589
|
+
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
|
|
540
590
|
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
|
|
541
591
|
|
|
542
592
|
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
|
|
@@ -548,7 +598,7 @@ export async function handleFeishuMessage(params: {
|
|
|
548
598
|
});
|
|
549
599
|
|
|
550
600
|
if (!groupAllowed) {
|
|
551
|
-
log(`feishu:
|
|
601
|
+
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in group allowlist`);
|
|
552
602
|
return;
|
|
553
603
|
}
|
|
554
604
|
|
|
@@ -574,7 +624,7 @@ export async function handleFeishuMessage(params: {
|
|
|
574
624
|
});
|
|
575
625
|
|
|
576
626
|
if (requireMention && !ctx.mentionedBot) {
|
|
577
|
-
log(`feishu: message in group ${ctx.chatId} did not mention bot, recording to history`);
|
|
627
|
+
log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot, recording to history`);
|
|
578
628
|
if (chatHistories) {
|
|
579
629
|
recordPendingHistoryEntryIfEnabled({
|
|
580
630
|
historyMap: chatHistories,
|
|
@@ -600,7 +650,7 @@ export async function handleFeishuMessage(params: {
|
|
|
600
650
|
senderId: ctx.senderOpenId,
|
|
601
651
|
});
|
|
602
652
|
if (!match.allowed) {
|
|
603
|
-
log(`feishu: sender ${ctx.senderOpenId} not in DM allowlist`);
|
|
653
|
+
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in DM allowlist`);
|
|
604
654
|
return;
|
|
605
655
|
}
|
|
606
656
|
}
|
|
@@ -614,19 +664,62 @@ export async function handleFeishuMessage(params: {
|
|
|
614
664
|
const feishuFrom = `feishu:${ctx.senderOpenId}`;
|
|
615
665
|
const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
|
|
616
666
|
|
|
617
|
-
|
|
667
|
+
// Resolve peer ID for session routing
|
|
668
|
+
// When topicSessionMode is enabled, messages within a topic (identified by root_id)
|
|
669
|
+
// get a separate session from the main group chat.
|
|
670
|
+
let peerId = isGroup ? ctx.chatId : ctx.senderOpenId;
|
|
671
|
+
if (isGroup && ctx.rootId) {
|
|
672
|
+
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
|
|
673
|
+
const topicSessionMode = groupConfig?.topicSessionMode ?? feishuCfg?.topicSessionMode ?? "disabled";
|
|
674
|
+
if (topicSessionMode === "enabled") {
|
|
675
|
+
// Use chatId:topic:rootId as peer ID for topic-scoped sessions
|
|
676
|
+
peerId = `${ctx.chatId}:topic:${ctx.rootId}`;
|
|
677
|
+
log(`feishu[${account.accountId}]: topic session isolation enabled, peer=${peerId}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let route = core.channel.routing.resolveAgentRoute({
|
|
618
682
|
cfg,
|
|
619
683
|
channel: "feishu",
|
|
684
|
+
accountId: account.accountId,
|
|
620
685
|
peer: {
|
|
621
|
-
kind: isGroup ? "group" : "
|
|
622
|
-
id:
|
|
686
|
+
kind: isGroup ? "group" : "direct",
|
|
687
|
+
id: peerId,
|
|
623
688
|
},
|
|
624
689
|
});
|
|
625
690
|
|
|
691
|
+
// Dynamic agent creation for DM users
|
|
692
|
+
// When enabled, creates a unique agent instance with its own workspace for each DM user.
|
|
693
|
+
let effectiveCfg = cfg;
|
|
694
|
+
if (!isGroup && route.matchedBy === "default") {
|
|
695
|
+
const dynamicCfg = feishuCfg?.dynamicAgentCreation as DynamicAgentCreationConfig | undefined;
|
|
696
|
+
if (dynamicCfg?.enabled) {
|
|
697
|
+
const runtime = getFeishuRuntime();
|
|
698
|
+
const result = await maybeCreateDynamicAgent({
|
|
699
|
+
cfg,
|
|
700
|
+
runtime,
|
|
701
|
+
senderOpenId: ctx.senderOpenId,
|
|
702
|
+
dynamicCfg,
|
|
703
|
+
log: (msg) => log(msg),
|
|
704
|
+
});
|
|
705
|
+
if (result.created) {
|
|
706
|
+
effectiveCfg = result.updatedCfg;
|
|
707
|
+
// Re-resolve route with updated config
|
|
708
|
+
route = core.channel.routing.resolveAgentRoute({
|
|
709
|
+
cfg: result.updatedCfg,
|
|
710
|
+
channel: "feishu",
|
|
711
|
+
accountId: account.accountId,
|
|
712
|
+
peer: { kind: "dm", id: ctx.senderOpenId },
|
|
713
|
+
});
|
|
714
|
+
log(`feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
626
719
|
const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
|
|
627
720
|
const inboundLabel = isGroup
|
|
628
|
-
? `Feishu message in group ${ctx.chatId}`
|
|
629
|
-
: `Feishu DM from ${ctx.senderOpenId}`;
|
|
721
|
+
? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
|
|
722
|
+
: `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
|
|
630
723
|
|
|
631
724
|
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
|
632
725
|
sessionKey: route.sessionKey,
|
|
@@ -642,6 +735,7 @@ export async function handleFeishuMessage(params: {
|
|
|
642
735
|
content: event.message.content,
|
|
643
736
|
maxBytes: mediaMaxBytes,
|
|
644
737
|
log,
|
|
738
|
+
accountId: account.accountId,
|
|
645
739
|
});
|
|
646
740
|
const mediaPayload = buildFeishuMediaPayload(mediaList);
|
|
647
741
|
|
|
@@ -649,13 +743,13 @@ export async function handleFeishuMessage(params: {
|
|
|
649
743
|
let quotedContent: string | undefined;
|
|
650
744
|
if (ctx.parentId) {
|
|
651
745
|
try {
|
|
652
|
-
const quotedMsg = await getMessageFeishu({ cfg, messageId: ctx.parentId });
|
|
746
|
+
const quotedMsg = await getMessageFeishu({ cfg, messageId: ctx.parentId, accountId: account.accountId });
|
|
653
747
|
if (quotedMsg) {
|
|
654
748
|
quotedContent = quotedMsg.content;
|
|
655
|
-
log(`feishu: fetched quoted message: ${quotedContent?.slice(0, 100)}`);
|
|
749
|
+
log(`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`);
|
|
656
750
|
}
|
|
657
751
|
} catch (err) {
|
|
658
|
-
log(`feishu: failed to fetch quoted message: ${String(err)}`);
|
|
752
|
+
log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
|
|
659
753
|
}
|
|
660
754
|
}
|
|
661
755
|
|
|
@@ -722,9 +816,10 @@ export async function handleFeishuMessage(params: {
|
|
|
722
816
|
runtime: runtime as RuntimeEnv,
|
|
723
817
|
chatId: ctx.chatId,
|
|
724
818
|
replyToMessageId: ctx.messageId,
|
|
819
|
+
accountId: account.accountId,
|
|
725
820
|
});
|
|
726
821
|
|
|
727
|
-
log(`feishu: dispatching permission error notification to agent`);
|
|
822
|
+
log(`feishu[${account.accountId}]: dispatching permission error notification to agent`);
|
|
728
823
|
|
|
729
824
|
await core.channel.reply.dispatchReplyFromConfig({
|
|
730
825
|
ctx: permissionCtx,
|
|
@@ -795,9 +890,10 @@ export async function handleFeishuMessage(params: {
|
|
|
795
890
|
chatId: ctx.chatId,
|
|
796
891
|
replyToMessageId: ctx.messageId,
|
|
797
892
|
mentionTargets: ctx.mentionTargets,
|
|
893
|
+
accountId: account.accountId,
|
|
798
894
|
});
|
|
799
895
|
|
|
800
|
-
log(`feishu: dispatching to agent (session=${route.sessionKey})`);
|
|
896
|
+
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
|
|
801
897
|
|
|
802
898
|
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
|
803
899
|
ctx: ctxPayload,
|
|
@@ -816,8 +912,8 @@ export async function handleFeishuMessage(params: {
|
|
|
816
912
|
});
|
|
817
913
|
}
|
|
818
914
|
|
|
819
|
-
log(`feishu: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
|
|
915
|
+
log(`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
|
|
820
916
|
} catch (err) {
|
|
821
|
-
error(`feishu: failed to dispatch message: ${String(err)}`);
|
|
917
|
+
error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
|
|
822
918
|
}
|
|
823
919
|
}
|
package/src/channel.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
|
3
3
|
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveFeishuAccount,
|
|
6
|
+
resolveFeishuCredentials,
|
|
7
|
+
listFeishuAccountIds,
|
|
8
|
+
resolveDefaultFeishuAccountId,
|
|
9
|
+
} from "./accounts.js";
|
|
5
10
|
import { feishuOutbound } from "./outbound.js";
|
|
6
11
|
import { probeFeishu } from "./probe.js";
|
|
7
12
|
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
|
@@ -34,11 +39,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
34
39
|
pairing: {
|
|
35
40
|
idLabel: "feishuUserId",
|
|
36
41
|
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
|
|
37
|
-
notifyApproval: async ({ cfg, id }) => {
|
|
42
|
+
notifyApproval: async ({ cfg, id, accountId }) => {
|
|
38
43
|
await sendMessageFeishu({
|
|
39
44
|
cfg,
|
|
40
45
|
to: id,
|
|
41
46
|
text: PAIRING_APPROVED_MESSAGE,
|
|
47
|
+
accountId,
|
|
42
48
|
});
|
|
43
49
|
},
|
|
44
50
|
},
|
|
@@ -71,7 +77,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
71
77
|
appSecret: { type: "string" },
|
|
72
78
|
encryptKey: { type: "string" },
|
|
73
79
|
verificationToken: { type: "string" },
|
|
74
|
-
domain: {
|
|
80
|
+
domain: {
|
|
81
|
+
oneOf: [
|
|
82
|
+
{ type: "string", enum: ["feishu", "lark"] },
|
|
83
|
+
{ type: "string", format: "uri", pattern: "^https://" },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
75
86
|
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
76
87
|
webhookPath: { type: "string" },
|
|
77
88
|
webhookPort: { type: "integer", minimum: 1 },
|
|
@@ -80,49 +91,118 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
80
91
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
81
92
|
groupAllowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
82
93
|
requireMention: { type: "boolean" },
|
|
94
|
+
topicSessionMode: { type: "string", enum: ["disabled", "enabled"] },
|
|
83
95
|
historyLimit: { type: "integer", minimum: 0 },
|
|
84
96
|
dmHistoryLimit: { type: "integer", minimum: 0 },
|
|
85
97
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
86
98
|
chunkMode: { type: "string", enum: ["length", "newline"] },
|
|
87
99
|
mediaMaxMb: { type: "number", minimum: 0 },
|
|
88
100
|
renderMode: { type: "string", enum: ["auto", "raw", "card"] },
|
|
101
|
+
accounts: {
|
|
102
|
+
type: "object",
|
|
103
|
+
additionalProperties: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
enabled: { type: "boolean" },
|
|
107
|
+
name: { type: "string" },
|
|
108
|
+
appId: { type: "string" },
|
|
109
|
+
appSecret: { type: "string" },
|
|
110
|
+
encryptKey: { type: "string" },
|
|
111
|
+
verificationToken: { type: "string" },
|
|
112
|
+
domain: { type: "string", enum: ["feishu", "lark"] },
|
|
113
|
+
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
89
117
|
},
|
|
90
118
|
},
|
|
91
119
|
},
|
|
92
120
|
config: {
|
|
93
|
-
listAccountIds: () =>
|
|
94
|
-
resolveAccount: (cfg) => resolveFeishuAccount({ cfg }),
|
|
95
|
-
defaultAccountId: () =>
|
|
96
|
-
setAccountEnabled: ({ cfg, enabled }) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
|
122
|
+
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
|
123
|
+
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
|
124
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
125
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
126
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
127
|
+
|
|
128
|
+
if (isDefault) {
|
|
129
|
+
// For default account, set top-level enabled
|
|
130
|
+
return {
|
|
131
|
+
...cfg,
|
|
132
|
+
channels: {
|
|
133
|
+
...cfg.channels,
|
|
134
|
+
feishu: {
|
|
135
|
+
...cfg.channels?.feishu,
|
|
136
|
+
enabled,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// For named accounts, set enabled in accounts[accountId]
|
|
143
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
144
|
+
return {
|
|
145
|
+
...cfg,
|
|
146
|
+
channels: {
|
|
147
|
+
...cfg.channels,
|
|
148
|
+
feishu: {
|
|
149
|
+
...feishuCfg,
|
|
150
|
+
accounts: {
|
|
151
|
+
...feishuCfg?.accounts,
|
|
152
|
+
[accountId]: {
|
|
153
|
+
...feishuCfg?.accounts?.[accountId],
|
|
154
|
+
enabled,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
103
158
|
},
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
deleteAccount: ({ cfg }) => {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
next
|
|
112
|
-
|
|
113
|
-
delete
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
deleteAccount: ({ cfg, accountId }) => {
|
|
162
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
163
|
+
|
|
164
|
+
if (isDefault) {
|
|
165
|
+
// Delete entire feishu config
|
|
166
|
+
const next = { ...cfg } as ClawdbotConfig;
|
|
167
|
+
const nextChannels = { ...cfg.channels };
|
|
168
|
+
delete (nextChannels as Record<string, unknown>).feishu;
|
|
169
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
170
|
+
next.channels = nextChannels;
|
|
171
|
+
} else {
|
|
172
|
+
delete next.channels;
|
|
173
|
+
}
|
|
174
|
+
return next;
|
|
114
175
|
}
|
|
115
|
-
|
|
176
|
+
|
|
177
|
+
// Delete specific account from accounts
|
|
178
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
179
|
+
const accounts = { ...feishuCfg?.accounts };
|
|
180
|
+
delete accounts[accountId];
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
...cfg,
|
|
184
|
+
channels: {
|
|
185
|
+
...cfg.channels,
|
|
186
|
+
feishu: {
|
|
187
|
+
...feishuCfg,
|
|
188
|
+
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
116
192
|
},
|
|
117
|
-
isConfigured: (
|
|
118
|
-
Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)),
|
|
193
|
+
isConfigured: (account) => account.configured,
|
|
119
194
|
describeAccount: (account) => ({
|
|
120
195
|
accountId: account.accountId,
|
|
121
196
|
enabled: account.enabled,
|
|
122
197
|
configured: account.configured,
|
|
198
|
+
name: account.name,
|
|
199
|
+
appId: account.appId,
|
|
200
|
+
domain: account.domain,
|
|
123
201
|
}),
|
|
124
|
-
resolveAllowFrom: ({ cfg }) =>
|
|
125
|
-
|
|
202
|
+
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
203
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
204
|
+
return account.config?.allowFrom ?? [];
|
|
205
|
+
},
|
|
126
206
|
formatAllowFrom: ({ allowFrom }) =>
|
|
127
207
|
allowFrom
|
|
128
208
|
.map((entry) => String(entry).trim())
|
|
@@ -130,28 +210,53 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
130
210
|
.map((entry) => entry.toLowerCase()),
|
|
131
211
|
},
|
|
132
212
|
security: {
|
|
133
|
-
collectWarnings: ({ cfg }) => {
|
|
134
|
-
const
|
|
213
|
+
collectWarnings: ({ cfg, accountId }) => {
|
|
214
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
215
|
+
const feishuCfg = account.config;
|
|
135
216
|
const defaultGroupPolicy = (cfg.channels as Record<string, { groupPolicy?: string }> | undefined)?.defaults?.groupPolicy;
|
|
136
217
|
const groupPolicy = feishuCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
137
218
|
if (groupPolicy !== "open") return [];
|
|
138
219
|
return [
|
|
139
|
-
`- Feishu groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
|
|
220
|
+
`- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
|
|
140
221
|
];
|
|
141
222
|
},
|
|
142
223
|
},
|
|
143
224
|
setup: {
|
|
144
225
|
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
145
|
-
applyAccountConfig: ({ cfg }) =>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
...cfg
|
|
151
|
-
|
|
226
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
227
|
+
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
228
|
+
|
|
229
|
+
if (isDefault) {
|
|
230
|
+
return {
|
|
231
|
+
...cfg,
|
|
232
|
+
channels: {
|
|
233
|
+
...cfg.channels,
|
|
234
|
+
feishu: {
|
|
235
|
+
...cfg.channels?.feishu,
|
|
236
|
+
enabled: true,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
243
|
+
return {
|
|
244
|
+
...cfg,
|
|
245
|
+
channels: {
|
|
246
|
+
...cfg.channels,
|
|
247
|
+
feishu: {
|
|
248
|
+
...feishuCfg,
|
|
249
|
+
accounts: {
|
|
250
|
+
...feishuCfg?.accounts,
|
|
251
|
+
[accountId]: {
|
|
252
|
+
...feishuCfg?.accounts?.[accountId],
|
|
253
|
+
enabled: true,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
152
257
|
},
|
|
153
|
-
}
|
|
154
|
-
}
|
|
258
|
+
};
|
|
259
|
+
},
|
|
155
260
|
},
|
|
156
261
|
onboarding: feishuOnboardingAdapter,
|
|
157
262
|
messaging: {
|
|
@@ -163,14 +268,14 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
163
268
|
},
|
|
164
269
|
directory: {
|
|
165
270
|
self: async () => null,
|
|
166
|
-
listPeers: async ({ cfg, query, limit }) =>
|
|
167
|
-
listFeishuDirectoryPeers({ cfg, query, limit }),
|
|
168
|
-
listGroups: async ({ cfg, query, limit }) =>
|
|
169
|
-
listFeishuDirectoryGroups({ cfg, query, limit }),
|
|
170
|
-
listPeersLive: async ({ cfg, query, limit }) =>
|
|
171
|
-
listFeishuDirectoryPeersLive({ cfg, query, limit }),
|
|
172
|
-
listGroupsLive: async ({ cfg, query, limit }) =>
|
|
173
|
-
listFeishuDirectoryGroupsLive({ cfg, query, limit }),
|
|
271
|
+
listPeers: async ({ cfg, query, limit, accountId }) =>
|
|
272
|
+
listFeishuDirectoryPeers({ cfg, query, limit, accountId }),
|
|
273
|
+
listGroups: async ({ cfg, query, limit, accountId }) =>
|
|
274
|
+
listFeishuDirectoryGroups({ cfg, query, limit, accountId }),
|
|
275
|
+
listPeersLive: async ({ cfg, query, limit, accountId }) =>
|
|
276
|
+
listFeishuDirectoryPeersLive({ cfg, query, limit, accountId }),
|
|
277
|
+
listGroupsLive: async ({ cfg, query, limit, accountId }) =>
|
|
278
|
+
listFeishuDirectoryGroupsLive({ cfg, query, limit, accountId }),
|
|
174
279
|
},
|
|
175
280
|
outbound: feishuOutbound,
|
|
176
281
|
status: {
|
|
@@ -192,12 +297,17 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
192
297
|
probe: snapshot.probe,
|
|
193
298
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
194
299
|
}),
|
|
195
|
-
probeAccount: async ({ cfg }) =>
|
|
196
|
-
|
|
300
|
+
probeAccount: async ({ cfg, accountId }) => {
|
|
301
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
302
|
+
return await probeFeishu(account);
|
|
303
|
+
},
|
|
197
304
|
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
198
305
|
accountId: account.accountId,
|
|
199
306
|
enabled: account.enabled,
|
|
200
307
|
configured: account.configured,
|
|
308
|
+
name: account.name,
|
|
309
|
+
appId: account.appId,
|
|
310
|
+
domain: account.domain,
|
|
201
311
|
running: runtime?.running ?? false,
|
|
202
312
|
lastStartAt: runtime?.lastStartAt ?? null,
|
|
203
313
|
lastStopAt: runtime?.lastStopAt ?? null,
|
|
@@ -209,10 +319,10 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
209
319
|
gateway: {
|
|
210
320
|
startAccount: async (ctx) => {
|
|
211
321
|
const { monitorFeishuProvider } = await import("./monitor.js");
|
|
212
|
-
const
|
|
213
|
-
const port =
|
|
322
|
+
const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
323
|
+
const port = account.config?.webhookPort ?? null;
|
|
214
324
|
ctx.setStatus({ accountId: ctx.accountId, port });
|
|
215
|
-
ctx.log?.info(`starting feishu
|
|
325
|
+
ctx.log?.info(`starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`);
|
|
216
326
|
return monitorFeishuProvider({
|
|
217
327
|
config: ctx.cfg,
|
|
218
328
|
runtime: ctx.runtime,
|