@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.
Files changed (188) hide show
  1. package/api.ts +31 -0
  2. package/channel-entry.ts +20 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +16 -0
  5. package/index.ts +70 -53
  6. package/openclaw.plugin.json +1653 -4
  7. package/package.json +32 -7
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.test.ts +14 -0
  14. package/setup-entry.ts +13 -0
  15. package/src/accounts.test.ts +95 -7
  16. package/src/accounts.ts +199 -117
  17. package/src/app-registration.ts +331 -0
  18. package/src/approval-auth.test.ts +24 -0
  19. package/src/approval-auth.ts +25 -0
  20. package/src/async.test.ts +35 -0
  21. package/src/async.ts +43 -1
  22. package/src/audio-preflight.runtime.ts +9 -0
  23. package/src/bitable.test.ts +131 -0
  24. package/src/bitable.ts +59 -22
  25. package/src/bot-content.ts +474 -0
  26. package/src/bot-group-name.test.ts +108 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.broadcast.test.ts +463 -0
  30. package/src/bot.card-action.test.ts +519 -5
  31. package/src/bot.checkBotMentioned.test.ts +92 -20
  32. package/src/bot.helpers.test.ts +118 -0
  33. package/src/bot.stripBotMention.test.ts +13 -21
  34. package/src/bot.test.ts +1334 -401
  35. package/src/bot.ts +778 -775
  36. package/src/card-action.ts +408 -40
  37. package/src/card-interaction.test.ts +129 -0
  38. package/src/card-interaction.ts +159 -0
  39. package/src/card-test-helpers.ts +47 -0
  40. package/src/card-ux-approval.ts +65 -0
  41. package/src/card-ux-launcher.test.ts +99 -0
  42. package/src/card-ux-launcher.ts +121 -0
  43. package/src/card-ux-shared.ts +33 -0
  44. package/src/channel-runtime-api.ts +16 -0
  45. package/src/channel.runtime.ts +47 -0
  46. package/src/channel.test.ts +914 -3
  47. package/src/channel.ts +1252 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +84 -28
  50. package/src/chat.ts +68 -10
  51. package/src/client.test.ts +212 -103
  52. package/src/client.ts +115 -21
  53. package/src/comment-dispatcher-runtime-api.ts +6 -0
  54. package/src/comment-dispatcher.test.ts +169 -0
  55. package/src/comment-dispatcher.ts +107 -0
  56. package/src/comment-handler-runtime-api.ts +3 -0
  57. package/src/comment-handler.test.ts +486 -0
  58. package/src/comment-handler.ts +309 -0
  59. package/src/comment-reaction.test.ts +166 -0
  60. package/src/comment-reaction.ts +259 -0
  61. package/src/comment-shared.test.ts +182 -0
  62. package/src/comment-shared.ts +365 -0
  63. package/src/comment-target.ts +44 -0
  64. package/src/config-schema.test.ts +63 -1
  65. package/src/config-schema.ts +31 -4
  66. package/src/conversation-id.test.ts +18 -0
  67. package/src/conversation-id.ts +199 -0
  68. package/src/dedup-runtime-api.ts +1 -0
  69. package/src/dedup.ts +32 -94
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +119 -20
  72. package/src/directory.ts +61 -91
  73. package/src/doc-schema.ts +1 -1
  74. package/src/docx-batch-insert.test.ts +39 -38
  75. package/src/docx-batch-insert.ts +55 -19
  76. package/src/docx-color-text.ts +9 -4
  77. package/src/docx-table-ops.test.ts +53 -0
  78. package/src/docx-table-ops.ts +52 -34
  79. package/src/docx-types.ts +38 -0
  80. package/src/docx.account-selection.test.ts +12 -3
  81. package/src/docx.test.ts +314 -74
  82. package/src/docx.ts +278 -122
  83. package/src/drive-schema.ts +47 -1
  84. package/src/drive.test.ts +1219 -0
  85. package/src/drive.ts +614 -13
  86. package/src/dynamic-agent.ts +10 -4
  87. package/src/event-types.ts +45 -0
  88. package/src/external-keys.ts +1 -1
  89. package/src/lifecycle.test-support.ts +220 -0
  90. package/src/media.test.ts +375 -26
  91. package/src/media.ts +434 -88
  92. package/src/mention-target.types.ts +5 -0
  93. package/src/mention.ts +32 -51
  94. package/src/message-action-contract.ts +13 -0
  95. package/src/monitor-state-runtime-api.ts +7 -0
  96. package/src/monitor-transport-runtime-api.ts +7 -0
  97. package/src/monitor.account.ts +218 -312
  98. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  99. package/src/monitor.bot-identity.ts +86 -0
  100. package/src/monitor.bot-menu-handler.ts +165 -0
  101. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  102. package/src/monitor.bot-menu.test.ts +178 -0
  103. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  104. package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
  105. package/src/monitor.cleanup.test.ts +376 -0
  106. package/src/monitor.comment-notice-handler.ts +105 -0
  107. package/src/monitor.comment.test.ts +937 -0
  108. package/src/monitor.comment.ts +1386 -0
  109. package/src/monitor.lifecycle.test.ts +4 -0
  110. package/src/monitor.message-handler.ts +339 -0
  111. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  112. package/src/monitor.reaction.test.ts +108 -48
  113. package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
  114. package/src/monitor.startup.test.ts +11 -9
  115. package/src/monitor.startup.ts +26 -16
  116. package/src/monitor.state.ts +20 -5
  117. package/src/monitor.synthetic-error.ts +18 -0
  118. package/src/monitor.test-mocks.ts +2 -2
  119. package/src/monitor.transport.ts +220 -60
  120. package/src/monitor.ts +15 -10
  121. package/src/monitor.webhook-e2e.test.ts +65 -7
  122. package/src/monitor.webhook-security.test.ts +122 -0
  123. package/src/monitor.webhook.test-helpers.ts +44 -26
  124. package/src/outbound-runtime-api.ts +1 -0
  125. package/src/outbound.test.ts +616 -37
  126. package/src/outbound.ts +623 -81
  127. package/src/perm-schema.ts +1 -1
  128. package/src/perm.ts +1 -7
  129. package/src/pins.ts +108 -0
  130. package/src/policy.test.ts +297 -117
  131. package/src/policy.ts +142 -29
  132. package/src/post.ts +7 -6
  133. package/src/probe.test.ts +14 -9
  134. package/src/probe.ts +26 -16
  135. package/src/processing-claims.ts +59 -0
  136. package/src/qr-terminal.ts +1 -0
  137. package/src/reactions.ts +4 -34
  138. package/src/reasoning-preview.test.ts +59 -0
  139. package/src/reasoning-preview.ts +20 -0
  140. package/src/reply-dispatcher-runtime-api.ts +7 -0
  141. package/src/reply-dispatcher.test.ts +660 -29
  142. package/src/reply-dispatcher.ts +407 -154
  143. package/src/runtime.ts +6 -3
  144. package/src/secret-contract.ts +145 -0
  145. package/src/secret-input.ts +1 -13
  146. package/src/security-audit-shared.ts +69 -0
  147. package/src/security-audit.test.ts +61 -0
  148. package/src/security-audit.ts +1 -0
  149. package/src/send-result.ts +1 -1
  150. package/src/send-target.test.ts +9 -3
  151. package/src/send-target.ts +10 -4
  152. package/src/send.reply-fallback.test.ts +77 -2
  153. package/src/send.test.ts +386 -4
  154. package/src/send.ts +399 -86
  155. package/src/sequential-key.test.ts +72 -0
  156. package/src/sequential-key.ts +28 -0
  157. package/src/sequential-queue.test.ts +92 -0
  158. package/src/sequential-queue.ts +16 -0
  159. package/src/session-conversation.ts +42 -0
  160. package/src/session-route.ts +48 -0
  161. package/src/setup-core.ts +51 -0
  162. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  163. package/src/setup-surface.ts +581 -0
  164. package/src/streaming-card.test.ts +138 -2
  165. package/src/streaming-card.ts +134 -18
  166. package/src/subagent-hooks.test.ts +603 -0
  167. package/src/subagent-hooks.ts +397 -0
  168. package/src/targets.ts +3 -13
  169. package/src/test-support/lifecycle-test-support.ts +479 -0
  170. package/src/thread-bindings.test.ts +143 -0
  171. package/src/thread-bindings.ts +330 -0
  172. package/src/tool-account-routing.test.ts +66 -8
  173. package/src/tool-account.test.ts +44 -0
  174. package/src/tool-account.ts +40 -17
  175. package/src/tool-factory-test-harness.ts +11 -8
  176. package/src/tool-result.ts +3 -1
  177. package/src/tools-config.ts +1 -1
  178. package/src/types.ts +16 -15
  179. package/src/typing.ts +10 -6
  180. package/src/wiki-schema.ts +1 -1
  181. package/src/wiki.ts +1 -7
  182. package/subagent-hooks-api.ts +31 -0
  183. package/tsconfig.json +16 -0
  184. package/src/feishu-command-handler.ts +0 -59
  185. package/src/onboarding.status.test.ts +0 -25
  186. package/src/onboarding.ts +0 -489
  187. package/src/send-message.ts +0 -71
  188. 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 type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
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
- for (let i = 0; i < 10 && !started.includes(accountId); i += 1) {
43
- await Promise.resolve();
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 Promise.resolve();
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 = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
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 Promise.resolve();
181
+ await waitForStartedAccount(started, "alpha");
180
182
  expect(started).toEqual(["alpha"]);
181
183
 
182
184
  abortController.abort();
@@ -1,8 +1,26 @@
1
- import type { RuntimeEnv } from "openclaw/plugin-sdk/feishu";
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
- export const FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS = 10_000;
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
- return message?.toLowerCase().includes("timeout") || message?.toLowerCase().includes("timed out")
20
- ? true
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?.toLowerCase().includes("aborted") ?? false;
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
- if (options.abortSignal?.aborted || isAbortErrorMessage(result.error)) {
62
+ const probeError = result.error ?? undefined;
63
+ if (options.abortSignal?.aborted || isAbortErrorMessage(probeError)) {
46
64
  return {};
47
65
  }
48
66
 
49
- if (isTimeoutErrorMessage(result.error)) {
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
- }
@@ -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 "openclaw/plugin-sdk/feishu";
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 = 1024 * 1024;
17
- export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
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
  }