@openclaw/feishu 2026.3.13 → 2026.5.2-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 +1827 -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 +1253 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +135 -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 +406 -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 +33 -95
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +116 -20
- package/src/directory.ts +60 -92
- 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 +403 -26
- package/src/media.ts +509 -132
- 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.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 +105 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +414 -95
- 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 +453 -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
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
|
}
|
package/src/monitor.transport.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import * as http from "http";
|
|
2
1
|
import crypto from "node:crypto";
|
|
2
|
+
import * as http from "node:http";
|
|
3
3
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
4
|
+
import { waitForAbortableDelay } from "./async.js";
|
|
5
|
+
import { createFeishuWSClient } from "./client.js";
|
|
4
6
|
import {
|
|
5
7
|
applyBasicWebhookRequestGuards,
|
|
6
|
-
readJsonBodyWithLimit,
|
|
7
8
|
type RuntimeEnv,
|
|
8
9
|
installRequestBodyLimitGuard,
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
readWebhookBodyOrReject,
|
|
11
|
+
safeEqualSecret,
|
|
12
|
+
} from "./monitor-transport-runtime-api.js";
|
|
11
13
|
import {
|
|
12
14
|
botNames,
|
|
13
15
|
botOpenIds,
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
} from "./monitor.state.js";
|
|
21
23
|
import type { ResolvedFeishuAccount } from "./types.js";
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
type MonitorTransportParams = {
|
|
24
26
|
account: ResolvedFeishuAccount;
|
|
25
27
|
accountId: string;
|
|
26
28
|
runtime?: RuntimeEnv;
|
|
@@ -28,6 +30,13 @@ export type MonitorTransportParams = {
|
|
|
28
30
|
eventDispatcher: Lark.EventDispatcher;
|
|
29
31
|
};
|
|
30
32
|
|
|
33
|
+
const FEISHU_WS_RECONNECT_INITIAL_DELAY_MS = 1_000;
|
|
34
|
+
const FEISHU_WS_RECONNECT_MAX_DELAY_MS = 30_000;
|
|
35
|
+
const FEISHU_WS_LOG_ERROR_MAX_LENGTH = 500;
|
|
36
|
+
const FEISHU_WS_RECONNECT_EXHAUSTED_RE = /^WebSocket reconnect exhausted after \d+ attempts?/;
|
|
37
|
+
const FEISHU_WS_AUTORECONNECT_DISABLED_ERROR =
|
|
38
|
+
"WebSocket connect failed and autoReconnect is disabled";
|
|
39
|
+
|
|
31
40
|
function isFeishuWebhookPayload(value: unknown): value is Record<string, unknown> {
|
|
32
41
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
33
42
|
}
|
|
@@ -39,14 +48,23 @@ function buildFeishuWebhookEnvelope(
|
|
|
39
48
|
return Object.assign(Object.create({ headers: req.headers }), payload) as Record<string, unknown>;
|
|
40
49
|
}
|
|
41
50
|
|
|
51
|
+
function parseFeishuWebhookPayload(rawBody: string): Record<string, unknown> | null {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(rawBody) as unknown;
|
|
54
|
+
return isFeishuWebhookPayload(parsed) ? parsed : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
function isFeishuWebhookSignatureValid(params: {
|
|
43
61
|
headers: http.IncomingHttpHeaders;
|
|
44
|
-
|
|
62
|
+
rawBody: string;
|
|
45
63
|
encryptKey?: string;
|
|
46
64
|
}): boolean {
|
|
47
65
|
const encryptKey = params.encryptKey?.trim();
|
|
48
66
|
if (!encryptKey) {
|
|
49
|
-
return
|
|
67
|
+
return false;
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
const timestampHeader = params.headers["x-lark-request-timestamp"];
|
|
@@ -61,9 +79,9 @@ function isFeishuWebhookSignatureValid(params: {
|
|
|
61
79
|
|
|
62
80
|
const computedSignature = crypto
|
|
63
81
|
.createHash("sha256")
|
|
64
|
-
.update(timestamp + nonce + encryptKey +
|
|
82
|
+
.update(timestamp + nonce + encryptKey + params.rawBody)
|
|
65
83
|
.digest("hex");
|
|
66
|
-
return computedSignature
|
|
84
|
+
return safeEqualSecret(computedSignature, signature);
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
function respondText(res: http.ServerResponse, statusCode: number, body: string): void {
|
|
@@ -72,6 +90,104 @@ function respondText(res: http.ServerResponse, statusCode: number, body: string)
|
|
|
72
90
|
res.end(body);
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
function getFeishuWsReconnectDelayMs(attempt: number): number {
|
|
94
|
+
return Math.min(
|
|
95
|
+
FEISHU_WS_RECONNECT_INITIAL_DELAY_MS * 2 ** Math.max(0, attempt - 1),
|
|
96
|
+
FEISHU_WS_RECONNECT_MAX_DELAY_MS,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatFeishuWsErrorForLog(err: unknown): string {
|
|
101
|
+
const raw = err instanceof Error ? err.message || err.name : String(err);
|
|
102
|
+
const singleLine = Array.from(raw, (char) => {
|
|
103
|
+
const code = char.charCodeAt(0);
|
|
104
|
+
return code <= 31 || code === 127 ? " " : char;
|
|
105
|
+
}).join("");
|
|
106
|
+
const redacted = singleLine
|
|
107
|
+
.replace(/:\/\/[^:@/\s]+:[^@/\s]+@/g, "://[redacted]@")
|
|
108
|
+
.replace(/\b(authorization\s*[:=]\s*Bearer\s+)[^\s,;]+/gi, "$1[redacted]")
|
|
109
|
+
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/-]+=*/g, "$1[redacted]")
|
|
110
|
+
.replace(
|
|
111
|
+
/\b((?:app[_-]?secret|tenant[_-]?access[_-]?token|access[_-]?token|refresh[_-]?token|token|secret|password)\s*[:=]\s*)[^\s&;,]+/gi,
|
|
112
|
+
"$1[redacted]",
|
|
113
|
+
)
|
|
114
|
+
.replace(/\s+/g, " ")
|
|
115
|
+
.trim();
|
|
116
|
+
|
|
117
|
+
if (!redacted) {
|
|
118
|
+
return "unknown error";
|
|
119
|
+
}
|
|
120
|
+
if (redacted.length <= FEISHU_WS_LOG_ERROR_MAX_LENGTH) {
|
|
121
|
+
return redacted;
|
|
122
|
+
}
|
|
123
|
+
return `${redacted.slice(0, FEISHU_WS_LOG_ERROR_MAX_LENGTH)}...`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isFeishuWsTerminalError(err: Error): boolean {
|
|
127
|
+
const message = err.message.trim();
|
|
128
|
+
return (
|
|
129
|
+
FEISHU_WS_RECONNECT_EXHAUSTED_RE.test(message) ||
|
|
130
|
+
message.startsWith(FEISHU_WS_AUTORECONNECT_DISABLED_ERROR)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cleanupFeishuWsClient(params: {
|
|
135
|
+
accountId: string;
|
|
136
|
+
wsClient?: Lark.WSClient;
|
|
137
|
+
error: (message: string) => void;
|
|
138
|
+
clearIdentity: boolean;
|
|
139
|
+
}): void {
|
|
140
|
+
const { accountId, wsClient, error, clearIdentity } = params;
|
|
141
|
+
if (wsClient) {
|
|
142
|
+
try {
|
|
143
|
+
wsClient.close();
|
|
144
|
+
} catch (err) {
|
|
145
|
+
error(
|
|
146
|
+
`feishu[${accountId}]: error closing WebSocket client: ${formatFeishuWsErrorForLog(err)}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
wsClients.delete(accountId);
|
|
151
|
+
if (clearIdentity) {
|
|
152
|
+
botOpenIds.delete(accountId);
|
|
153
|
+
botNames.delete(accountId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function waitForFeishuWsCycleEnd(params: {
|
|
158
|
+
abortSignal?: AbortSignal;
|
|
159
|
+
terminalError: Promise<Error>;
|
|
160
|
+
}): Promise<"abort" | Error> {
|
|
161
|
+
if (params.abortSignal?.aborted) {
|
|
162
|
+
return Promise.resolve("abort");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
let settled = false;
|
|
167
|
+
let handleAbort: (() => void) | undefined;
|
|
168
|
+
|
|
169
|
+
const finish = (result: "abort" | Error) => {
|
|
170
|
+
if (settled) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
settled = true;
|
|
174
|
+
if (handleAbort) {
|
|
175
|
+
params.abortSignal?.removeEventListener("abort", handleAbort);
|
|
176
|
+
}
|
|
177
|
+
resolve(result);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
handleAbort = () => finish("abort");
|
|
181
|
+
params.abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
182
|
+
if (params.abortSignal?.aborted) {
|
|
183
|
+
finish("abort");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
void params.terminalError.then(finish);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
75
191
|
export async function monitorWebSocket({
|
|
76
192
|
account,
|
|
77
193
|
accountId,
|
|
@@ -80,41 +196,81 @@ export async function monitorWebSocket({
|
|
|
80
196
|
eventDispatcher,
|
|
81
197
|
}: MonitorTransportParams): Promise<void> {
|
|
82
198
|
const log = runtime?.log ?? console.log;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const wsClient = createFeishuWSClient(account);
|
|
86
|
-
wsClients.set(accountId, wsClient);
|
|
87
|
-
|
|
88
|
-
return new Promise((resolve, reject) => {
|
|
89
|
-
const cleanup = () => {
|
|
90
|
-
wsClients.delete(accountId);
|
|
91
|
-
botOpenIds.delete(accountId);
|
|
92
|
-
botNames.delete(accountId);
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const handleAbort = () => {
|
|
96
|
-
log(`feishu[${accountId}]: abort signal received, stopping`);
|
|
97
|
-
cleanup();
|
|
98
|
-
resolve();
|
|
99
|
-
};
|
|
199
|
+
const error = runtime?.error ?? console.error;
|
|
100
200
|
|
|
201
|
+
let attempt = 0;
|
|
202
|
+
while (true) {
|
|
101
203
|
if (abortSignal?.aborted) {
|
|
102
|
-
|
|
103
|
-
resolve();
|
|
104
|
-
return;
|
|
204
|
+
break;
|
|
105
205
|
}
|
|
106
206
|
|
|
107
|
-
|
|
108
|
-
|
|
207
|
+
let wsClient: Lark.WSClient | undefined;
|
|
109
208
|
try {
|
|
110
|
-
|
|
209
|
+
let reportTerminalError: (err: Error) => void = () => {};
|
|
210
|
+
const terminalError = new Promise<Error>((resolve) => {
|
|
211
|
+
reportTerminalError = resolve;
|
|
212
|
+
});
|
|
213
|
+
const handleWsError = (err: Error) => {
|
|
214
|
+
if (isFeishuWsTerminalError(err)) {
|
|
215
|
+
reportTerminalError(err);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
error(
|
|
220
|
+
`feishu[${accountId}]: WebSocket SDK reported recoverable error: ${formatFeishuWsErrorForLog(err)}`,
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
log(`feishu[${accountId}]: starting WebSocket connection...`);
|
|
224
|
+
wsClient = await createFeishuWSClient(account, {
|
|
225
|
+
onError: handleWsError,
|
|
226
|
+
});
|
|
227
|
+
if (abortSignal?.aborted) {
|
|
228
|
+
cleanupFeishuWsClient({ accountId, wsClient, error, clearIdentity: true });
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
wsClients.set(accountId, wsClient);
|
|
232
|
+
await wsClient.start({ eventDispatcher });
|
|
233
|
+
attempt = 0;
|
|
111
234
|
log(`feishu[${accountId}]: WebSocket client started`);
|
|
235
|
+
const cycleEnd = await waitForFeishuWsCycleEnd({ abortSignal, terminalError });
|
|
236
|
+
if (cycleEnd === "abort") {
|
|
237
|
+
log(`feishu[${accountId}]: abort signal received, stopping`);
|
|
238
|
+
cleanupFeishuWsClient({ accountId, wsClient, error, clearIdentity: true });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
cleanupFeishuWsClient({ accountId, wsClient, error, clearIdentity: false });
|
|
243
|
+
if (abortSignal?.aborted) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
attempt += 1;
|
|
248
|
+
const delayMs = getFeishuWsReconnectDelayMs(attempt);
|
|
249
|
+
error(
|
|
250
|
+
`feishu[${accountId}]: WebSocket connection ended, recreating client in ${delayMs}ms: ${formatFeishuWsErrorForLog(cycleEnd)}`,
|
|
251
|
+
);
|
|
252
|
+
const shouldRetry = await waitForAbortableDelay(delayMs, abortSignal);
|
|
253
|
+
if (!shouldRetry) {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
112
256
|
} catch (err) {
|
|
113
|
-
|
|
114
|
-
abortSignal?.
|
|
115
|
-
|
|
257
|
+
cleanupFeishuWsClient({ accountId, wsClient, error, clearIdentity: false });
|
|
258
|
+
if (abortSignal?.aborted) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
attempt += 1;
|
|
263
|
+
const delayMs = getFeishuWsReconnectDelayMs(attempt);
|
|
264
|
+
error(
|
|
265
|
+
`feishu[${accountId}]: WebSocket start failed, retrying in ${delayMs}ms: ${formatFeishuWsErrorForLog(err)}`,
|
|
266
|
+
);
|
|
267
|
+
const shouldRetry = await waitForAbortableDelay(delayMs, abortSignal);
|
|
268
|
+
if (!shouldRetry) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
116
271
|
}
|
|
117
|
-
}
|
|
272
|
+
}
|
|
273
|
+
cleanupFeishuWsClient({ accountId, wsClient: undefined, error, clearIdentity: true });
|
|
118
274
|
}
|
|
119
275
|
|
|
120
276
|
export async function monitorWebhook({
|
|
@@ -126,6 +282,10 @@ export async function monitorWebhook({
|
|
|
126
282
|
}: MonitorTransportParams): Promise<void> {
|
|
127
283
|
const log = runtime?.log ?? console.log;
|
|
128
284
|
const error = runtime?.error ?? console.error;
|
|
285
|
+
const encryptKey = account.encryptKey?.trim();
|
|
286
|
+
if (!encryptKey) {
|
|
287
|
+
throw new Error(`Feishu account "${accountId}" webhook mode requires encryptKey`);
|
|
288
|
+
}
|
|
129
289
|
|
|
130
290
|
const port = account.config.webhookPort ?? 3000;
|
|
131
291
|
const path = account.config.webhookPath ?? "/feishu/events";
|
|
@@ -165,38 +325,41 @@ export async function monitorWebhook({
|
|
|
165
325
|
|
|
166
326
|
void (async () => {
|
|
167
327
|
try {
|
|
168
|
-
const
|
|
328
|
+
const body = await readWebhookBodyOrReject({
|
|
329
|
+
req,
|
|
330
|
+
res,
|
|
169
331
|
maxBytes: FEISHU_WEBHOOK_MAX_BODY_BYTES,
|
|
170
332
|
timeoutMs: FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
|
|
333
|
+
profile: "pre-auth",
|
|
171
334
|
});
|
|
172
|
-
if (
|
|
335
|
+
if (!body.ok || res.writableEnded) {
|
|
173
336
|
return;
|
|
174
337
|
}
|
|
175
|
-
if (
|
|
176
|
-
if (bodyResult.code === "INVALID_JSON") {
|
|
177
|
-
respondText(res, 400, "Invalid JSON");
|
|
178
|
-
}
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (!isFeishuWebhookPayload(bodyResult.value)) {
|
|
182
|
-
respondText(res, 400, "Invalid JSON");
|
|
338
|
+
if (guard.isTripped()) {
|
|
183
339
|
return;
|
|
184
340
|
}
|
|
341
|
+
const rawBody = body.value;
|
|
185
342
|
|
|
186
|
-
//
|
|
343
|
+
// Reject invalid signatures before any JSON parsing to keep the auth boundary strict.
|
|
187
344
|
if (
|
|
188
345
|
!isFeishuWebhookSignatureValid({
|
|
189
346
|
headers: req.headers,
|
|
190
|
-
|
|
191
|
-
encryptKey
|
|
347
|
+
rawBody,
|
|
348
|
+
encryptKey,
|
|
192
349
|
})
|
|
193
350
|
) {
|
|
194
351
|
respondText(res, 401, "Invalid signature");
|
|
195
352
|
return;
|
|
196
353
|
}
|
|
197
354
|
|
|
198
|
-
const
|
|
199
|
-
|
|
355
|
+
const payload = parseFeishuWebhookPayload(rawBody);
|
|
356
|
+
if (!payload) {
|
|
357
|
+
respondText(res, 400, "Invalid JSON");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const { isChallenge, challenge } = Lark.generateChallenge(payload, {
|
|
362
|
+
encryptKey,
|
|
200
363
|
});
|
|
201
364
|
if (isChallenge) {
|
|
202
365
|
res.statusCode = 200;
|
|
@@ -205,21 +368,18 @@ export async function monitorWebhook({
|
|
|
205
368
|
return;
|
|
206
369
|
}
|
|
207
370
|
|
|
208
|
-
const value = await eventDispatcher.invoke(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
371
|
+
const value = await eventDispatcher.invoke(buildFeishuWebhookEnvelope(req, payload), {
|
|
372
|
+
needCheck: false,
|
|
373
|
+
});
|
|
212
374
|
if (!res.headersSent) {
|
|
213
375
|
res.statusCode = 200;
|
|
214
376
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
215
377
|
res.end(JSON.stringify(value));
|
|
216
378
|
}
|
|
217
379
|
} catch (err) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
respondText(res, 500, "Internal Server Error");
|
|
222
|
-
}
|
|
380
|
+
error(`feishu[${accountId}]: webhook handler error: ${String(err)}`);
|
|
381
|
+
if (!res.headersSent) {
|
|
382
|
+
respondText(res, 500, "Internal Server Error");
|
|
223
383
|
}
|
|
224
384
|
} finally {
|
|
225
385
|
guard.dispose();
|
package/src/monitor.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from "
|
|
2
|
-
import { listEnabledFeishuAccounts,
|
|
3
|
-
import {
|
|
4
|
-
monitorSingleAccount,
|
|
5
|
-
resolveReactionSyntheticEvent,
|
|
6
|
-
type FeishuReactionCreatedEvent,
|
|
7
|
-
} from "./monitor.account.js";
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
|
2
|
+
import { listEnabledFeishuAccounts, resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
8
3
|
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
|
|
9
4
|
import {
|
|
10
5
|
clearFeishuWebhookRateLimitStateForTest,
|
|
@@ -20,13 +15,18 @@ export type MonitorFeishuOpts = {
|
|
|
20
15
|
accountId?: string;
|
|
21
16
|
};
|
|
22
17
|
|
|
18
|
+
let monitorAccountRuntimePromise: Promise<typeof import("./monitor.account.js")> | undefined;
|
|
19
|
+
|
|
20
|
+
async function loadMonitorAccountRuntime() {
|
|
21
|
+
monitorAccountRuntimePromise ??= import("./monitor.account.js");
|
|
22
|
+
return await monitorAccountRuntimePromise;
|
|
23
|
+
}
|
|
24
|
+
|
|
23
25
|
export {
|
|
24
26
|
clearFeishuWebhookRateLimitStateForTest,
|
|
25
27
|
getFeishuWebhookRateLimitStateSizeForTest,
|
|
26
28
|
isWebhookRateLimitedForTest,
|
|
27
|
-
resolveReactionSyntheticEvent,
|
|
28
29
|
};
|
|
29
|
-
export type { FeishuReactionCreatedEvent };
|
|
30
30
|
|
|
31
31
|
export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
|
|
32
32
|
const cfg = opts.config;
|
|
@@ -37,10 +37,14 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
37
37
|
const log = opts.runtime?.log ?? console.log;
|
|
38
38
|
|
|
39
39
|
if (opts.accountId) {
|
|
40
|
-
const account =
|
|
40
|
+
const account = resolveFeishuRuntimeAccount(
|
|
41
|
+
{ cfg, accountId: opts.accountId },
|
|
42
|
+
{ requireEventSecrets: true },
|
|
43
|
+
);
|
|
41
44
|
if (!account.enabled || !account.configured) {
|
|
42
45
|
throw new Error(`Feishu account "${opts.accountId}" not configured or disabled`);
|
|
43
46
|
}
|
|
47
|
+
const { monitorSingleAccount } = await loadMonitorAccountRuntime();
|
|
44
48
|
return monitorSingleAccount({
|
|
45
49
|
cfg,
|
|
46
50
|
account,
|
|
@@ -58,6 +62,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
58
62
|
`feishu: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`,
|
|
59
63
|
);
|
|
60
64
|
|
|
65
|
+
const { monitorSingleAccount } = await loadMonitorAccountRuntime();
|
|
61
66
|
const monitorPromises: Promise<void>[] = [];
|
|
62
67
|
for (const account of accounts) {
|
|
63
68
|
if (opts.abortSignal?.aborted) {
|