@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
package/src/client.test.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import
|
|
1
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { FeishuConfigSchema } from "./config-schema.js";
|
|
3
|
+
import type { ResolvedFeishuAccount } from "./types.js";
|
|
3
4
|
|
|
5
|
+
type CreateFeishuClient = typeof import("./client.js").createFeishuClient;
|
|
6
|
+
type CreateFeishuWSClient = typeof import("./client.js").createFeishuWSClient;
|
|
7
|
+
type ClearClientCache = typeof import("./client.js").clearClientCache;
|
|
8
|
+
type SetFeishuClientRuntimeForTest = typeof import("./client.js").setFeishuClientRuntimeForTest;
|
|
9
|
+
|
|
10
|
+
const clientCtorMock = vi.hoisted(() =>
|
|
11
|
+
vi.fn(function clientCtor() {
|
|
12
|
+
return { connected: true };
|
|
13
|
+
}),
|
|
14
|
+
);
|
|
4
15
|
const wsClientCtorMock = vi.hoisted(() =>
|
|
5
16
|
vi.fn(function wsClientCtor() {
|
|
6
17
|
return { connected: true };
|
|
7
18
|
}),
|
|
8
19
|
);
|
|
9
|
-
const
|
|
10
|
-
vi.fn(function
|
|
11
|
-
return {
|
|
20
|
+
const proxyAgentCtorMock = vi.hoisted(() =>
|
|
21
|
+
vi.fn(function proxyAgentCtor() {
|
|
22
|
+
return { proxied: true };
|
|
12
23
|
}),
|
|
13
24
|
);
|
|
14
|
-
|
|
15
25
|
const mockBaseHttpInstance = vi.hoisted(() => ({
|
|
16
26
|
request: vi.fn().mockResolvedValue({}),
|
|
17
27
|
get: vi.fn().mockResolvedValue({}),
|
|
@@ -22,36 +32,64 @@ const mockBaseHttpInstance = vi.hoisted(() => ({
|
|
|
22
32
|
head: vi.fn().mockResolvedValue({}),
|
|
23
33
|
options: vi.fn().mockResolvedValue({}),
|
|
24
34
|
}));
|
|
35
|
+
const proxyEnvKeys = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"] as const;
|
|
36
|
+
type ProxyEnvKey = (typeof proxyEnvKeys)[number];
|
|
37
|
+
const registerFeishuDocToolsMock = vi.hoisted(() => vi.fn());
|
|
38
|
+
const registerFeishuChatToolsMock = vi.hoisted(() => vi.fn());
|
|
39
|
+
const registerFeishuWikiToolsMock = vi.hoisted(() => vi.fn());
|
|
40
|
+
const registerFeishuDriveToolsMock = vi.hoisted(() => vi.fn());
|
|
41
|
+
const registerFeishuPermToolsMock = vi.hoisted(() => vi.fn());
|
|
42
|
+
const registerFeishuBitableToolsMock = vi.hoisted(() => vi.fn());
|
|
43
|
+
const feishuPluginMock = vi.hoisted(() => ({ id: "feishu-test-plugin" }));
|
|
44
|
+
const setFeishuRuntimeMock = vi.hoisted(() => vi.fn());
|
|
45
|
+
const registerFeishuSubagentHooksMock = vi.hoisted(() => vi.fn());
|
|
46
|
+
|
|
47
|
+
let createFeishuClient: CreateFeishuClient;
|
|
48
|
+
let createFeishuWSClient: CreateFeishuWSClient;
|
|
49
|
+
let clearClientCache: ClearClientCache;
|
|
50
|
+
let setFeishuClientRuntimeForTest: SetFeishuClientRuntimeForTest;
|
|
51
|
+
let FEISHU_HTTP_TIMEOUT_MS: number;
|
|
52
|
+
let FEISHU_HTTP_TIMEOUT_MAX_MS: number;
|
|
53
|
+
let FEISHU_HTTP_TIMEOUT_ENV_VAR: string;
|
|
25
54
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
WSClient: wsClientCtorMock,
|
|
32
|
-
EventDispatcher: vi.fn(),
|
|
33
|
-
defaultHttpInstance: mockBaseHttpInstance,
|
|
55
|
+
let priorProxyEnv: Partial<Record<ProxyEnvKey, string | undefined>> = {};
|
|
56
|
+
let priorFeishuTimeoutEnv: string | undefined;
|
|
57
|
+
|
|
58
|
+
vi.mock("./channel.js", () => ({
|
|
59
|
+
feishuPlugin: feishuPluginMock,
|
|
34
60
|
}));
|
|
35
61
|
|
|
36
|
-
vi.mock("
|
|
37
|
-
|
|
62
|
+
vi.mock("./docx.js", () => ({
|
|
63
|
+
registerFeishuDocTools: registerFeishuDocToolsMock,
|
|
38
64
|
}));
|
|
39
65
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
createFeishuWSClient,
|
|
44
|
-
clearClientCache,
|
|
45
|
-
FEISHU_HTTP_TIMEOUT_MS,
|
|
46
|
-
FEISHU_HTTP_TIMEOUT_MAX_MS,
|
|
47
|
-
FEISHU_HTTP_TIMEOUT_ENV_VAR,
|
|
48
|
-
} from "./client.js";
|
|
66
|
+
vi.mock("./chat.js", () => ({
|
|
67
|
+
registerFeishuChatTools: registerFeishuChatToolsMock,
|
|
68
|
+
}));
|
|
49
69
|
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
vi.mock("./wiki.js", () => ({
|
|
71
|
+
registerFeishuWikiTools: registerFeishuWikiToolsMock,
|
|
72
|
+
}));
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
vi.mock("./drive.js", () => ({
|
|
75
|
+
registerFeishuDriveTools: registerFeishuDriveToolsMock,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
vi.mock("./perm.js", () => ({
|
|
79
|
+
registerFeishuPermTools: registerFeishuPermToolsMock,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
vi.mock("./bitable.js", () => ({
|
|
83
|
+
registerFeishuBitableTools: registerFeishuBitableToolsMock,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
vi.mock("./runtime.js", () => ({
|
|
87
|
+
setFeishuRuntime: setFeishuRuntimeMock,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
vi.mock("./subagent-hooks.js", () => ({
|
|
91
|
+
registerFeishuSubagentHooks: registerFeishuSubagentHooksMock,
|
|
92
|
+
}));
|
|
55
93
|
|
|
56
94
|
const baseAccount: ResolvedFeishuAccount = {
|
|
57
95
|
accountId: "main",
|
|
@@ -61,14 +99,70 @@ const baseAccount: ResolvedFeishuAccount = {
|
|
|
61
99
|
appId: "app_123",
|
|
62
100
|
appSecret: "secret_123", // pragma: allowlist secret
|
|
63
101
|
domain: "feishu",
|
|
64
|
-
config: {}
|
|
102
|
+
config: FeishuConfigSchema.parse({}),
|
|
65
103
|
};
|
|
66
104
|
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
return calls[0]?.[0] ?? {};
|
|
105
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
106
|
+
return typeof value === "object" && value !== null;
|
|
70
107
|
}
|
|
71
108
|
|
|
109
|
+
type HttpInstanceLike = {
|
|
110
|
+
get: (url: string, options?: Record<string, unknown>) => Promise<unknown>;
|
|
111
|
+
post: (url: string, body?: unknown, options?: Record<string, unknown>) => Promise<unknown>;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function readCallOptions(
|
|
115
|
+
mock: { mock: { calls: unknown[][] } },
|
|
116
|
+
index = -1,
|
|
117
|
+
): Record<string, unknown> {
|
|
118
|
+
const call = index < 0 ? mock.mock.calls.at(index)?.[0] : mock.mock.calls[index]?.[0];
|
|
119
|
+
return isRecord(call) ? call : {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function firstWsClientOptions(): {
|
|
123
|
+
agent?: unknown;
|
|
124
|
+
wsConfig?: unknown;
|
|
125
|
+
onError?: unknown;
|
|
126
|
+
onReady?: unknown;
|
|
127
|
+
onReconnected?: unknown;
|
|
128
|
+
onReconnecting?: unknown;
|
|
129
|
+
} {
|
|
130
|
+
const options = readCallOptions(wsClientCtorMock, 0);
|
|
131
|
+
return {
|
|
132
|
+
agent: options.agent,
|
|
133
|
+
wsConfig: options.wsConfig,
|
|
134
|
+
onError: options.onError,
|
|
135
|
+
onReady: options.onReady,
|
|
136
|
+
onReconnected: options.onReconnected,
|
|
137
|
+
onReconnecting: options.onReconnecting,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
beforeAll(async () => {
|
|
142
|
+
vi.doMock("@larksuiteoapi/node-sdk", () => ({
|
|
143
|
+
AppType: { SelfBuild: "self" },
|
|
144
|
+
Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" },
|
|
145
|
+
LoggerLevel: { info: "info" },
|
|
146
|
+
Client: clientCtorMock,
|
|
147
|
+
WSClient: wsClientCtorMock,
|
|
148
|
+
EventDispatcher: vi.fn(),
|
|
149
|
+
defaultHttpInstance: mockBaseHttpInstance,
|
|
150
|
+
}));
|
|
151
|
+
vi.doMock("proxy-agent", () => ({
|
|
152
|
+
ProxyAgent: proxyAgentCtorMock,
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
({
|
|
156
|
+
createFeishuClient,
|
|
157
|
+
createFeishuWSClient,
|
|
158
|
+
clearClientCache,
|
|
159
|
+
setFeishuClientRuntimeForTest,
|
|
160
|
+
FEISHU_HTTP_TIMEOUT_MS,
|
|
161
|
+
FEISHU_HTTP_TIMEOUT_MAX_MS,
|
|
162
|
+
FEISHU_HTTP_TIMEOUT_ENV_VAR,
|
|
163
|
+
} = await import("./client.js"));
|
|
164
|
+
});
|
|
165
|
+
|
|
72
166
|
beforeEach(() => {
|
|
73
167
|
priorProxyEnv = {};
|
|
74
168
|
priorFeishuTimeoutEnv = process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
|
|
@@ -78,6 +172,21 @@ beforeEach(() => {
|
|
|
78
172
|
delete process.env[key];
|
|
79
173
|
}
|
|
80
174
|
vi.clearAllMocks();
|
|
175
|
+
clearClientCache();
|
|
176
|
+
setFeishuClientRuntimeForTest({
|
|
177
|
+
sdk: {
|
|
178
|
+
AppType: { SelfBuild: "self" } as never,
|
|
179
|
+
Domain: {
|
|
180
|
+
Feishu: "https://open.feishu.cn",
|
|
181
|
+
Lark: "https://open.larksuite.com",
|
|
182
|
+
} as never,
|
|
183
|
+
LoggerLevel: { info: "info" } as never,
|
|
184
|
+
Client: clientCtorMock as never,
|
|
185
|
+
WSClient: wsClientCtorMock as never,
|
|
186
|
+
EventDispatcher: vi.fn() as never,
|
|
187
|
+
defaultHttpInstance: mockBaseHttpInstance as never,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
81
190
|
});
|
|
82
191
|
|
|
83
192
|
afterEach(() => {
|
|
@@ -94,19 +203,23 @@ afterEach(() => {
|
|
|
94
203
|
} else {
|
|
95
204
|
process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR] = priorFeishuTimeoutEnv;
|
|
96
205
|
}
|
|
206
|
+
setFeishuClientRuntimeForTest();
|
|
97
207
|
});
|
|
98
208
|
|
|
99
209
|
describe("createFeishuClient HTTP timeout", () => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
210
|
+
const getLastClientHttpInstance = (): HttpInstanceLike | undefined => {
|
|
211
|
+
const httpInstance = readCallOptions(clientCtorMock).httpInstance;
|
|
212
|
+
if (
|
|
213
|
+
isRecord(httpInstance) &&
|
|
214
|
+
typeof httpInstance.get === "function" &&
|
|
215
|
+
typeof httpInstance.post === "function"
|
|
216
|
+
) {
|
|
217
|
+
return {
|
|
218
|
+
get: httpInstance.get as HttpInstanceLike["get"],
|
|
219
|
+
post: httpInstance.post as HttpInstanceLike["post"],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
110
223
|
};
|
|
111
224
|
|
|
112
225
|
const expectGetCallTimeout = async (timeout: number) => {
|
|
@@ -122,21 +235,16 @@ describe("createFeishuClient HTTP timeout", () => {
|
|
|
122
235
|
it("passes a custom httpInstance with default timeout to Lark.Client", () => {
|
|
123
236
|
createFeishuClient({ appId: "app_1", appSecret: "secret_1", accountId: "timeout-test" }); // pragma: allowlist secret
|
|
124
237
|
|
|
125
|
-
|
|
126
|
-
const lastCall = calls[calls.length - 1][0] as { httpInstance?: unknown };
|
|
127
|
-
expect(lastCall.httpInstance).toBeDefined();
|
|
238
|
+
expect(readCallOptions(clientCtorMock).httpInstance).toBeDefined();
|
|
128
239
|
});
|
|
129
240
|
|
|
130
241
|
it("injects default timeout into HTTP request options", async () => {
|
|
131
242
|
createFeishuClient({ appId: "app_2", appSecret: "secret_2", accountId: "timeout-inject" }); // pragma: allowlist secret
|
|
132
243
|
|
|
133
|
-
const
|
|
134
|
-
const lastCall = calls[calls.length - 1][0] as {
|
|
135
|
-
httpInstance: { post: (...args: unknown[]) => Promise<unknown> };
|
|
136
|
-
};
|
|
137
|
-
const httpInstance = lastCall.httpInstance;
|
|
244
|
+
const httpInstance = getLastClientHttpInstance();
|
|
138
245
|
|
|
139
|
-
|
|
246
|
+
expect(httpInstance).toBeDefined();
|
|
247
|
+
await httpInstance?.post(
|
|
140
248
|
"https://example.com/api",
|
|
141
249
|
{ data: 1 },
|
|
142
250
|
{ headers: { "X-Custom": "yes" } },
|
|
@@ -152,13 +260,10 @@ describe("createFeishuClient HTTP timeout", () => {
|
|
|
152
260
|
it("allows explicit timeout override per-request", async () => {
|
|
153
261
|
createFeishuClient({ appId: "app_3", appSecret: "secret_3", accountId: "timeout-override" }); // pragma: allowlist secret
|
|
154
262
|
|
|
155
|
-
const
|
|
156
|
-
const lastCall = calls[calls.length - 1][0] as {
|
|
157
|
-
httpInstance: { get: (...args: unknown[]) => Promise<unknown> };
|
|
158
|
-
};
|
|
159
|
-
const httpInstance = lastCall.httpInstance;
|
|
263
|
+
const httpInstance = getLastClientHttpInstance();
|
|
160
264
|
|
|
161
|
-
|
|
265
|
+
expect(httpInstance).toBeDefined();
|
|
266
|
+
await httpInstance?.get("https://example.com/api", { timeout: 5_000 });
|
|
162
267
|
|
|
163
268
|
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
|
|
164
269
|
"https://example.com/api",
|
|
@@ -241,13 +346,10 @@ describe("createFeishuClient HTTP timeout", () => {
|
|
|
241
346
|
config: { httpTimeoutMs: 45_000 },
|
|
242
347
|
});
|
|
243
348
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
httpInstance: { get: (...args: unknown[]) => Promise<unknown> };
|
|
249
|
-
};
|
|
250
|
-
await lastCall.httpInstance.get("https://example.com/api");
|
|
349
|
+
expect(clientCtorMock.mock.calls.length).toBe(2);
|
|
350
|
+
const httpInstance = getLastClientHttpInstance();
|
|
351
|
+
expect(httpInstance).toBeDefined();
|
|
352
|
+
await httpInstance?.get("https://example.com/api");
|
|
251
353
|
|
|
252
354
|
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
|
|
253
355
|
"https://example.com/api",
|
|
@@ -257,68 +359,75 @@ describe("createFeishuClient HTTP timeout", () => {
|
|
|
257
359
|
});
|
|
258
360
|
|
|
259
361
|
describe("createFeishuWSClient proxy handling", () => {
|
|
260
|
-
it("
|
|
261
|
-
createFeishuWSClient(baseAccount);
|
|
362
|
+
it("passes heartbeat wsConfig defaults to Lark.WSClient", async () => {
|
|
363
|
+
await createFeishuWSClient(baseAccount);
|
|
262
364
|
|
|
263
|
-
expect(httpsProxyAgentCtorMock).not.toHaveBeenCalled();
|
|
264
365
|
const options = firstWsClientOptions();
|
|
265
|
-
expect(options
|
|
366
|
+
expect(options.wsConfig).toEqual({
|
|
367
|
+
PingInterval: 30,
|
|
368
|
+
PingTimeout: 3,
|
|
369
|
+
});
|
|
266
370
|
});
|
|
267
371
|
|
|
268
|
-
it("
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
372
|
+
it("passes lifecycle callbacks while preserving heartbeat wsConfig defaults", async () => {
|
|
373
|
+
const onError = vi.fn();
|
|
374
|
+
const onReady = vi.fn();
|
|
375
|
+
const onReconnected = vi.fn();
|
|
376
|
+
const onReconnecting = vi.fn();
|
|
377
|
+
|
|
378
|
+
await createFeishuWSClient(baseAccount, {
|
|
379
|
+
onError,
|
|
380
|
+
onReady,
|
|
381
|
+
onReconnected,
|
|
382
|
+
onReconnecting,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const options = firstWsClientOptions();
|
|
386
|
+
expect(options.onError).toBe(onError);
|
|
387
|
+
expect(options.onReady).toBe(onReady);
|
|
388
|
+
expect(options.onReconnected).toBe(onReconnected);
|
|
389
|
+
expect(options.onReconnecting).toBe(onReconnecting);
|
|
390
|
+
expect(options.wsConfig).toEqual({
|
|
391
|
+
PingInterval: 30,
|
|
392
|
+
PingTimeout: 3,
|
|
393
|
+
});
|
|
394
|
+
});
|
|
276
395
|
|
|
277
|
-
|
|
396
|
+
it("does not set a ws proxy agent when proxy env is absent", async () => {
|
|
397
|
+
await createFeishuWSClient(baseAccount);
|
|
278
398
|
|
|
279
|
-
|
|
280
|
-
// overwrite https_proxy. We assert https proxies still win over http.
|
|
281
|
-
const expectedProxy = process.env.https_proxy || process.env.HTTPS_PROXY;
|
|
282
|
-
expect(expectedProxy).toBeTruthy();
|
|
283
|
-
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
284
|
-
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith(expectedProxy);
|
|
399
|
+
expect(proxyAgentCtorMock).not.toHaveBeenCalled();
|
|
285
400
|
const options = firstWsClientOptions();
|
|
286
|
-
expect(options.agent).
|
|
401
|
+
expect(options.agent).toBeUndefined();
|
|
287
402
|
});
|
|
288
403
|
|
|
289
|
-
it("
|
|
404
|
+
it("creates a ws proxy agent when lowercase https_proxy is set", async () => {
|
|
290
405
|
process.env.https_proxy = "http://lower-https:8001";
|
|
291
406
|
|
|
292
|
-
createFeishuWSClient(baseAccount);
|
|
407
|
+
await createFeishuWSClient(baseAccount);
|
|
293
408
|
|
|
294
|
-
|
|
295
|
-
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
296
|
-
expect(expectedHttpsProxy).toBeTruthy();
|
|
297
|
-
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith(expectedHttpsProxy);
|
|
409
|
+
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
298
410
|
const options = firstWsClientOptions();
|
|
299
|
-
expect(options.agent).toEqual({
|
|
411
|
+
expect(options.agent).toEqual({ proxied: true });
|
|
300
412
|
});
|
|
301
413
|
|
|
302
|
-
it("
|
|
414
|
+
it("creates a ws proxy agent when uppercase HTTPS_PROXY is set", async () => {
|
|
303
415
|
process.env.HTTPS_PROXY = "http://upper-https:8002";
|
|
304
|
-
process.env.http_proxy = "http://lower-http:8003";
|
|
305
416
|
|
|
306
|
-
createFeishuWSClient(baseAccount);
|
|
417
|
+
await createFeishuWSClient(baseAccount);
|
|
307
418
|
|
|
308
|
-
expect(
|
|
309
|
-
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://upper-https:8002");
|
|
419
|
+
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
310
420
|
const options = firstWsClientOptions();
|
|
311
|
-
expect(options.agent).toEqual({
|
|
421
|
+
expect(options.agent).toEqual({ proxied: true });
|
|
312
422
|
});
|
|
313
423
|
|
|
314
|
-
it("
|
|
424
|
+
it("falls back to HTTP_PROXY for ws proxy agent creation", async () => {
|
|
315
425
|
process.env.HTTP_PROXY = "http://upper-http:8999";
|
|
316
426
|
|
|
317
|
-
createFeishuWSClient(baseAccount);
|
|
427
|
+
await createFeishuWSClient(baseAccount);
|
|
318
428
|
|
|
319
|
-
expect(
|
|
320
|
-
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://upper-http:8999");
|
|
429
|
+
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
321
430
|
const options = firstWsClientOptions();
|
|
322
|
-
expect(options.agent).toEqual({
|
|
431
|
+
expect(options.agent).toEqual({ proxied: true });
|
|
323
432
|
});
|
|
324
433
|
});
|
package/src/client.ts
CHANGED
|
@@ -1,20 +1,94 @@
|
|
|
1
|
+
import type { Agent } from "node:https";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
1
3
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
readPluginPackageVersion,
|
|
6
|
+
resolveAmbientNodeProxyAgent,
|
|
7
|
+
} from "openclaw/plugin-sdk/extension-shared";
|
|
3
8
|
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
|
4
9
|
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const pluginVersion = readPluginPackageVersion({ require });
|
|
12
|
+
|
|
13
|
+
export { pluginVersion };
|
|
14
|
+
|
|
15
|
+
const FEISHU_USER_AGENT = `openclaw-feishu-builtin/${pluginVersion}/${process.platform}`;
|
|
16
|
+
export { FEISHU_USER_AGENT };
|
|
17
|
+
|
|
18
|
+
const FEISHU_WS_CONFIG = {
|
|
19
|
+
PingInterval: 30,
|
|
20
|
+
PingTimeout: 3,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/** User-Agent header value for all Feishu API requests. */
|
|
24
|
+
export function getFeishuUserAgent(): string {
|
|
25
|
+
return FEISHU_USER_AGENT;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type FeishuClientSdk = Pick<
|
|
29
|
+
typeof Lark,
|
|
30
|
+
| "AppType"
|
|
31
|
+
| "Client"
|
|
32
|
+
| "defaultHttpInstance"
|
|
33
|
+
| "Domain"
|
|
34
|
+
| "EventDispatcher"
|
|
35
|
+
| "LoggerLevel"
|
|
36
|
+
| "WSClient"
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
const defaultFeishuClientSdk: FeishuClientSdk = {
|
|
40
|
+
AppType: Lark.AppType,
|
|
41
|
+
Client: Lark.Client,
|
|
42
|
+
defaultHttpInstance: Lark.defaultHttpInstance,
|
|
43
|
+
Domain: Lark.Domain,
|
|
44
|
+
EventDispatcher: Lark.EventDispatcher,
|
|
45
|
+
LoggerLevel: Lark.LoggerLevel,
|
|
46
|
+
WSClient: Lark.WSClient,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let feishuClientSdk: FeishuClientSdk = defaultFeishuClientSdk;
|
|
50
|
+
|
|
51
|
+
// Override the SDK's default User-Agent interceptor.
|
|
52
|
+
// The Lark SDK registers an axios request interceptor that sets
|
|
53
|
+
// 'oapi-node-sdk/1.0.0'. Axios request interceptors execute in LIFO order
|
|
54
|
+
// (last-registered runs first), so simply appending ours doesn't work — the
|
|
55
|
+
// SDK's interceptor would run last and overwrite our UA. We must clear
|
|
56
|
+
// handlers[] first, then register our own as the sole interceptor.
|
|
57
|
+
//
|
|
58
|
+
// Risk is low: the SDK only registers one interceptor (UA) at init time, and
|
|
59
|
+
// we clear it at module load before any other code can register handlers.
|
|
60
|
+
// If a future SDK version adds more interceptors, the upgrade will need
|
|
61
|
+
// compatibility verification regardless.
|
|
62
|
+
{
|
|
63
|
+
const inst = Lark.defaultHttpInstance as {
|
|
64
|
+
interceptors?: {
|
|
65
|
+
request: { handlers: unknown[]; use: (fn: (req: unknown) => unknown) => void };
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
if (inst.interceptors?.request) {
|
|
69
|
+
inst.interceptors.request.handlers = [];
|
|
70
|
+
inst.interceptors.request.use((req: unknown) => {
|
|
71
|
+
const r = req as { headers?: Record<string, string> };
|
|
72
|
+
if (r.headers) {
|
|
73
|
+
r.headers["User-Agent"] = getFeishuUserAgent();
|
|
74
|
+
}
|
|
75
|
+
return req;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
5
80
|
/** Default HTTP timeout for Feishu API requests (30 seconds). */
|
|
6
81
|
export const FEISHU_HTTP_TIMEOUT_MS = 30_000;
|
|
7
82
|
export const FEISHU_HTTP_TIMEOUT_MAX_MS = 300_000;
|
|
8
83
|
export const FEISHU_HTTP_TIMEOUT_ENV_VAR = "OPENCLAW_FEISHU_HTTP_TIMEOUT_MS";
|
|
9
84
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return new HttpsProxyAgent(proxyUrl);
|
|
85
|
+
type FeishuHttpInstanceLike = Pick<
|
|
86
|
+
typeof feishuClientSdk.defaultHttpInstance,
|
|
87
|
+
"request" | "get" | "post" | "put" | "patch" | "delete" | "head" | "options"
|
|
88
|
+
>;
|
|
89
|
+
|
|
90
|
+
async function getWsProxyAgent() {
|
|
91
|
+
return resolveAmbientNodeProxyAgent<Agent>();
|
|
18
92
|
}
|
|
19
93
|
|
|
20
94
|
// Multi-account client cache
|
|
@@ -28,21 +102,21 @@ const clientCache = new Map<
|
|
|
28
102
|
|
|
29
103
|
function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
|
30
104
|
if (domain === "lark") {
|
|
31
|
-
return
|
|
105
|
+
return feishuClientSdk.Domain.Lark;
|
|
32
106
|
}
|
|
33
107
|
if (domain === "feishu" || !domain) {
|
|
34
|
-
return
|
|
108
|
+
return feishuClientSdk.Domain.Feishu;
|
|
35
109
|
}
|
|
36
110
|
return domain.replace(/\/+$/, ""); // Custom URL for private deployment
|
|
37
111
|
}
|
|
38
112
|
|
|
39
113
|
/**
|
|
40
114
|
* Create an HTTP instance that delegates to the Lark SDK's default instance
|
|
41
|
-
* but injects a default request timeout to prevent
|
|
42
|
-
*
|
|
115
|
+
* but injects a default request timeout and User-Agent header to prevent
|
|
116
|
+
* indefinite hangs and set a standardized User-Agent per OAPI best practices.
|
|
43
117
|
*/
|
|
44
118
|
function createTimeoutHttpInstance(defaultTimeoutMs: number): Lark.HttpInstance {
|
|
45
|
-
const base:
|
|
119
|
+
const base: FeishuHttpInstanceLike = feishuClientSdk.defaultHttpInstance;
|
|
46
120
|
|
|
47
121
|
function injectTimeout<D>(opts?: Lark.HttpRequestOptions<D>): Lark.HttpRequestOptions<D> {
|
|
48
122
|
return { timeout: defaultTimeoutMs, ...opts } as Lark.HttpRequestOptions<D>;
|
|
@@ -129,10 +203,10 @@ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client
|
|
|
129
203
|
}
|
|
130
204
|
|
|
131
205
|
// Create new client with timeout-aware HTTP instance
|
|
132
|
-
const client = new
|
|
206
|
+
const client = new feishuClientSdk.Client({
|
|
133
207
|
appId,
|
|
134
208
|
appSecret,
|
|
135
|
-
appType:
|
|
209
|
+
appType: feishuClientSdk.AppType.SelfBuild,
|
|
136
210
|
domain: resolveDomain(domain),
|
|
137
211
|
httpInstance: createTimeoutHttpInstance(defaultHttpTimeoutMs),
|
|
138
212
|
});
|
|
@@ -146,24 +220,36 @@ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client
|
|
|
146
220
|
return client;
|
|
147
221
|
}
|
|
148
222
|
|
|
223
|
+
export type FeishuWsClientCallbacks = Pick<
|
|
224
|
+
ConstructorParameters<typeof feishuClientSdk.WSClient>[0],
|
|
225
|
+
"onError" | "onReady" | "onReconnected" | "onReconnecting"
|
|
226
|
+
>;
|
|
227
|
+
|
|
149
228
|
/**
|
|
150
229
|
* Create a Feishu WebSocket client for an account.
|
|
151
230
|
* Note: WSClient is not cached since each call creates a new connection.
|
|
152
231
|
*/
|
|
153
|
-
export function createFeishuWSClient(
|
|
232
|
+
export async function createFeishuWSClient(
|
|
233
|
+
account: ResolvedFeishuAccount,
|
|
234
|
+
callbacks: FeishuWsClientCallbacks = {},
|
|
235
|
+
): Promise<Lark.WSClient> {
|
|
154
236
|
const { accountId, appId, appSecret, domain } = account;
|
|
155
237
|
|
|
156
238
|
if (!appId || !appSecret) {
|
|
157
239
|
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
|
158
240
|
}
|
|
159
241
|
|
|
160
|
-
const agent = getWsProxyAgent();
|
|
161
|
-
return new
|
|
242
|
+
const agent = await getWsProxyAgent();
|
|
243
|
+
return new feishuClientSdk.WSClient({
|
|
162
244
|
appId,
|
|
163
245
|
appSecret,
|
|
164
246
|
domain: resolveDomain(domain),
|
|
165
|
-
|
|
247
|
+
...callbacks,
|
|
248
|
+
loggerLevel: feishuClientSdk.LoggerLevel.info,
|
|
249
|
+
wsConfig: FEISHU_WS_CONFIG,
|
|
166
250
|
...(agent ? { agent } : {}),
|
|
251
|
+
} as ConstructorParameters<typeof feishuClientSdk.WSClient>[0] & {
|
|
252
|
+
wsConfig: typeof FEISHU_WS_CONFIG;
|
|
167
253
|
});
|
|
168
254
|
}
|
|
169
255
|
|
|
@@ -171,7 +257,7 @@ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSCli
|
|
|
171
257
|
* Create an event dispatcher for an account.
|
|
172
258
|
*/
|
|
173
259
|
export function createEventDispatcher(account: ResolvedFeishuAccount): Lark.EventDispatcher {
|
|
174
|
-
return new
|
|
260
|
+
return new feishuClientSdk.EventDispatcher({
|
|
175
261
|
encryptKey: account.encryptKey,
|
|
176
262
|
verificationToken: account.verificationToken,
|
|
177
263
|
});
|
|
@@ -194,3 +280,11 @@ export function clearClientCache(accountId?: string): void {
|
|
|
194
280
|
clientCache.clear();
|
|
195
281
|
}
|
|
196
282
|
}
|
|
283
|
+
|
|
284
|
+
export function setFeishuClientRuntimeForTest(overrides?: {
|
|
285
|
+
sdk?: Partial<FeishuClientSdk>;
|
|
286
|
+
}): void {
|
|
287
|
+
feishuClientSdk = overrides?.sdk
|
|
288
|
+
? { ...defaultFeishuClientSdk, ...overrides.sdk }
|
|
289
|
+
: defaultFeishuClientSdk;
|
|
290
|
+
}
|