@m1heng-clawd/feishu 0.1.8 → 0.1.10
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 +42 -28
- package/index.ts +3 -1
- package/package.json +5 -5
- package/src/bitable-tools/actions.ts +199 -0
- package/src/bitable-tools/common.ts +90 -0
- package/src/bitable-tools/meta.ts +80 -0
- package/src/bitable-tools/register.ts +195 -0
- package/src/bitable-tools/schemas.ts +221 -0
- package/src/bitable.ts +1 -441
- package/src/bot.ts +130 -68
- package/src/channel.ts +6 -8
- package/src/config-schema.ts +7 -0
- package/src/dedup.ts +31 -0
- package/src/docx.ts +440 -62
- package/src/drive-schema.ts +20 -0
- package/src/drive.ts +84 -27
- package/src/dynamic-agent.ts +8 -4
- package/src/media.ts +25 -40
- package/src/monitor.ts +29 -2
- package/src/onboarding.ts +186 -105
- package/src/perm.ts +23 -24
- package/src/probe.ts +108 -4
- package/src/reply-dispatcher.ts +137 -73
- package/src/streaming-card.ts +211 -0
- package/src/targets.ts +1 -1
- package/src/task-tools/actions.ts +137 -0
- package/src/task-tools/common.ts +18 -0
- package/src/task-tools/register.ts +101 -0
- package/src/task-tools/schemas.ts +138 -0
- package/src/task.ts +1 -0
- package/src/tools-common/feishu-api.ts +184 -0
- package/src/tools-common/tool-context.ts +23 -0
- package/src/tools-common/tool-exec.ts +73 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/wiki.ts +42 -43
package/src/bot.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo, ResolvedFeish
|
|
|
10
10
|
import { getFeishuRuntime } from "./runtime.js";
|
|
11
11
|
import { createFeishuClient } from "./client.js";
|
|
12
12
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
13
|
+
import { tryRecordMessage } from "./dedup.js";
|
|
13
14
|
import {
|
|
14
15
|
resolveFeishuGroupConfig,
|
|
15
16
|
resolveFeishuReplyPolicy,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
isFeishuGroupAllowed,
|
|
18
19
|
} from "./policy.js";
|
|
19
20
|
import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
|
|
20
|
-
import { getMessageFeishu } from "./send.js";
|
|
21
|
+
import { getMessageFeishu, sendMessageFeishu } from "./send.js";
|
|
21
22
|
import { downloadImageFeishu, downloadMessageResourceFeishu } from "./media.js";
|
|
22
23
|
import {
|
|
23
24
|
extractMentionTargets,
|
|
@@ -25,39 +26,9 @@ import {
|
|
|
25
26
|
isMentionForwardRequest,
|
|
26
27
|
} from "./mention.js";
|
|
27
28
|
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
|
29
|
+
import { runWithFeishuToolContext } from "./tools-common/tool-context.js";
|
|
28
30
|
import type { DynamicAgentCreationConfig } from "./types.js";
|
|
29
31
|
|
|
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
|
-
}
|
|
60
|
-
|
|
61
32
|
// --- Permission error extraction ---
|
|
62
33
|
// Extract permission grant URL from Feishu API error response.
|
|
63
34
|
type PermissionError = {
|
|
@@ -66,6 +37,20 @@ type PermissionError = {
|
|
|
66
37
|
grantUrl?: string;
|
|
67
38
|
};
|
|
68
39
|
|
|
40
|
+
function decodeHtmlEntities(raw: string): string {
|
|
41
|
+
return raw
|
|
42
|
+
.replace(/&/gi, "&")
|
|
43
|
+
.replace(/'|'/gi, "'")
|
|
44
|
+
.replace(/"/gi, '"');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractFirstUrl(raw: string): string | undefined {
|
|
48
|
+
if (!raw) return undefined;
|
|
49
|
+
const decoded = decodeHtmlEntities(raw);
|
|
50
|
+
const urlMatch = decoded.match(/https?:\/\/[^\s"'<>]+/i);
|
|
51
|
+
return urlMatch?.[0];
|
|
52
|
+
}
|
|
53
|
+
|
|
69
54
|
function extractPermissionError(err: unknown): PermissionError | null {
|
|
70
55
|
if (!err || typeof err !== "object") return null;
|
|
71
56
|
|
|
@@ -83,10 +68,12 @@ function extractPermissionError(err: unknown): PermissionError | null {
|
|
|
83
68
|
// Feishu permission error code: 99991672
|
|
84
69
|
if (feishuErr.code !== 99991672) return null;
|
|
85
70
|
|
|
86
|
-
// Extract the grant URL from the error message (contains the direct link)
|
|
87
71
|
const msg = feishuErr.msg ?? "";
|
|
88
|
-
const
|
|
89
|
-
const
|
|
72
|
+
const grantUrlFromMsg = extractFirstUrl(msg);
|
|
73
|
+
const grantUrlFromViolations = feishuErr.error?.permission_violations
|
|
74
|
+
?.map((item) => extractFirstUrl(item.uri ?? ""))
|
|
75
|
+
.find((url): url is string => Boolean(url));
|
|
76
|
+
const grantUrl = grantUrlFromMsg ?? grantUrlFromViolations;
|
|
90
77
|
|
|
91
78
|
return {
|
|
92
79
|
code: feishuErr.code,
|
|
@@ -220,6 +207,8 @@ function parseMessageContent(content: string, messageType: string): string {
|
|
|
220
207
|
function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string): boolean {
|
|
221
208
|
const mentions = event.message.mentions ?? [];
|
|
222
209
|
if (mentions.length === 0) return false;
|
|
210
|
+
// Fallback for degraded startup/probe scenarios: if botOpenId is unavailable,
|
|
211
|
+
// keep historical behavior and treat any mention as a mention trigger.
|
|
223
212
|
if (!botOpenId) return mentions.length > 0;
|
|
224
213
|
return mentions.some((m) => m.id.open_id === botOpenId);
|
|
225
214
|
}
|
|
@@ -541,7 +530,8 @@ export async function handleFeishuMessage(params: {
|
|
|
541
530
|
|
|
542
531
|
// Dedup check: skip if this message was already processed
|
|
543
532
|
const messageId = event.message.message_id;
|
|
544
|
-
|
|
533
|
+
const dedupAccountId = accountId || "default";
|
|
534
|
+
if (!tryRecordMessage(messageId, dedupAccountId)) {
|
|
545
535
|
log(`feishu: skipping duplicate message ${messageId}`);
|
|
546
536
|
return;
|
|
547
537
|
}
|
|
@@ -582,12 +572,16 @@ export async function handleFeishuMessage(params: {
|
|
|
582
572
|
0,
|
|
583
573
|
feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
|
|
584
574
|
);
|
|
575
|
+
const groupConfig = isGroup
|
|
576
|
+
? resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId })
|
|
577
|
+
: undefined;
|
|
578
|
+
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
|
579
|
+
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
|
580
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
585
581
|
|
|
586
582
|
if (isGroup) {
|
|
587
583
|
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
|
588
584
|
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
|
589
|
-
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
|
|
590
|
-
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
|
|
591
585
|
|
|
592
586
|
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
|
|
593
587
|
const groupAllowed = isFeishuGroupAllowed({
|
|
@@ -640,24 +634,73 @@ export async function handleFeishuMessage(params: {
|
|
|
640
634
|
}
|
|
641
635
|
return;
|
|
642
636
|
}
|
|
643
|
-
} else {
|
|
644
|
-
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
|
645
|
-
const allowFrom = feishuCfg?.allowFrom ?? [];
|
|
646
|
-
|
|
647
|
-
if (dmPolicy === "allowlist") {
|
|
648
|
-
const match = resolveFeishuAllowlistMatch({
|
|
649
|
-
allowFrom,
|
|
650
|
-
senderId: ctx.senderOpenId,
|
|
651
|
-
});
|
|
652
|
-
if (!match.allowed) {
|
|
653
|
-
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in DM allowlist`);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
637
|
}
|
|
658
638
|
|
|
659
639
|
try {
|
|
660
640
|
const core = getFeishuRuntime();
|
|
641
|
+
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
|
|
642
|
+
ctx.content,
|
|
643
|
+
cfg,
|
|
644
|
+
);
|
|
645
|
+
const storeAllowFrom =
|
|
646
|
+
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized)
|
|
647
|
+
? await core.channel.pairing.readAllowFromStore("feishu").catch(() => [])
|
|
648
|
+
: [];
|
|
649
|
+
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
650
|
+
const dmAllowed = resolveFeishuAllowlistMatch({
|
|
651
|
+
allowFrom: effectiveDmAllowFrom,
|
|
652
|
+
senderId: ctx.senderOpenId,
|
|
653
|
+
senderName: ctx.senderName,
|
|
654
|
+
}).allowed;
|
|
655
|
+
|
|
656
|
+
if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
|
|
657
|
+
if (dmPolicy === "pairing") {
|
|
658
|
+
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
|
659
|
+
channel: "feishu",
|
|
660
|
+
id: ctx.senderOpenId,
|
|
661
|
+
meta: { name: ctx.senderName },
|
|
662
|
+
});
|
|
663
|
+
if (created) {
|
|
664
|
+
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
|
665
|
+
try {
|
|
666
|
+
await sendMessageFeishu({
|
|
667
|
+
cfg,
|
|
668
|
+
to: `user:${ctx.senderOpenId}`,
|
|
669
|
+
text: core.channel.pairing.buildPairingReply({
|
|
670
|
+
channel: "feishu",
|
|
671
|
+
idLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
|
672
|
+
code,
|
|
673
|
+
}),
|
|
674
|
+
accountId: account.accountId,
|
|
675
|
+
});
|
|
676
|
+
} catch (err) {
|
|
677
|
+
log(
|
|
678
|
+
`feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
log(
|
|
684
|
+
`feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const commandAllowFrom = isGroup ? (groupConfig?.allowFrom ?? []) : effectiveDmAllowFrom;
|
|
691
|
+
const senderAllowedForCommands = resolveFeishuAllowlistMatch({
|
|
692
|
+
allowFrom: commandAllowFrom,
|
|
693
|
+
senderId: ctx.senderOpenId,
|
|
694
|
+
senderName: ctx.senderName,
|
|
695
|
+
}).allowed;
|
|
696
|
+
const commandAuthorized = shouldComputeCommandAuthorized
|
|
697
|
+
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
698
|
+
useAccessGroups,
|
|
699
|
+
authorizers: [
|
|
700
|
+
{ configured: commandAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
|
701
|
+
],
|
|
702
|
+
})
|
|
703
|
+
: undefined;
|
|
661
704
|
|
|
662
705
|
// In group chats, the session is scoped to the group, but the *speaker* is the sender.
|
|
663
706
|
// Using a group-scoped From causes the agent to treat different users as the same person.
|
|
@@ -683,7 +726,7 @@ export async function handleFeishuMessage(params: {
|
|
|
683
726
|
channel: "feishu",
|
|
684
727
|
accountId: account.accountId,
|
|
685
728
|
peer: {
|
|
686
|
-
kind: isGroup ? "group" : "
|
|
729
|
+
kind: isGroup ? "group" : "direct",
|
|
687
730
|
id: peerId,
|
|
688
731
|
},
|
|
689
732
|
});
|
|
@@ -700,6 +743,7 @@ export async function handleFeishuMessage(params: {
|
|
|
700
743
|
runtime,
|
|
701
744
|
senderOpenId: ctx.senderOpenId,
|
|
702
745
|
dynamicCfg,
|
|
746
|
+
accountId: account.accountId,
|
|
703
747
|
log: (msg) => log(msg),
|
|
704
748
|
});
|
|
705
749
|
if (result.created) {
|
|
@@ -709,7 +753,7 @@ export async function handleFeishuMessage(params: {
|
|
|
709
753
|
cfg: result.updatedCfg,
|
|
710
754
|
channel: "feishu",
|
|
711
755
|
accountId: account.accountId,
|
|
712
|
-
peer: { kind: "
|
|
756
|
+
peer: { kind: "direct", id: ctx.senderOpenId },
|
|
713
757
|
});
|
|
714
758
|
log(`feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`);
|
|
715
759
|
}
|
|
@@ -804,7 +848,7 @@ export async function handleFeishuMessage(params: {
|
|
|
804
848
|
MessageSid: `${ctx.messageId}:permission-error`,
|
|
805
849
|
Timestamp: Date.now(),
|
|
806
850
|
WasMentioned: false,
|
|
807
|
-
CommandAuthorized:
|
|
851
|
+
CommandAuthorized: commandAuthorized,
|
|
808
852
|
OriginatingChannel: "feishu" as const,
|
|
809
853
|
OriginatingTo: feishuTo,
|
|
810
854
|
});
|
|
@@ -821,12 +865,21 @@ export async function handleFeishuMessage(params: {
|
|
|
821
865
|
|
|
822
866
|
log(`feishu[${account.accountId}]: dispatching permission error notification to agent`);
|
|
823
867
|
|
|
824
|
-
await
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
868
|
+
await runWithFeishuToolContext(
|
|
869
|
+
{
|
|
870
|
+
channel: "feishu",
|
|
871
|
+
accountId: account.accountId,
|
|
872
|
+
sessionKey: route.sessionKey,
|
|
873
|
+
},
|
|
874
|
+
// Keep account context available while the agent executes plugin tools.
|
|
875
|
+
() =>
|
|
876
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
877
|
+
ctx: permissionCtx,
|
|
878
|
+
cfg,
|
|
879
|
+
dispatcher: permDispatcher,
|
|
880
|
+
replyOptions: permReplyOptions,
|
|
881
|
+
}),
|
|
882
|
+
);
|
|
830
883
|
|
|
831
884
|
markPermIdle();
|
|
832
885
|
}
|
|
@@ -877,7 +930,7 @@ export async function handleFeishuMessage(params: {
|
|
|
877
930
|
MessageSid: ctx.messageId,
|
|
878
931
|
Timestamp: Date.now(),
|
|
879
932
|
WasMentioned: ctx.mentionedBot,
|
|
880
|
-
CommandAuthorized:
|
|
933
|
+
CommandAuthorized: commandAuthorized,
|
|
881
934
|
OriginatingChannel: "feishu" as const,
|
|
882
935
|
OriginatingTo: feishuTo,
|
|
883
936
|
...mediaPayload,
|
|
@@ -895,12 +948,21 @@ export async function handleFeishuMessage(params: {
|
|
|
895
948
|
|
|
896
949
|
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
|
|
897
950
|
|
|
898
|
-
const { queuedFinal, counts } = await
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
951
|
+
const { queuedFinal, counts } = await runWithFeishuToolContext(
|
|
952
|
+
{
|
|
953
|
+
channel: "feishu",
|
|
954
|
+
accountId: account.accountId,
|
|
955
|
+
sessionKey: route.sessionKey,
|
|
956
|
+
},
|
|
957
|
+
// Tool calls produced by this turn should resolve to the same inbound account.
|
|
958
|
+
() =>
|
|
959
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
960
|
+
ctx: ctxPayload,
|
|
961
|
+
cfg,
|
|
962
|
+
dispatcher,
|
|
963
|
+
replyOptions,
|
|
964
|
+
}),
|
|
965
|
+
);
|
|
904
966
|
|
|
905
967
|
markDispatchIdle();
|
|
906
968
|
|
package/src/channel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
|
3
3
|
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
|
4
4
|
import {
|
|
5
5
|
resolveFeishuAccount,
|
|
@@ -29,7 +29,7 @@ const meta = {
|
|
|
29
29
|
blurb: "飞书/Lark enterprise messaging.",
|
|
30
30
|
aliases: ["lark"],
|
|
31
31
|
order: 70,
|
|
32
|
-
}
|
|
32
|
+
};
|
|
33
33
|
|
|
34
34
|
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
35
35
|
id: "feishu",
|
|
@@ -39,12 +39,11 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
39
39
|
pairing: {
|
|
40
40
|
idLabel: "feishuUserId",
|
|
41
41
|
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
|
|
42
|
-
notifyApproval: async ({ cfg, id
|
|
42
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
43
43
|
await sendMessageFeishu({
|
|
44
44
|
cfg,
|
|
45
45
|
to: id,
|
|
46
46
|
text: PAIRING_APPROVED_MESSAGE,
|
|
47
|
-
accountId,
|
|
48
47
|
});
|
|
49
48
|
},
|
|
50
49
|
},
|
|
@@ -201,7 +200,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
201
200
|
}),
|
|
202
201
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
203
202
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
204
|
-
return account.config?.allowFrom ?? [];
|
|
203
|
+
return (account.config?.allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
205
204
|
},
|
|
206
205
|
formatAllowFrom: ({ allowFrom }) =>
|
|
207
206
|
allowFrom
|
|
@@ -222,7 +221,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
222
221
|
},
|
|
223
222
|
},
|
|
224
223
|
setup: {
|
|
225
|
-
resolveAccountId: () =>
|
|
224
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
226
225
|
applyAccountConfig: ({ cfg, accountId }) => {
|
|
227
226
|
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
228
227
|
|
|
@@ -297,8 +296,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
297
296
|
probe: snapshot.probe,
|
|
298
297
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
299
298
|
}),
|
|
300
|
-
probeAccount: async ({
|
|
301
|
-
const account = resolveFeishuAccount({ cfg, accountId });
|
|
299
|
+
probeAccount: async ({ account }) => {
|
|
302
300
|
return await probeFeishu(account);
|
|
303
301
|
},
|
|
304
302
|
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
package/src/config-schema.ts
CHANGED
|
@@ -36,6 +36,9 @@ const MarkdownConfigSchema = z
|
|
|
36
36
|
// Message render mode: auto (default) = detect markdown, raw = plain text, card = always card
|
|
37
37
|
const RenderModeSchema = z.enum(["auto", "raw", "card"]).optional();
|
|
38
38
|
|
|
39
|
+
// Streaming card mode: default false. When enabled, card replies use Feishu Card Kit streaming API.
|
|
40
|
+
const StreamingModeSchema = z.boolean().optional();
|
|
41
|
+
|
|
39
42
|
const BlockStreamingCoalesceSchema = z
|
|
40
43
|
.object({
|
|
41
44
|
enabled: z.boolean().optional(),
|
|
@@ -74,6 +77,7 @@ const DynamicAgentCreationSchema = z
|
|
|
74
77
|
* Dependencies:
|
|
75
78
|
* - wiki requires doc (wiki content is edited via doc tools)
|
|
76
79
|
* - perm can work independently but is typically used with drive
|
|
80
|
+
* - task can work independently
|
|
77
81
|
*/
|
|
78
82
|
const FeishuToolsConfigSchema = z
|
|
79
83
|
.object({
|
|
@@ -82,6 +86,7 @@ const FeishuToolsConfigSchema = z
|
|
|
82
86
|
drive: z.boolean().optional(), // Cloud storage operations (default: true)
|
|
83
87
|
perm: z.boolean().optional(), // Permission management (default: false, sensitive)
|
|
84
88
|
scopes: z.boolean().optional(), // App scopes diagnostic (default: true)
|
|
89
|
+
task: z.boolean().optional(), // Task operations (default: true)
|
|
85
90
|
})
|
|
86
91
|
.strict()
|
|
87
92
|
.optional();
|
|
@@ -142,6 +147,7 @@ export const FeishuAccountConfigSchema = z
|
|
|
142
147
|
mediaMaxMb: z.number().positive().optional(),
|
|
143
148
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
144
149
|
renderMode: RenderModeSchema,
|
|
150
|
+
streaming: StreamingModeSchema,
|
|
145
151
|
tools: FeishuToolsConfigSchema,
|
|
146
152
|
})
|
|
147
153
|
.strict();
|
|
@@ -177,6 +183,7 @@ export const FeishuConfigSchema = z
|
|
|
177
183
|
mediaMaxMb: z.number().positive().optional(),
|
|
178
184
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
179
185
|
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
|
186
|
+
streaming: StreamingModeSchema,
|
|
180
187
|
tools: FeishuToolsConfigSchema,
|
|
181
188
|
// Dynamic agent creation for DM users
|
|
182
189
|
dynamicAgentCreation: DynamicAgentCreationSchema,
|
package/src/dedup.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const DEDUP_TTL_MS = 30 * 60 * 1000;
|
|
2
|
+
const DEDUP_MAX_SIZE = 1_000;
|
|
3
|
+
const DEDUP_CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
|
4
|
+
const processedMessageIds = new Map<string, number>();
|
|
5
|
+
let lastCleanupTime = Date.now();
|
|
6
|
+
|
|
7
|
+
export function tryRecordMessage(messageId: string, scope = "default"): boolean {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const dedupKey = `${scope}:${messageId}`;
|
|
10
|
+
|
|
11
|
+
if (now - lastCleanupTime > DEDUP_CLEANUP_INTERVAL_MS) {
|
|
12
|
+
for (const [id, ts] of processedMessageIds) {
|
|
13
|
+
if (now - ts > DEDUP_TTL_MS) {
|
|
14
|
+
processedMessageIds.delete(id);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
lastCleanupTime = now;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (processedMessageIds.has(dedupKey)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (processedMessageIds.size >= DEDUP_MAX_SIZE) {
|
|
25
|
+
const first = processedMessageIds.keys().next().value!;
|
|
26
|
+
processedMessageIds.delete(first);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
processedMessageIds.set(dedupKey, now);
|
|
30
|
+
return true;
|
|
31
|
+
}
|