@openclaw/feishu 2026.3.1 → 2026.3.2
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/package.json +1 -1
- package/src/accounts.test.ts +74 -3
- package/src/accounts.ts +69 -10
- package/src/bot.checkBotMentioned.test.ts +1 -1
- package/src/bot.test.ts +390 -29
- package/src/bot.ts +131 -61
- package/src/channel.ts +20 -4
- package/src/client.test.ts +14 -0
- package/src/config-schema.test.ts +19 -0
- package/src/config-schema.ts +13 -9
- package/src/dedup.ts +47 -1
- package/src/doc-schema.ts +16 -22
- package/src/docx.account-selection.test.ts +7 -13
- package/src/docx.test.ts +41 -189
- package/src/media.test.ts +104 -1
- package/src/media.ts +21 -1
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +266 -18
- package/src/monitor.reaction.test.ts +345 -2
- package/src/monitor.startup.test.ts +17 -1
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +84 -8
- package/src/monitor.test-mocks.ts +12 -0
- package/src/monitor.webhook-security.test.ts +26 -9
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.ts +144 -52
- package/src/probe.test.ts +38 -20
- package/src/probe.ts +57 -37
- package/src/reply-dispatcher.test.ts +41 -0
- package/src/reply-dispatcher.ts +26 -7
- package/src/secret-input.ts +19 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +6 -2
- package/src/targets.test.ts +29 -0
- package/src/targets.ts +21 -1
- package/src/types.ts +9 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js";
|
|
3
4
|
|
|
4
5
|
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
|
5
6
|
|
|
@@ -12,7 +13,22 @@ vi.mock("./client.js", () => ({
|
|
|
12
13
|
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
|
|
13
14
|
}));
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
vi.mock("./runtime.js", () => ({
|
|
17
|
+
getFeishuRuntime: () => ({
|
|
18
|
+
channel: {
|
|
19
|
+
debounce: {
|
|
20
|
+
resolveInboundDebounceMs: () => 0,
|
|
21
|
+
createInboundDebouncer: () => ({
|
|
22
|
+
enqueue: async () => {},
|
|
23
|
+
flushKey: async () => {},
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
text: {
|
|
27
|
+
hasControlCommand: () => false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
16
32
|
|
|
17
33
|
function buildMultiAccountWebsocketConfig(accountIds: string[]): ClawdbotConfig {
|
|
18
34
|
return {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolveFeishuWebhookAnomalyDefaultsForTest,
|
|
4
|
+
resolveFeishuWebhookRateLimitDefaultsForTest,
|
|
5
|
+
} from "./monitor.state.js";
|
|
6
|
+
|
|
7
|
+
describe("feishu monitor state defaults", () => {
|
|
8
|
+
it("falls back to hard defaults when sdk defaults are missing", () => {
|
|
9
|
+
expect(resolveFeishuWebhookRateLimitDefaultsForTest(undefined)).toEqual({
|
|
10
|
+
windowMs: 60_000,
|
|
11
|
+
maxRequests: 120,
|
|
12
|
+
maxTrackedKeys: 4_096,
|
|
13
|
+
});
|
|
14
|
+
expect(resolveFeishuWebhookAnomalyDefaultsForTest(undefined)).toEqual({
|
|
15
|
+
maxTrackedKeys: 4_096,
|
|
16
|
+
ttlMs: 21_600_000,
|
|
17
|
+
logEvery: 25,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps valid sdk values and repairs invalid fields", () => {
|
|
22
|
+
expect(
|
|
23
|
+
resolveFeishuWebhookRateLimitDefaultsForTest({
|
|
24
|
+
windowMs: 45_000,
|
|
25
|
+
maxRequests: 0,
|
|
26
|
+
maxTrackedKeys: -1,
|
|
27
|
+
}),
|
|
28
|
+
).toEqual({
|
|
29
|
+
windowMs: 45_000,
|
|
30
|
+
maxRequests: 120,
|
|
31
|
+
maxTrackedKeys: 4_096,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
resolveFeishuWebhookAnomalyDefaultsForTest({
|
|
36
|
+
maxTrackedKeys: 2048,
|
|
37
|
+
ttlMs: Number.NaN,
|
|
38
|
+
logEvery: 10,
|
|
39
|
+
}),
|
|
40
|
+
).toEqual({
|
|
41
|
+
maxTrackedKeys: 2048,
|
|
42
|
+
ttlMs: 21_600_000,
|
|
43
|
+
logEvery: 10,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/monitor.state.ts
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
createFixedWindowRateLimiter,
|
|
5
5
|
createWebhookAnomalyTracker,
|
|
6
6
|
type RuntimeEnv,
|
|
7
|
-
WEBHOOK_ANOMALY_COUNTER_DEFAULTS,
|
|
8
|
-
WEBHOOK_RATE_LIMIT_DEFAULTS,
|
|
7
|
+
WEBHOOK_ANOMALY_COUNTER_DEFAULTS as WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK,
|
|
8
|
+
WEBHOOK_RATE_LIMIT_DEFAULTS as WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK,
|
|
9
9
|
} from "openclaw/plugin-sdk";
|
|
10
10
|
|
|
11
11
|
export const wsClients = new Map<string, Lark.WSClient>();
|
|
@@ -15,16 +15,92 @@ export const botOpenIds = new Map<string, string>();
|
|
|
15
15
|
export const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
16
16
|
export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
|
17
17
|
|
|
18
|
+
type WebhookRateLimitDefaults = {
|
|
19
|
+
windowMs: number;
|
|
20
|
+
maxRequests: number;
|
|
21
|
+
maxTrackedKeys: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type WebhookAnomalyDefaults = {
|
|
25
|
+
maxTrackedKeys: number;
|
|
26
|
+
ttlMs: number;
|
|
27
|
+
logEvery: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS: WebhookRateLimitDefaults = {
|
|
31
|
+
windowMs: 60_000,
|
|
32
|
+
maxRequests: 120,
|
|
33
|
+
maxTrackedKeys: 4_096,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS: WebhookAnomalyDefaults = {
|
|
37
|
+
maxTrackedKeys: 4_096,
|
|
38
|
+
ttlMs: 6 * 60 * 60_000,
|
|
39
|
+
logEvery: 25,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function coercePositiveInt(value: unknown, fallback: number): number {
|
|
43
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
const normalized = Math.floor(value);
|
|
47
|
+
return normalized > 0 ? normalized : fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveFeishuWebhookRateLimitDefaultsForTest(
|
|
51
|
+
defaults: unknown,
|
|
52
|
+
): WebhookRateLimitDefaults {
|
|
53
|
+
const resolved = defaults as Partial<WebhookRateLimitDefaults> | null | undefined;
|
|
54
|
+
return {
|
|
55
|
+
windowMs: coercePositiveInt(
|
|
56
|
+
resolved?.windowMs,
|
|
57
|
+
FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.windowMs,
|
|
58
|
+
),
|
|
59
|
+
maxRequests: coercePositiveInt(
|
|
60
|
+
resolved?.maxRequests,
|
|
61
|
+
FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.maxRequests,
|
|
62
|
+
),
|
|
63
|
+
maxTrackedKeys: coercePositiveInt(
|
|
64
|
+
resolved?.maxTrackedKeys,
|
|
65
|
+
FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.maxTrackedKeys,
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveFeishuWebhookAnomalyDefaultsForTest(
|
|
71
|
+
defaults: unknown,
|
|
72
|
+
): WebhookAnomalyDefaults {
|
|
73
|
+
const resolved = defaults as Partial<WebhookAnomalyDefaults> | null | undefined;
|
|
74
|
+
return {
|
|
75
|
+
maxTrackedKeys: coercePositiveInt(
|
|
76
|
+
resolved?.maxTrackedKeys,
|
|
77
|
+
FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.maxTrackedKeys,
|
|
78
|
+
),
|
|
79
|
+
ttlMs: coercePositiveInt(resolved?.ttlMs, FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.ttlMs),
|
|
80
|
+
logEvery: coercePositiveInt(
|
|
81
|
+
resolved?.logEvery,
|
|
82
|
+
FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.logEvery,
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const feishuWebhookRateLimitDefaults = resolveFeishuWebhookRateLimitDefaultsForTest(
|
|
88
|
+
WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK,
|
|
89
|
+
);
|
|
90
|
+
const feishuWebhookAnomalyDefaults = resolveFeishuWebhookAnomalyDefaultsForTest(
|
|
91
|
+
WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK,
|
|
92
|
+
);
|
|
93
|
+
|
|
18
94
|
export const feishuWebhookRateLimiter = createFixedWindowRateLimiter({
|
|
19
|
-
windowMs:
|
|
20
|
-
maxRequests:
|
|
21
|
-
maxTrackedKeys:
|
|
95
|
+
windowMs: feishuWebhookRateLimitDefaults.windowMs,
|
|
96
|
+
maxRequests: feishuWebhookRateLimitDefaults.maxRequests,
|
|
97
|
+
maxTrackedKeys: feishuWebhookRateLimitDefaults.maxTrackedKeys,
|
|
22
98
|
});
|
|
23
99
|
|
|
24
100
|
const feishuWebhookAnomalyTracker = createWebhookAnomalyTracker({
|
|
25
|
-
maxTrackedKeys:
|
|
26
|
-
ttlMs:
|
|
27
|
-
logEvery:
|
|
101
|
+
maxTrackedKeys: feishuWebhookAnomalyDefaults.maxTrackedKeys,
|
|
102
|
+
ttlMs: feishuWebhookAnomalyDefaults.ttlMs,
|
|
103
|
+
logEvery: feishuWebhookAnomalyDefaults.logEvery,
|
|
28
104
|
});
|
|
29
105
|
|
|
30
106
|
export function clearFeishuWebhookRateLimitStateForTest(): void {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
export const probeFeishuMock: ReturnType<typeof vi.fn> = vi.fn();
|
|
4
|
+
|
|
5
|
+
vi.mock("./probe.js", () => ({
|
|
6
|
+
probeFeishu: probeFeishuMock,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("./client.js", () => ({
|
|
10
|
+
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
|
|
11
|
+
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
|
|
12
|
+
}));
|
|
@@ -5,15 +5,6 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
|
|
6
6
|
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
|
7
7
|
|
|
8
|
-
vi.mock("@larksuiteoapi/node-sdk", () => ({
|
|
9
|
-
adaptDefault: vi.fn(
|
|
10
|
-
() => (_req: unknown, res: { statusCode?: number; end: (s: string) => void }) => {
|
|
11
|
-
res.statusCode = 200;
|
|
12
|
-
res.end("ok");
|
|
13
|
-
},
|
|
14
|
-
),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
8
|
vi.mock("./probe.js", () => ({
|
|
18
9
|
probeFeishu: probeFeishuMock,
|
|
19
10
|
}));
|
|
@@ -23,6 +14,32 @@ vi.mock("./client.js", () => ({
|
|
|
23
14
|
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
|
|
24
15
|
}));
|
|
25
16
|
|
|
17
|
+
vi.mock("./runtime.js", () => ({
|
|
18
|
+
getFeishuRuntime: () => ({
|
|
19
|
+
channel: {
|
|
20
|
+
debounce: {
|
|
21
|
+
resolveInboundDebounceMs: () => 0,
|
|
22
|
+
createInboundDebouncer: () => ({
|
|
23
|
+
enqueue: async () => {},
|
|
24
|
+
flushKey: async () => {},
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
text: {
|
|
28
|
+
hasControlCommand: () => false,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("@larksuiteoapi/node-sdk", () => ({
|
|
35
|
+
adaptDefault: vi.fn(
|
|
36
|
+
() => (_req: unknown, res: { statusCode?: number; end: (s: string) => void }) => {
|
|
37
|
+
res.statusCode = 200;
|
|
38
|
+
res.end("ok");
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
}));
|
|
42
|
+
|
|
26
43
|
import {
|
|
27
44
|
clearFeishuWebhookRateLimitStateForTest,
|
|
28
45
|
getFeishuWebhookRateLimitStateSizeForTest,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { feishuOnboardingAdapter } from "./onboarding.js";
|
|
4
|
+
|
|
5
|
+
describe("feishu onboarding status", () => {
|
|
6
|
+
it("treats SecretRef appSecret as configured when appId is present", async () => {
|
|
7
|
+
const status = await feishuOnboardingAdapter.getStatus({
|
|
8
|
+
cfg: {
|
|
9
|
+
channels: {
|
|
10
|
+
feishu: {
|
|
11
|
+
appId: "cli_a123456",
|
|
12
|
+
appSecret: {
|
|
13
|
+
source: "env",
|
|
14
|
+
provider: "default",
|
|
15
|
+
id: "FEISHU_APP_SECRET",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} as OpenClawConfig,
|
|
20
|
+
accountOverrides: {},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(status.configured).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
package/src/onboarding.ts
CHANGED
|
@@ -3,9 +3,16 @@ import type {
|
|
|
3
3
|
ChannelOnboardingDmPolicy,
|
|
4
4
|
ClawdbotConfig,
|
|
5
5
|
DmPolicy,
|
|
6
|
+
SecretInput,
|
|
6
7
|
WizardPrompter,
|
|
7
8
|
} from "openclaw/plugin-sdk";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
addWildcardAllowFrom,
|
|
11
|
+
DEFAULT_ACCOUNT_ID,
|
|
12
|
+
formatDocsLink,
|
|
13
|
+
hasConfiguredSecretInput,
|
|
14
|
+
promptSingleChannelSecretInput,
|
|
15
|
+
} from "openclaw/plugin-sdk";
|
|
9
16
|
import { resolveFeishuCredentials } from "./accounts.js";
|
|
10
17
|
import { probeFeishu } from "./probe.js";
|
|
11
18
|
import type { FeishuConfig } from "./types.js";
|
|
@@ -104,23 +111,18 @@ async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void>
|
|
|
104
111
|
);
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
async function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}> {
|
|
114
|
+
async function promptFeishuAppId(params: {
|
|
115
|
+
prompter: WizardPrompter;
|
|
116
|
+
initialValue?: string;
|
|
117
|
+
}): Promise<string> {
|
|
111
118
|
const appId = String(
|
|
112
|
-
await prompter.text({
|
|
119
|
+
await params.prompter.text({
|
|
113
120
|
message: "Enter Feishu App ID",
|
|
121
|
+
initialValue: params.initialValue,
|
|
114
122
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
115
123
|
}),
|
|
116
124
|
).trim();
|
|
117
|
-
|
|
118
|
-
await prompter.text({
|
|
119
|
-
message: "Enter Feishu App Secret",
|
|
120
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
121
|
-
}),
|
|
122
|
-
).trim();
|
|
123
|
-
return { appId, appSecret };
|
|
125
|
+
return appId;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
function setFeishuGroupPolicy(
|
|
@@ -167,13 +169,30 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
167
169
|
channel,
|
|
168
170
|
getStatus: async ({ cfg }) => {
|
|
169
171
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
170
|
-
const
|
|
172
|
+
const topLevelConfigured = Boolean(
|
|
173
|
+
feishuCfg?.appId?.trim() && hasConfiguredSecretInput(feishuCfg?.appSecret),
|
|
174
|
+
);
|
|
175
|
+
const accountConfigured = Object.values(feishuCfg?.accounts ?? {}).some((account) => {
|
|
176
|
+
if (!account || typeof account !== "object") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const accountAppId =
|
|
180
|
+
typeof account.appId === "string" ? account.appId.trim() : feishuCfg?.appId?.trim();
|
|
181
|
+
const accountSecretConfigured =
|
|
182
|
+
hasConfiguredSecretInput(account.appSecret) ||
|
|
183
|
+
hasConfiguredSecretInput(feishuCfg?.appSecret);
|
|
184
|
+
return Boolean(accountAppId && accountSecretConfigured);
|
|
185
|
+
});
|
|
186
|
+
const configured = topLevelConfigured || accountConfigured;
|
|
187
|
+
const resolvedCredentials = resolveFeishuCredentials(feishuCfg, {
|
|
188
|
+
allowUnresolvedSecretRef: true,
|
|
189
|
+
});
|
|
171
190
|
|
|
172
191
|
// Try to probe if configured
|
|
173
192
|
let probeResult = null;
|
|
174
|
-
if (configured &&
|
|
193
|
+
if (configured && resolvedCredentials) {
|
|
175
194
|
try {
|
|
176
|
-
probeResult = await probeFeishu(
|
|
195
|
+
probeResult = await probeFeishu(resolvedCredentials);
|
|
177
196
|
} catch {
|
|
178
197
|
// Ignore probe errors
|
|
179
198
|
}
|
|
@@ -201,52 +220,53 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
201
220
|
|
|
202
221
|
configure: async ({ cfg, prompter }) => {
|
|
203
222
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
204
|
-
const resolved = resolveFeishuCredentials(feishuCfg
|
|
205
|
-
|
|
223
|
+
const resolved = resolveFeishuCredentials(feishuCfg, {
|
|
224
|
+
allowUnresolvedSecretRef: true,
|
|
225
|
+
});
|
|
226
|
+
const hasConfigSecret = hasConfiguredSecretInput(feishuCfg?.appSecret);
|
|
227
|
+
const hasConfigCreds = Boolean(feishuCfg?.appId?.trim() && hasConfigSecret);
|
|
206
228
|
const canUseEnv = Boolean(
|
|
207
229
|
!hasConfigCreds && process.env.FEISHU_APP_ID?.trim() && process.env.FEISHU_APP_SECRET?.trim(),
|
|
208
230
|
);
|
|
209
231
|
|
|
210
232
|
let next = cfg;
|
|
211
233
|
let appId: string | null = null;
|
|
212
|
-
let appSecret:
|
|
234
|
+
let appSecret: SecretInput | null = null;
|
|
235
|
+
let appSecretProbeValue: string | null = null;
|
|
213
236
|
|
|
214
237
|
if (!resolved) {
|
|
215
238
|
await noteFeishuCredentialHelp(prompter);
|
|
216
239
|
}
|
|
217
240
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
const appSecretResult = await promptSingleChannelSecretInput({
|
|
242
|
+
cfg: next,
|
|
243
|
+
prompter,
|
|
244
|
+
providerHint: "feishu",
|
|
245
|
+
credentialLabel: "App Secret",
|
|
246
|
+
accountConfigured: Boolean(resolved),
|
|
247
|
+
canUseEnv,
|
|
248
|
+
hasConfigToken: hasConfigSecret,
|
|
249
|
+
envPrompt: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
250
|
+
keepPrompt: "Feishu App Secret already configured. Keep it?",
|
|
251
|
+
inputPrompt: "Enter Feishu App Secret",
|
|
252
|
+
preferredEnvVar: "FEISHU_APP_SECRET",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (appSecretResult.action === "use-env") {
|
|
256
|
+
next = {
|
|
257
|
+
...next,
|
|
258
|
+
channels: {
|
|
259
|
+
...next.channels,
|
|
260
|
+
feishu: { ...next.channels?.feishu, enabled: true },
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
} else if (appSecretResult.action === "set") {
|
|
264
|
+
appSecret = appSecretResult.value;
|
|
265
|
+
appSecretProbeValue = appSecretResult.resolvedValue;
|
|
266
|
+
appId = await promptFeishuAppId({
|
|
267
|
+
prompter,
|
|
268
|
+
initialValue: feishuCfg?.appId?.trim() || process.env.FEISHU_APP_ID?.trim(),
|
|
240
269
|
});
|
|
241
|
-
if (!keep) {
|
|
242
|
-
const entered = await promptFeishuCredentials(prompter);
|
|
243
|
-
appId = entered.appId;
|
|
244
|
-
appSecret = entered.appSecret;
|
|
245
|
-
}
|
|
246
|
-
} else {
|
|
247
|
-
const entered = await promptFeishuCredentials(prompter);
|
|
248
|
-
appId = entered.appId;
|
|
249
|
-
appSecret = entered.appSecret;
|
|
250
270
|
}
|
|
251
271
|
|
|
252
272
|
if (appId && appSecret) {
|
|
@@ -264,9 +284,12 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
264
284
|
};
|
|
265
285
|
|
|
266
286
|
// Test connection
|
|
267
|
-
const testCfg = next.channels?.feishu as FeishuConfig;
|
|
268
287
|
try {
|
|
269
|
-
const probe = await probeFeishu(
|
|
288
|
+
const probe = await probeFeishu({
|
|
289
|
+
appId,
|
|
290
|
+
appSecret: appSecretProbeValue ?? undefined,
|
|
291
|
+
domain: (next.channels?.feishu as FeishuConfig | undefined)?.domain,
|
|
292
|
+
});
|
|
270
293
|
if (probe.ok) {
|
|
271
294
|
await prompter.note(
|
|
272
295
|
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
@@ -283,6 +306,75 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
283
306
|
}
|
|
284
307
|
}
|
|
285
308
|
|
|
309
|
+
const currentMode =
|
|
310
|
+
(next.channels?.feishu as FeishuConfig | undefined)?.connectionMode ?? "websocket";
|
|
311
|
+
const connectionMode = (await prompter.select({
|
|
312
|
+
message: "Feishu connection mode",
|
|
313
|
+
options: [
|
|
314
|
+
{ value: "websocket", label: "WebSocket (default)" },
|
|
315
|
+
{ value: "webhook", label: "Webhook" },
|
|
316
|
+
],
|
|
317
|
+
initialValue: currentMode,
|
|
318
|
+
})) as "websocket" | "webhook";
|
|
319
|
+
next = {
|
|
320
|
+
...next,
|
|
321
|
+
channels: {
|
|
322
|
+
...next.channels,
|
|
323
|
+
feishu: {
|
|
324
|
+
...next.channels?.feishu,
|
|
325
|
+
connectionMode,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (connectionMode === "webhook") {
|
|
331
|
+
const currentVerificationToken = (next.channels?.feishu as FeishuConfig | undefined)
|
|
332
|
+
?.verificationToken;
|
|
333
|
+
const verificationTokenResult = await promptSingleChannelSecretInput({
|
|
334
|
+
cfg: next,
|
|
335
|
+
prompter,
|
|
336
|
+
providerHint: "feishu-webhook",
|
|
337
|
+
credentialLabel: "verification token",
|
|
338
|
+
accountConfigured: hasConfiguredSecretInput(currentVerificationToken),
|
|
339
|
+
canUseEnv: false,
|
|
340
|
+
hasConfigToken: hasConfiguredSecretInput(currentVerificationToken),
|
|
341
|
+
envPrompt: "",
|
|
342
|
+
keepPrompt: "Feishu verification token already configured. Keep it?",
|
|
343
|
+
inputPrompt: "Enter Feishu verification token",
|
|
344
|
+
preferredEnvVar: "FEISHU_VERIFICATION_TOKEN",
|
|
345
|
+
});
|
|
346
|
+
if (verificationTokenResult.action === "set") {
|
|
347
|
+
next = {
|
|
348
|
+
...next,
|
|
349
|
+
channels: {
|
|
350
|
+
...next.channels,
|
|
351
|
+
feishu: {
|
|
352
|
+
...next.channels?.feishu,
|
|
353
|
+
verificationToken: verificationTokenResult.value,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath;
|
|
359
|
+
const webhookPath = String(
|
|
360
|
+
await prompter.text({
|
|
361
|
+
message: "Feishu webhook path",
|
|
362
|
+
initialValue: currentWebhookPath ?? "/feishu/events",
|
|
363
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
364
|
+
}),
|
|
365
|
+
).trim();
|
|
366
|
+
next = {
|
|
367
|
+
...next,
|
|
368
|
+
channels: {
|
|
369
|
+
...next.channels,
|
|
370
|
+
feishu: {
|
|
371
|
+
...next.channels?.feishu,
|
|
372
|
+
webhookPath,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
286
378
|
// Domain selection
|
|
287
379
|
const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu";
|
|
288
380
|
const domain = await prompter.select({
|
package/src/probe.test.ts
CHANGED
|
@@ -59,7 +59,7 @@ describe("probeFeishu", () => {
|
|
|
59
59
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it("
|
|
62
|
+
it("passes the probe timeout to the Feishu request", async () => {
|
|
63
63
|
const requestFn = setupClient({
|
|
64
64
|
code: 0,
|
|
65
65
|
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
@@ -105,7 +105,6 @@ describe("probeFeishu", () => {
|
|
|
105
105
|
expect(result).toMatchObject({ ok: false, error: "probe aborted" });
|
|
106
106
|
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
|
107
107
|
});
|
|
108
|
-
|
|
109
108
|
it("returns cached result on subsequent calls within TTL", async () => {
|
|
110
109
|
const requestFn = setupClient({
|
|
111
110
|
code: 0,
|
|
@@ -133,7 +132,7 @@ describe("probeFeishu", () => {
|
|
|
133
132
|
await probeFeishu(creds);
|
|
134
133
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
135
134
|
|
|
136
|
-
// Advance time past the
|
|
135
|
+
// Advance time past the success TTL
|
|
137
136
|
vi.advanceTimersByTime(10 * 60 * 1000 + 1);
|
|
138
137
|
|
|
139
138
|
await probeFeishu(creds);
|
|
@@ -143,29 +142,48 @@ describe("probeFeishu", () => {
|
|
|
143
142
|
}
|
|
144
143
|
});
|
|
145
144
|
|
|
146
|
-
it("
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
it("caches failed probe results (API error) for the error TTL", async () => {
|
|
146
|
+
vi.useFakeTimers();
|
|
147
|
+
try {
|
|
148
|
+
const requestFn = makeRequestFn({ code: 99, msg: "token expired" });
|
|
149
|
+
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
|
149
150
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
const creds = { appId: "cli_123", appSecret: "secret" };
|
|
152
|
+
const first = await probeFeishu(creds);
|
|
153
|
+
const second = await probeFeishu(creds);
|
|
154
|
+
expect(first).toMatchObject({ ok: false, error: "API error: token expired" });
|
|
155
|
+
expect(second).toMatchObject({ ok: false, error: "API error: token expired" });
|
|
156
|
+
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
vi.advanceTimersByTime(60 * 1000 + 1);
|
|
159
|
+
|
|
160
|
+
await probeFeishu(creds);
|
|
161
|
+
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
162
|
+
} finally {
|
|
163
|
+
vi.useRealTimers();
|
|
164
|
+
}
|
|
157
165
|
});
|
|
158
166
|
|
|
159
|
-
it("
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
it("caches thrown request errors for the error TTL", async () => {
|
|
168
|
+
vi.useFakeTimers();
|
|
169
|
+
try {
|
|
170
|
+
const requestFn = vi.fn().mockRejectedValue(new Error("network error"));
|
|
171
|
+
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
|
162
172
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
const creds = { appId: "cli_123", appSecret: "secret" };
|
|
174
|
+
const first = await probeFeishu(creds);
|
|
175
|
+
const second = await probeFeishu(creds);
|
|
176
|
+
expect(first).toMatchObject({ ok: false, error: "network error" });
|
|
177
|
+
expect(second).toMatchObject({ ok: false, error: "network error" });
|
|
178
|
+
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
166
179
|
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
vi.advanceTimersByTime(60 * 1000 + 1);
|
|
181
|
+
|
|
182
|
+
await probeFeishu(creds);
|
|
183
|
+
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
184
|
+
} finally {
|
|
185
|
+
vi.useRealTimers();
|
|
186
|
+
}
|
|
169
187
|
});
|
|
170
188
|
|
|
171
189
|
it("caches per account independently", async () => {
|