@openclaw/feishu 2026.3.13 → 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 +95 -7
- 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 +778 -775
- 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 +63 -1
- 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 +32 -94
- 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 +375 -26
- package/src/media.ts +434 -88
- 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 +218 -312
- 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 +108 -48
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +11 -9
- 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 +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- 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 +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- 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 +660 -29
- package/src/reply-dispatcher.ts +407 -154
- 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 +77 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +399 -86
- 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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import "./lifecycle.test-support.js";
|
|
4
|
+
import {
|
|
5
|
+
getFeishuLifecycleTestMocks,
|
|
6
|
+
resetFeishuLifecycleTestMocks,
|
|
7
|
+
} from "./lifecycle.test-support.js";
|
|
8
|
+
import {
|
|
9
|
+
createFeishuLifecycleConfig,
|
|
10
|
+
createFeishuLifecycleReplyDispatcher,
|
|
11
|
+
createFeishuTextMessageEvent,
|
|
12
|
+
expectFeishuReplyDispatcherSentFinalReplyOnce,
|
|
13
|
+
expectFeishuReplyPipelineDedupedAcrossReplay,
|
|
14
|
+
expectFeishuReplyPipelineDedupedAfterPostSendFailure,
|
|
15
|
+
installFeishuLifecycleReplyRuntime,
|
|
16
|
+
mockFeishuReplyOnceDispatch,
|
|
17
|
+
restoreFeishuLifecycleStateDir,
|
|
18
|
+
setFeishuLifecycleStateDir,
|
|
19
|
+
setupFeishuMessageReceiveLifecycleHandler,
|
|
20
|
+
} from "./test-support/lifecycle-test-support.js";
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
createFeishuReplyDispatcherMock,
|
|
24
|
+
dispatchReplyFromConfigMock,
|
|
25
|
+
finalizeInboundContextMock,
|
|
26
|
+
resolveAgentRouteMock,
|
|
27
|
+
withReplyDispatcherMock,
|
|
28
|
+
} = getFeishuLifecycleTestMocks();
|
|
29
|
+
|
|
30
|
+
let lastRuntime = createRuntimeEnv();
|
|
31
|
+
let lifecycleCore: ReturnType<typeof installFeishuLifecycleReplyRuntime>;
|
|
32
|
+
const handleMessageMock = vi.fn();
|
|
33
|
+
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
|
|
34
|
+
const lifecycleConfig = createFeishuLifecycleConfig({
|
|
35
|
+
accountId: "acct-lifecycle",
|
|
36
|
+
appId: "cli_test",
|
|
37
|
+
appSecret: "secret_test",
|
|
38
|
+
accountConfig: {
|
|
39
|
+
groupPolicy: "open",
|
|
40
|
+
groups: {
|
|
41
|
+
oc_group_1: {
|
|
42
|
+
requireMention: false,
|
|
43
|
+
groupSessionScope: "group_topic_sender",
|
|
44
|
+
replyInThread: "enabled",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
async function setupLifecycleMonitor() {
|
|
51
|
+
lastRuntime = createRuntimeEnv();
|
|
52
|
+
return setupFeishuMessageReceiveLifecycleHandler({
|
|
53
|
+
runtime: lastRuntime,
|
|
54
|
+
core: lifecycleCore,
|
|
55
|
+
cfg: lifecycleConfig,
|
|
56
|
+
accountId: "acct-lifecycle",
|
|
57
|
+
handleMessage: handleMessageMock,
|
|
58
|
+
resolveDebounceText: ({ event }) => {
|
|
59
|
+
const parsed = JSON.parse(event.message.content) as { text?: string };
|
|
60
|
+
return parsed.text ?? "";
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("Feishu reply-once lifecycle", () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
vi.useRealTimers();
|
|
68
|
+
resetFeishuLifecycleTestMocks();
|
|
69
|
+
handleMessageMock.mockReset();
|
|
70
|
+
lastRuntime = createRuntimeEnv();
|
|
71
|
+
setFeishuLifecycleStateDir("openclaw-feishu-lifecycle");
|
|
72
|
+
|
|
73
|
+
createFeishuReplyDispatcherMock.mockReturnValue(createFeishuLifecycleReplyDispatcher());
|
|
74
|
+
|
|
75
|
+
resolveAgentRouteMock.mockReturnValue({
|
|
76
|
+
agentId: "main",
|
|
77
|
+
channel: "feishu",
|
|
78
|
+
accountId: "acct-lifecycle",
|
|
79
|
+
sessionKey: "agent:main:feishu:group:oc_group_1",
|
|
80
|
+
mainSessionKey: "agent:main:main",
|
|
81
|
+
matchedBy: "default",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
mockFeishuReplyOnceDispatch({
|
|
85
|
+
dispatchReplyFromConfigMock,
|
|
86
|
+
replyText: "reply once",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
withReplyDispatcherMock.mockImplementation(async ({ run }) => await run());
|
|
90
|
+
handleMessageMock.mockImplementation(async ({ event }) => {
|
|
91
|
+
const reply = createFeishuReplyDispatcherMock({
|
|
92
|
+
accountId: "acct-lifecycle",
|
|
93
|
+
chatId: event.message.chat_id,
|
|
94
|
+
replyToMessageId: event.message.root_id ?? event.message.message_id,
|
|
95
|
+
replyInThread: true,
|
|
96
|
+
rootId: event.message.root_id,
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
await withReplyDispatcherMock({
|
|
100
|
+
dispatcher: reply.dispatcher,
|
|
101
|
+
onSettled: () => reply.markDispatchIdle(),
|
|
102
|
+
run: () =>
|
|
103
|
+
dispatchReplyFromConfigMock({
|
|
104
|
+
ctx: {
|
|
105
|
+
AccountId: "acct-lifecycle",
|
|
106
|
+
MessageSid: event.message.message_id,
|
|
107
|
+
},
|
|
108
|
+
dispatcher: reply.dispatcher,
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
lastRuntime?.error(`feishu[acct-lifecycle]: failed to dispatch message: ${String(err)}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
lifecycleCore = installFeishuLifecycleReplyRuntime({
|
|
117
|
+
resolveAgentRouteMock,
|
|
118
|
+
finalizeInboundContextMock,
|
|
119
|
+
dispatchReplyFromConfigMock,
|
|
120
|
+
withReplyDispatcherMock,
|
|
121
|
+
storePath: "/tmp/feishu-lifecycle-sessions.json",
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterEach(() => {
|
|
126
|
+
vi.useRealTimers();
|
|
127
|
+
restoreFeishuLifecycleStateDir(originalStateDir);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("routes a topic-bound inbound event and emits one reply across duplicate replay", async () => {
|
|
131
|
+
const onMessage = await setupLifecycleMonitor();
|
|
132
|
+
const event = createFeishuTextMessageEvent({
|
|
133
|
+
messageId: "om_lifecycle_once",
|
|
134
|
+
chatId: "oc_group_1",
|
|
135
|
+
rootId: "om_root_topic_1",
|
|
136
|
+
threadId: "omt_topic_1",
|
|
137
|
+
text: "hello from topic",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await expectFeishuReplyPipelineDedupedAcrossReplay({
|
|
141
|
+
handler: onMessage,
|
|
142
|
+
event,
|
|
143
|
+
dispatchReplyFromConfigMock,
|
|
144
|
+
createFeishuReplyDispatcherMock,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(lastRuntime?.error).not.toHaveBeenCalled();
|
|
148
|
+
expect(handleMessageMock).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
|
|
150
|
+
expect(createFeishuReplyDispatcherMock).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(createFeishuReplyDispatcherMock).toHaveBeenCalledWith(
|
|
152
|
+
expect.objectContaining({
|
|
153
|
+
accountId: "acct-lifecycle",
|
|
154
|
+
chatId: "oc_group_1",
|
|
155
|
+
replyToMessageId: "om_root_topic_1",
|
|
156
|
+
replyInThread: true,
|
|
157
|
+
rootId: "om_root_topic_1",
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
expectFeishuReplyDispatcherSentFinalReplyOnce({ createFeishuReplyDispatcherMock });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("does not duplicate delivery when the first attempt fails after sending the reply", async () => {
|
|
164
|
+
const onMessage = await setupLifecycleMonitor();
|
|
165
|
+
const event = createFeishuTextMessageEvent({
|
|
166
|
+
messageId: "om_lifecycle_retry",
|
|
167
|
+
chatId: "oc_group_1",
|
|
168
|
+
rootId: "om_root_topic_1",
|
|
169
|
+
threadId: "omt_topic_1",
|
|
170
|
+
text: "hello from topic",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
dispatchReplyFromConfigMock.mockImplementationOnce(async ({ dispatcher }) => {
|
|
174
|
+
await dispatcher.sendFinalReply({ text: "reply once" });
|
|
175
|
+
throw new Error("post-send failure");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await expectFeishuReplyPipelineDedupedAfterPostSendFailure({
|
|
179
|
+
handler: onMessage,
|
|
180
|
+
event,
|
|
181
|
+
dispatchReplyFromConfigMock,
|
|
182
|
+
runtimeErrorMock: lastRuntime?.error as ReturnType<typeof vi.fn>,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(lastRuntime?.error).toHaveBeenCalledTimes(1);
|
|
186
|
+
expect(handleMessageMock).toHaveBeenCalledTimes(1);
|
|
187
|
+
expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
|
|
188
|
+
expectFeishuReplyDispatcherSentFinalReplyOnce({ createFeishuReplyDispatcherMock });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createNonExitingRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { ClawdbotConfig } from "../runtime-api.js";
|
|
3
4
|
import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js";
|
|
4
5
|
|
|
5
6
|
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
|
@@ -39,9 +40,12 @@ function buildMultiAccountWebsocketConfig(accountIds: string[]): ClawdbotConfig
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
async function waitForStartedAccount(started: string[], accountId: string) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
await vi.waitFor(
|
|
44
|
+
() => {
|
|
45
|
+
expect(started).toContain(accountId);
|
|
46
|
+
},
|
|
47
|
+
{ timeout: 10_000 },
|
|
48
|
+
);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
afterEach(() => {
|
|
@@ -73,9 +77,7 @@ describe("Feishu monitor startup preflight", () => {
|
|
|
73
77
|
});
|
|
74
78
|
|
|
75
79
|
try {
|
|
76
|
-
await
|
|
77
|
-
await Promise.resolve();
|
|
78
|
-
|
|
80
|
+
await waitForStartedAccount(started, "alpha");
|
|
79
81
|
expect(started).toEqual(["alpha"]);
|
|
80
82
|
expect(maxInFlight).toBe(1);
|
|
81
83
|
} finally {
|
|
@@ -134,7 +136,7 @@ describe("Feishu monitor startup preflight", () => {
|
|
|
134
136
|
});
|
|
135
137
|
|
|
136
138
|
const abortController = new AbortController();
|
|
137
|
-
const runtime =
|
|
139
|
+
const runtime = createNonExitingRuntimeEnv();
|
|
138
140
|
const monitorPromise = monitorFeishuProvider({
|
|
139
141
|
config: buildMultiAccountWebsocketConfig(["alpha", "beta"]),
|
|
140
142
|
runtime,
|
|
@@ -176,7 +178,7 @@ describe("Feishu monitor startup preflight", () => {
|
|
|
176
178
|
});
|
|
177
179
|
|
|
178
180
|
try {
|
|
179
|
-
await
|
|
181
|
+
await waitForStartedAccount(started, "alpha");
|
|
180
182
|
expect(started).toEqual(["alpha"]);
|
|
181
183
|
|
|
182
184
|
abortController.abort();
|
package/src/monitor.startup.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
+
import type { RuntimeEnv } from "../runtime-api.js";
|
|
2
3
|
import { probeFeishu } from "./probe.js";
|
|
3
4
|
import type { ResolvedFeishuAccount } from "./types.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS = 30_000;
|
|
7
|
+
const FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV = "OPENCLAW_FEISHU_STARTUP_PROBE_TIMEOUT_MS";
|
|
8
|
+
|
|
9
|
+
function resolveStartupProbeTimeoutMs(): number {
|
|
10
|
+
const raw = process.env[FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV];
|
|
11
|
+
if (raw) {
|
|
12
|
+
const parsed = Number(raw);
|
|
13
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
14
|
+
return Math.floor(parsed);
|
|
15
|
+
}
|
|
16
|
+
console.warn(
|
|
17
|
+
`[feishu] ${FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV}="${raw}" is invalid; using default ${FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS}ms`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS = resolveStartupProbeTimeoutMs();
|
|
6
24
|
|
|
7
25
|
type FetchBotOpenIdOptions = {
|
|
8
26
|
runtime?: RuntimeEnv;
|
|
@@ -16,13 +34,12 @@ export type FeishuMonitorBotIdentity = {
|
|
|
16
34
|
};
|
|
17
35
|
|
|
18
36
|
function isTimeoutErrorMessage(message: string | undefined): boolean {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
: false;
|
|
37
|
+
const lower = normalizeLowercaseStringOrEmpty(message);
|
|
38
|
+
return lower.includes("timeout") || lower.includes("timed out");
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
function isAbortErrorMessage(message: string | undefined): boolean {
|
|
25
|
-
return message
|
|
42
|
+
return normalizeLowercaseStringOrEmpty(message).includes("aborted");
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
export async function fetchBotIdentityForMonitor(
|
|
@@ -42,11 +59,12 @@ export async function fetchBotIdentityForMonitor(
|
|
|
42
59
|
return { botOpenId: result.botOpenId, botName: result.botName };
|
|
43
60
|
}
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
const probeError = result.error ?? undefined;
|
|
63
|
+
if (options.abortSignal?.aborted || isAbortErrorMessage(probeError)) {
|
|
46
64
|
return {};
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
if (isTimeoutErrorMessage(
|
|
67
|
+
if (isTimeoutErrorMessage(probeError)) {
|
|
50
68
|
const error = options.runtime?.error ?? console.error;
|
|
51
69
|
error(
|
|
52
70
|
`feishu[${account.accountId}]: bot info probe timed out after ${timeoutMs}ms; continuing startup`,
|
|
@@ -54,11 +72,3 @@ export async function fetchBotIdentityForMonitor(
|
|
|
54
72
|
}
|
|
55
73
|
return {};
|
|
56
74
|
}
|
|
57
|
-
|
|
58
|
-
export async function fetchBotOpenIdForMonitor(
|
|
59
|
-
account: ResolvedFeishuAccount,
|
|
60
|
-
options: FetchBotOpenIdOptions = {},
|
|
61
|
-
): Promise<string | undefined> {
|
|
62
|
-
const identity = await fetchBotIdentityForMonitor(account, options);
|
|
63
|
-
return identity.botOpenId;
|
|
64
|
-
}
|
package/src/monitor.state.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import * as http from "http";
|
|
2
|
-
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
3
3
|
import {
|
|
4
4
|
createFixedWindowRateLimiter,
|
|
5
5
|
createWebhookAnomalyTracker,
|
|
6
6
|
type RuntimeEnv,
|
|
7
7
|
WEBHOOK_ANOMALY_COUNTER_DEFAULTS as WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK,
|
|
8
8
|
WEBHOOK_RATE_LIMIT_DEFAULTS as WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK,
|
|
9
|
-
} from "
|
|
9
|
+
} from "./monitor-state-runtime-api.js";
|
|
10
10
|
|
|
11
11
|
export const wsClients = new Map<string, Lark.WSClient>();
|
|
12
12
|
export const httpServers = new Map<string, http.Server>();
|
|
13
13
|
export const botOpenIds = new Map<string, string>();
|
|
14
14
|
export const botNames = new Map<string, string>();
|
|
15
15
|
|
|
16
|
-
export const FEISHU_WEBHOOK_MAX_BODY_BYTES =
|
|
17
|
-
export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS =
|
|
16
|
+
export const FEISHU_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
|
|
17
|
+
export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 5_000;
|
|
18
18
|
|
|
19
19
|
type WebhookRateLimitDefaults = {
|
|
20
20
|
windowMs: number;
|
|
@@ -104,6 +104,17 @@ const feishuWebhookAnomalyTracker = createWebhookAnomalyTracker({
|
|
|
104
104
|
logEvery: feishuWebhookAnomalyDefaults.logEvery,
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
function closeWsClient(client: Lark.WSClient | undefined): void {
|
|
108
|
+
if (!client) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
client.close();
|
|
113
|
+
} catch {
|
|
114
|
+
/* Best-effort cleanup */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
export function clearFeishuWebhookRateLimitStateForTest(): void {
|
|
108
119
|
feishuWebhookRateLimiter.clear();
|
|
109
120
|
feishuWebhookAnomalyTracker.clear();
|
|
@@ -134,6 +145,7 @@ export function recordWebhookStatus(
|
|
|
134
145
|
|
|
135
146
|
export function stopFeishuMonitorState(accountId?: string): void {
|
|
136
147
|
if (accountId) {
|
|
148
|
+
closeWsClient(wsClients.get(accountId));
|
|
137
149
|
wsClients.delete(accountId);
|
|
138
150
|
const server = httpServers.get(accountId);
|
|
139
151
|
if (server) {
|
|
@@ -145,6 +157,9 @@ export function stopFeishuMonitorState(accountId?: string): void {
|
|
|
145
157
|
return;
|
|
146
158
|
}
|
|
147
159
|
|
|
160
|
+
for (const client of wsClients.values()) {
|
|
161
|
+
closeWsClient(client);
|
|
162
|
+
}
|
|
148
163
|
wsClients.clear();
|
|
149
164
|
for (const server of httpServers.values()) {
|
|
150
165
|
server.close();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class FeishuRetryableSyntheticEventError extends Error {
|
|
2
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
3
|
+
super(message, options);
|
|
4
|
+
this.name = "FeishuRetryableSyntheticEventError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isFeishuRetryableSyntheticEventError(
|
|
9
|
+
error: unknown,
|
|
10
|
+
): error is FeishuRetryableSyntheticEventError {
|
|
11
|
+
return (
|
|
12
|
+
error instanceof FeishuRetryableSyntheticEventError ||
|
|
13
|
+
(typeof error === "object" &&
|
|
14
|
+
error !== null &&
|
|
15
|
+
"name" in error &&
|
|
16
|
+
error.name === "FeishuRetryableSyntheticEventError")
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
export function createFeishuClientMockModule(): {
|
|
4
|
-
createFeishuWSClient: () => { start: () => void };
|
|
4
|
+
createFeishuWSClient: () => { start: () => void; close: () => void };
|
|
5
5
|
createEventDispatcher: () => { register: () => void };
|
|
6
6
|
} {
|
|
7
7
|
return {
|
|
8
|
-
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
|
|
8
|
+
createFeishuWSClient: vi.fn(() => ({ start: vi.fn(), close: vi.fn() })),
|
|
9
9
|
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
|
|
10
10
|
};
|
|
11
11
|
}
|