@m1heng-clawd/feishu 0.1.9 → 0.1.11
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 +199 -31
- package/index.ts +3 -1
- package/package.json +4 -4
- package/skills/feishu-doc/SKILL.md +24 -0
- 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/doc-schema.ts +8 -0
- package/src/doc-write-service.ts +711 -0
- package/src/docx.ts +83 -287
- package/src/drive-schema.ts +20 -0
- package/src/drive.ts +57 -27
- package/src/dynamic-agent.ts +8 -4
- package/src/media.ts +25 -40
- package/src/monitor.ts +29 -2
- package/src/onboarding.ts +196 -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,7 +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;
|
|
223
|
-
|
|
210
|
+
// Keep explicit bot mention semantics: without a resolved botOpenId, do not trigger.
|
|
211
|
+
if (!botOpenId) return false;
|
|
224
212
|
return mentions.some((m) => m.id.open_id === botOpenId);
|
|
225
213
|
}
|
|
226
214
|
|
|
@@ -541,7 +529,8 @@ export async function handleFeishuMessage(params: {
|
|
|
541
529
|
|
|
542
530
|
// Dedup check: skip if this message was already processed
|
|
543
531
|
const messageId = event.message.message_id;
|
|
544
|
-
|
|
532
|
+
const dedupAccountId = accountId || "default";
|
|
533
|
+
if (!tryRecordMessage(messageId, dedupAccountId)) {
|
|
545
534
|
log(`feishu: skipping duplicate message ${messageId}`);
|
|
546
535
|
return;
|
|
547
536
|
}
|
|
@@ -582,12 +571,16 @@ export async function handleFeishuMessage(params: {
|
|
|
582
571
|
0,
|
|
583
572
|
feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
|
|
584
573
|
);
|
|
574
|
+
const groupConfig = isGroup
|
|
575
|
+
? resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId })
|
|
576
|
+
: undefined;
|
|
577
|
+
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
|
578
|
+
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
|
579
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
585
580
|
|
|
586
581
|
if (isGroup) {
|
|
587
582
|
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
|
588
583
|
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
|
589
|
-
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
|
|
590
|
-
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
|
|
591
584
|
|
|
592
585
|
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
|
|
593
586
|
const groupAllowed = isFeishuGroupAllowed({
|
|
@@ -640,24 +633,73 @@ export async function handleFeishuMessage(params: {
|
|
|
640
633
|
}
|
|
641
634
|
return;
|
|
642
635
|
}
|
|
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
636
|
}
|
|
658
637
|
|
|
659
638
|
try {
|
|
660
639
|
const core = getFeishuRuntime();
|
|
640
|
+
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
|
|
641
|
+
ctx.content,
|
|
642
|
+
cfg,
|
|
643
|
+
);
|
|
644
|
+
const storeAllowFrom =
|
|
645
|
+
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized)
|
|
646
|
+
? await core.channel.pairing.readAllowFromStore("feishu").catch(() => [])
|
|
647
|
+
: [];
|
|
648
|
+
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
649
|
+
const dmAllowed = resolveFeishuAllowlistMatch({
|
|
650
|
+
allowFrom: effectiveDmAllowFrom,
|
|
651
|
+
senderId: ctx.senderOpenId,
|
|
652
|
+
senderName: ctx.senderName,
|
|
653
|
+
}).allowed;
|
|
654
|
+
|
|
655
|
+
if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
|
|
656
|
+
if (dmPolicy === "pairing") {
|
|
657
|
+
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
|
658
|
+
channel: "feishu",
|
|
659
|
+
id: ctx.senderOpenId,
|
|
660
|
+
meta: { name: ctx.senderName },
|
|
661
|
+
});
|
|
662
|
+
if (created) {
|
|
663
|
+
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
|
664
|
+
try {
|
|
665
|
+
await sendMessageFeishu({
|
|
666
|
+
cfg,
|
|
667
|
+
to: `user:${ctx.senderOpenId}`,
|
|
668
|
+
text: core.channel.pairing.buildPairingReply({
|
|
669
|
+
channel: "feishu",
|
|
670
|
+
idLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
|
671
|
+
code,
|
|
672
|
+
}),
|
|
673
|
+
accountId: account.accountId,
|
|
674
|
+
});
|
|
675
|
+
} catch (err) {
|
|
676
|
+
log(
|
|
677
|
+
`feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
log(
|
|
683
|
+
`feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const commandAllowFrom = isGroup ? (groupConfig?.allowFrom ?? []) : effectiveDmAllowFrom;
|
|
690
|
+
const senderAllowedForCommands = resolveFeishuAllowlistMatch({
|
|
691
|
+
allowFrom: commandAllowFrom,
|
|
692
|
+
senderId: ctx.senderOpenId,
|
|
693
|
+
senderName: ctx.senderName,
|
|
694
|
+
}).allowed;
|
|
695
|
+
const commandAuthorized = shouldComputeCommandAuthorized
|
|
696
|
+
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
697
|
+
useAccessGroups,
|
|
698
|
+
authorizers: [
|
|
699
|
+
{ configured: commandAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
|
700
|
+
],
|
|
701
|
+
})
|
|
702
|
+
: undefined;
|
|
661
703
|
|
|
662
704
|
// In group chats, the session is scoped to the group, but the *speaker* is the sender.
|
|
663
705
|
// Using a group-scoped From causes the agent to treat different users as the same person.
|
|
@@ -700,6 +742,7 @@ export async function handleFeishuMessage(params: {
|
|
|
700
742
|
runtime,
|
|
701
743
|
senderOpenId: ctx.senderOpenId,
|
|
702
744
|
dynamicCfg,
|
|
745
|
+
accountId: account.accountId,
|
|
703
746
|
log: (msg) => log(msg),
|
|
704
747
|
});
|
|
705
748
|
if (result.created) {
|
|
@@ -709,7 +752,7 @@ export async function handleFeishuMessage(params: {
|
|
|
709
752
|
cfg: result.updatedCfg,
|
|
710
753
|
channel: "feishu",
|
|
711
754
|
accountId: account.accountId,
|
|
712
|
-
peer: { kind: "
|
|
755
|
+
peer: { kind: "direct", id: ctx.senderOpenId },
|
|
713
756
|
});
|
|
714
757
|
log(`feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`);
|
|
715
758
|
}
|
|
@@ -804,7 +847,7 @@ export async function handleFeishuMessage(params: {
|
|
|
804
847
|
MessageSid: `${ctx.messageId}:permission-error`,
|
|
805
848
|
Timestamp: Date.now(),
|
|
806
849
|
WasMentioned: false,
|
|
807
|
-
CommandAuthorized:
|
|
850
|
+
CommandAuthorized: commandAuthorized,
|
|
808
851
|
OriginatingChannel: "feishu" as const,
|
|
809
852
|
OriginatingTo: feishuTo,
|
|
810
853
|
});
|
|
@@ -821,12 +864,21 @@ export async function handleFeishuMessage(params: {
|
|
|
821
864
|
|
|
822
865
|
log(`feishu[${account.accountId}]: dispatching permission error notification to agent`);
|
|
823
866
|
|
|
824
|
-
await
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
867
|
+
await runWithFeishuToolContext(
|
|
868
|
+
{
|
|
869
|
+
channel: "feishu",
|
|
870
|
+
accountId: account.accountId,
|
|
871
|
+
sessionKey: route.sessionKey,
|
|
872
|
+
},
|
|
873
|
+
// Keep account context available while the agent executes plugin tools.
|
|
874
|
+
() =>
|
|
875
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
876
|
+
ctx: permissionCtx,
|
|
877
|
+
cfg,
|
|
878
|
+
dispatcher: permDispatcher,
|
|
879
|
+
replyOptions: permReplyOptions,
|
|
880
|
+
}),
|
|
881
|
+
);
|
|
830
882
|
|
|
831
883
|
markPermIdle();
|
|
832
884
|
}
|
|
@@ -877,9 +929,10 @@ export async function handleFeishuMessage(params: {
|
|
|
877
929
|
MessageSid: ctx.messageId,
|
|
878
930
|
Timestamp: Date.now(),
|
|
879
931
|
WasMentioned: ctx.mentionedBot,
|
|
880
|
-
CommandAuthorized:
|
|
932
|
+
CommandAuthorized: commandAuthorized,
|
|
881
933
|
OriginatingChannel: "feishu" as const,
|
|
882
934
|
OriginatingTo: feishuTo,
|
|
935
|
+
ReplyToBody: quotedContent,
|
|
883
936
|
...mediaPayload,
|
|
884
937
|
});
|
|
885
938
|
|
|
@@ -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
|
+
}
|
package/src/doc-schema.ts
CHANGED
|
@@ -22,6 +22,14 @@ export const FeishuDocSchema = Type.Union([
|
|
|
22
22
|
title: Type.String({ description: "Document title" }),
|
|
23
23
|
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
|
|
24
24
|
}),
|
|
25
|
+
Type.Object({
|
|
26
|
+
action: Type.Literal("create_and_write"),
|
|
27
|
+
title: Type.String({ description: "Document title" }),
|
|
28
|
+
content: Type.String({
|
|
29
|
+
description: "Markdown content to write immediately after document creation",
|
|
30
|
+
}),
|
|
31
|
+
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
|
|
32
|
+
}),
|
|
25
33
|
Type.Object({
|
|
26
34
|
action: Type.Literal("list_blocks"),
|
|
27
35
|
doc_token: Type.String({ description: "Document token" }),
|