@openclaw/feishu 2026.3.11 → 2026.3.13
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 +40 -16
- package/src/accounts.ts +5 -1
- package/src/bot.ts +20 -11
- package/src/channel.ts +2 -2
- package/src/config-schema.test.ts +67 -16
- package/src/config-schema.ts +30 -9
- package/src/dedup.ts +103 -0
- package/src/media.test.ts +38 -61
- package/src/media.ts +64 -76
- package/src/monitor.account.ts +39 -22
- package/src/monitor.reaction.test.ts +134 -65
- package/src/monitor.startup.test.ts +16 -30
- package/src/monitor.transport.ts +104 -6
- package/src/monitor.webhook-e2e.test.ts +214 -0
- package/src/monitor.webhook-security.test.ts +23 -92
- package/src/monitor.webhook.test-helpers.ts +98 -0
- package/src/onboarding.ts +31 -0
- package/src/outbound.test.ts +11 -16
- package/src/probe.test.ts +112 -113
- package/src/reactions.ts +20 -27
- package/src/reply-dispatcher.test.ts +65 -143
- package/src/reply-dispatcher.ts +37 -40
- package/src/send.reply-fallback.test.ts +50 -40
- package/src/send.ts +95 -91
- package/src/types.ts +14 -0
package/src/onboarding.ts
CHANGED
|
@@ -370,6 +370,37 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
370
370
|
},
|
|
371
371
|
};
|
|
372
372
|
}
|
|
373
|
+
const currentEncryptKey = (next.channels?.feishu as FeishuConfig | undefined)?.encryptKey;
|
|
374
|
+
const encryptKeyPromptState = buildSingleChannelSecretPromptState({
|
|
375
|
+
accountConfigured: hasConfiguredSecretInput(currentEncryptKey),
|
|
376
|
+
hasConfigToken: hasConfiguredSecretInput(currentEncryptKey),
|
|
377
|
+
allowEnv: false,
|
|
378
|
+
});
|
|
379
|
+
const encryptKeyResult = await promptSingleChannelSecretInput({
|
|
380
|
+
cfg: next,
|
|
381
|
+
prompter,
|
|
382
|
+
providerHint: "feishu-webhook",
|
|
383
|
+
credentialLabel: "encrypt key",
|
|
384
|
+
accountConfigured: encryptKeyPromptState.accountConfigured,
|
|
385
|
+
canUseEnv: encryptKeyPromptState.canUseEnv,
|
|
386
|
+
hasConfigToken: encryptKeyPromptState.hasConfigToken,
|
|
387
|
+
envPrompt: "",
|
|
388
|
+
keepPrompt: "Feishu encrypt key already configured. Keep it?",
|
|
389
|
+
inputPrompt: "Enter Feishu encrypt key",
|
|
390
|
+
preferredEnvVar: "FEISHU_ENCRYPT_KEY",
|
|
391
|
+
});
|
|
392
|
+
if (encryptKeyResult.action === "set") {
|
|
393
|
+
next = {
|
|
394
|
+
...next,
|
|
395
|
+
channels: {
|
|
396
|
+
...next.channels,
|
|
397
|
+
feishu: {
|
|
398
|
+
...next.channels?.feishu,
|
|
399
|
+
encryptKey: encryptKeyResult.value,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
373
404
|
const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath;
|
|
374
405
|
const webhookPath = String(
|
|
375
406
|
await prompter.text({
|
package/src/outbound.test.ts
CHANGED
|
@@ -29,12 +29,16 @@ vi.mock("./runtime.js", () => ({
|
|
|
29
29
|
import { feishuOutbound } from "./outbound.js";
|
|
30
30
|
const sendText = feishuOutbound.sendText!;
|
|
31
31
|
|
|
32
|
+
function resetOutboundMocks() {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
35
|
+
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
36
|
+
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
33
40
|
beforeEach(() => {
|
|
34
|
-
|
|
35
|
-
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
36
|
-
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
37
|
-
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
41
|
+
resetOutboundMocks();
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
async function createTmpImage(ext = ".png"): Promise<{ dir: string; file: string }> {
|
|
@@ -181,10 +185,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
181
185
|
|
|
182
186
|
describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
183
187
|
beforeEach(() => {
|
|
184
|
-
|
|
185
|
-
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
186
|
-
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
187
|
-
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
188
|
+
resetOutboundMocks();
|
|
188
189
|
});
|
|
189
190
|
|
|
190
191
|
it("forwards replyToId as replyToMessageId to sendMessageFeishu", async () => {
|
|
@@ -249,10 +250,7 @@ describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
|
249
250
|
|
|
250
251
|
describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
|
251
252
|
beforeEach(() => {
|
|
252
|
-
|
|
253
|
-
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
254
|
-
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
255
|
-
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
253
|
+
resetOutboundMocks();
|
|
256
254
|
});
|
|
257
255
|
|
|
258
256
|
it("forwards replyToId to sendMediaFeishu", async () => {
|
|
@@ -292,10 +290,7 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
|
|
292
290
|
|
|
293
291
|
describe("feishuOutbound.sendMedia renderMode", () => {
|
|
294
292
|
beforeEach(() => {
|
|
295
|
-
|
|
296
|
-
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
297
|
-
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
298
|
-
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
293
|
+
resetOutboundMocks();
|
|
299
294
|
});
|
|
300
295
|
|
|
301
296
|
it("uses markdown cards for captions when renderMode=card", async () => {
|
package/src/probe.test.ts
CHANGED
|
@@ -8,6 +8,22 @@ vi.mock("./client.js", () => ({
|
|
|
8
8
|
|
|
9
9
|
import { FEISHU_PROBE_REQUEST_TIMEOUT_MS, probeFeishu, clearProbeCache } from "./probe.js";
|
|
10
10
|
|
|
11
|
+
const DEFAULT_CREDS = { appId: "cli_123", appSecret: "secret" } as const; // pragma: allowlist secret
|
|
12
|
+
const DEFAULT_SUCCESS_RESPONSE = {
|
|
13
|
+
code: 0,
|
|
14
|
+
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
15
|
+
} as const;
|
|
16
|
+
const DEFAULT_SUCCESS_RESULT = {
|
|
17
|
+
ok: true,
|
|
18
|
+
appId: "cli_123",
|
|
19
|
+
botName: "TestBot",
|
|
20
|
+
botOpenId: "ou_abc123",
|
|
21
|
+
} as const;
|
|
22
|
+
const BOT1_RESPONSE = {
|
|
23
|
+
code: 0,
|
|
24
|
+
bot: { bot_name: "Bot1", open_id: "ou_1" },
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
11
27
|
function makeRequestFn(response: Record<string, unknown>) {
|
|
12
28
|
return vi.fn().mockResolvedValue(response);
|
|
13
29
|
}
|
|
@@ -18,6 +34,64 @@ function setupClient(response: Record<string, unknown>) {
|
|
|
18
34
|
return requestFn;
|
|
19
35
|
}
|
|
20
36
|
|
|
37
|
+
function setupSuccessClient() {
|
|
38
|
+
return setupClient(DEFAULT_SUCCESS_RESPONSE);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function expectDefaultSuccessResult(
|
|
42
|
+
creds = DEFAULT_CREDS,
|
|
43
|
+
expected: Awaited<ReturnType<typeof probeFeishu>> = DEFAULT_SUCCESS_RESULT,
|
|
44
|
+
) {
|
|
45
|
+
const result = await probeFeishu(creds);
|
|
46
|
+
expect(result).toEqual(expected);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function withFakeTimers(run: () => Promise<void>) {
|
|
50
|
+
vi.useFakeTimers();
|
|
51
|
+
try {
|
|
52
|
+
await run();
|
|
53
|
+
} finally {
|
|
54
|
+
vi.useRealTimers();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function expectErrorResultCached(params: {
|
|
59
|
+
requestFn: ReturnType<typeof vi.fn>;
|
|
60
|
+
expectedError: string;
|
|
61
|
+
ttlMs: number;
|
|
62
|
+
}) {
|
|
63
|
+
createFeishuClientMock.mockReturnValue({ request: params.requestFn });
|
|
64
|
+
|
|
65
|
+
const first = await probeFeishu(DEFAULT_CREDS);
|
|
66
|
+
const second = await probeFeishu(DEFAULT_CREDS);
|
|
67
|
+
expect(first).toMatchObject({ ok: false, error: params.expectedError });
|
|
68
|
+
expect(second).toMatchObject({ ok: false, error: params.expectedError });
|
|
69
|
+
expect(params.requestFn).toHaveBeenCalledTimes(1);
|
|
70
|
+
|
|
71
|
+
vi.advanceTimersByTime(params.ttlMs + 1);
|
|
72
|
+
|
|
73
|
+
await probeFeishu(DEFAULT_CREDS);
|
|
74
|
+
expect(params.requestFn).toHaveBeenCalledTimes(2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function expectFreshDefaultProbeAfter(
|
|
78
|
+
requestFn: ReturnType<typeof vi.fn>,
|
|
79
|
+
invalidate: () => void,
|
|
80
|
+
) {
|
|
81
|
+
await probeFeishu(DEFAULT_CREDS);
|
|
82
|
+
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
83
|
+
|
|
84
|
+
invalidate();
|
|
85
|
+
|
|
86
|
+
await probeFeishu(DEFAULT_CREDS);
|
|
87
|
+
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readSequentialDefaultProbePair() {
|
|
91
|
+
const first = await probeFeishu(DEFAULT_CREDS);
|
|
92
|
+
return { first, second: await probeFeishu(DEFAULT_CREDS) };
|
|
93
|
+
}
|
|
94
|
+
|
|
21
95
|
describe("probeFeishu", () => {
|
|
22
96
|
beforeEach(() => {
|
|
23
97
|
clearProbeCache();
|
|
@@ -44,28 +118,16 @@ describe("probeFeishu", () => {
|
|
|
44
118
|
});
|
|
45
119
|
|
|
46
120
|
it("returns bot info on successful probe", async () => {
|
|
47
|
-
const requestFn =
|
|
48
|
-
code: 0,
|
|
49
|
-
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
50
|
-
});
|
|
121
|
+
const requestFn = setupSuccessClient();
|
|
51
122
|
|
|
52
|
-
|
|
53
|
-
expect(result).toEqual({
|
|
54
|
-
ok: true,
|
|
55
|
-
appId: "cli_123",
|
|
56
|
-
botName: "TestBot",
|
|
57
|
-
botOpenId: "ou_abc123",
|
|
58
|
-
});
|
|
123
|
+
await expectDefaultSuccessResult();
|
|
59
124
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
60
125
|
});
|
|
61
126
|
|
|
62
127
|
it("passes the probe timeout to the Feishu request", async () => {
|
|
63
|
-
const requestFn =
|
|
64
|
-
code: 0,
|
|
65
|
-
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
66
|
-
});
|
|
128
|
+
const requestFn = setupSuccessClient();
|
|
67
129
|
|
|
68
|
-
await probeFeishu(
|
|
130
|
+
await probeFeishu(DEFAULT_CREDS);
|
|
69
131
|
|
|
70
132
|
expect(requestFn).toHaveBeenCalledWith(
|
|
71
133
|
expect.objectContaining({
|
|
@@ -77,19 +139,16 @@ describe("probeFeishu", () => {
|
|
|
77
139
|
});
|
|
78
140
|
|
|
79
141
|
it("returns timeout error when request exceeds timeout", async () => {
|
|
80
|
-
|
|
81
|
-
try {
|
|
142
|
+
await withFakeTimers(async () => {
|
|
82
143
|
const requestFn = vi.fn().mockImplementation(() => new Promise(() => {}));
|
|
83
144
|
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
|
84
145
|
|
|
85
|
-
const promise = probeFeishu(
|
|
146
|
+
const promise = probeFeishu(DEFAULT_CREDS, { timeoutMs: 1_000 });
|
|
86
147
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
87
148
|
const result = await promise;
|
|
88
149
|
|
|
89
150
|
expect(result).toMatchObject({ ok: false, error: "probe timed out after 1000ms" });
|
|
90
|
-
}
|
|
91
|
-
vi.useRealTimers();
|
|
92
|
-
}
|
|
151
|
+
});
|
|
93
152
|
});
|
|
94
153
|
|
|
95
154
|
it("returns aborted when abort signal is already aborted", async () => {
|
|
@@ -106,14 +165,9 @@ describe("probeFeishu", () => {
|
|
|
106
165
|
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
|
107
166
|
});
|
|
108
167
|
it("returns cached result on subsequent calls within TTL", async () => {
|
|
109
|
-
const requestFn =
|
|
110
|
-
code: 0,
|
|
111
|
-
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
112
|
-
});
|
|
168
|
+
const requestFn = setupSuccessClient();
|
|
113
169
|
|
|
114
|
-
const
|
|
115
|
-
const first = await probeFeishu(creds);
|
|
116
|
-
const second = await probeFeishu(creds);
|
|
170
|
+
const { first, second } = await readSequentialDefaultProbePair();
|
|
117
171
|
|
|
118
172
|
expect(first).toEqual(second);
|
|
119
173
|
// Only one API call should have been made
|
|
@@ -121,76 +175,37 @@ describe("probeFeishu", () => {
|
|
|
121
175
|
});
|
|
122
176
|
|
|
123
177
|
it("makes a fresh API call after cache expires", async () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const requestFn = setupClient({
|
|
127
|
-
code: 0,
|
|
128
|
-
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const creds = { appId: "cli_123", appSecret: "secret" }; // pragma: allowlist secret
|
|
132
|
-
await probeFeishu(creds);
|
|
133
|
-
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
134
|
-
|
|
135
|
-
// Advance time past the success TTL
|
|
136
|
-
vi.advanceTimersByTime(10 * 60 * 1000 + 1);
|
|
178
|
+
await withFakeTimers(async () => {
|
|
179
|
+
const requestFn = setupSuccessClient();
|
|
137
180
|
|
|
138
|
-
await
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
181
|
+
await expectFreshDefaultProbeAfter(requestFn, () => {
|
|
182
|
+
vi.advanceTimersByTime(10 * 60 * 1000 + 1);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
143
185
|
});
|
|
144
186
|
|
|
145
187
|
it("caches failed probe results (API error) for the error TTL", async () => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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);
|
|
157
|
-
|
|
158
|
-
vi.advanceTimersByTime(60 * 1000 + 1);
|
|
159
|
-
|
|
160
|
-
await probeFeishu(creds);
|
|
161
|
-
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
162
|
-
} finally {
|
|
163
|
-
vi.useRealTimers();
|
|
164
|
-
}
|
|
188
|
+
await withFakeTimers(async () => {
|
|
189
|
+
await expectErrorResultCached({
|
|
190
|
+
requestFn: makeRequestFn({ code: 99, msg: "token expired" }),
|
|
191
|
+
expectedError: "API error: token expired",
|
|
192
|
+
ttlMs: 60 * 1000,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
165
195
|
});
|
|
166
196
|
|
|
167
197
|
it("caches thrown request errors for the error TTL", async () => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
179
|
-
|
|
180
|
-
vi.advanceTimersByTime(60 * 1000 + 1);
|
|
181
|
-
|
|
182
|
-
await probeFeishu(creds);
|
|
183
|
-
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
184
|
-
} finally {
|
|
185
|
-
vi.useRealTimers();
|
|
186
|
-
}
|
|
198
|
+
await withFakeTimers(async () => {
|
|
199
|
+
await expectErrorResultCached({
|
|
200
|
+
requestFn: vi.fn().mockRejectedValue(new Error("network error")),
|
|
201
|
+
expectedError: "network error",
|
|
202
|
+
ttlMs: 60 * 1000,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
187
205
|
});
|
|
188
206
|
|
|
189
207
|
it("caches per account independently", async () => {
|
|
190
|
-
const requestFn = setupClient(
|
|
191
|
-
code: 0,
|
|
192
|
-
bot: { bot_name: "Bot1", open_id: "ou_1" },
|
|
193
|
-
});
|
|
208
|
+
const requestFn = setupClient(BOT1_RESPONSE);
|
|
194
209
|
|
|
195
210
|
await probeFeishu({ appId: "cli_aaa", appSecret: "s1" }); // pragma: allowlist secret
|
|
196
211
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
@@ -205,10 +220,7 @@ describe("probeFeishu", () => {
|
|
|
205
220
|
});
|
|
206
221
|
|
|
207
222
|
it("does not share cache between accounts with same appId but different appSecret", async () => {
|
|
208
|
-
const requestFn = setupClient(
|
|
209
|
-
code: 0,
|
|
210
|
-
bot: { bot_name: "Bot1", open_id: "ou_1" },
|
|
211
|
-
});
|
|
223
|
+
const requestFn = setupClient(BOT1_RESPONSE);
|
|
212
224
|
|
|
213
225
|
// First account with appId + secret A
|
|
214
226
|
await probeFeishu({ appId: "cli_shared", appSecret: "secret_aaa" }); // pragma: allowlist secret
|
|
@@ -221,10 +233,7 @@ describe("probeFeishu", () => {
|
|
|
221
233
|
});
|
|
222
234
|
|
|
223
235
|
it("uses accountId for cache key when available", async () => {
|
|
224
|
-
const requestFn = setupClient(
|
|
225
|
-
code: 0,
|
|
226
|
-
bot: { bot_name: "Bot1", open_id: "ou_1" },
|
|
227
|
-
});
|
|
236
|
+
const requestFn = setupClient(BOT1_RESPONSE);
|
|
228
237
|
|
|
229
238
|
// Two accounts with same appId+appSecret but different accountIds are cached separately
|
|
230
239
|
await probeFeishu({ accountId: "acct-1", appId: "cli_123", appSecret: "secret" }); // pragma: allowlist secret
|
|
@@ -239,19 +248,11 @@ describe("probeFeishu", () => {
|
|
|
239
248
|
});
|
|
240
249
|
|
|
241
250
|
it("clearProbeCache forces fresh API call", async () => {
|
|
242
|
-
const requestFn =
|
|
243
|
-
code: 0,
|
|
244
|
-
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const creds = { appId: "cli_123", appSecret: "secret" }; // pragma: allowlist secret
|
|
248
|
-
await probeFeishu(creds);
|
|
249
|
-
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
251
|
+
const requestFn = setupSuccessClient();
|
|
250
252
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
253
|
+
await expectFreshDefaultProbeAfter(requestFn, () => {
|
|
254
|
+
clearProbeCache();
|
|
255
|
+
});
|
|
255
256
|
});
|
|
256
257
|
|
|
257
258
|
it("handles response.data.bot fallback path", async () => {
|
|
@@ -260,10 +261,8 @@ describe("probeFeishu", () => {
|
|
|
260
261
|
data: { bot: { bot_name: "DataBot", open_id: "ou_data" } },
|
|
261
262
|
});
|
|
262
263
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
ok: true,
|
|
266
|
-
appId: "cli_123",
|
|
264
|
+
await expectDefaultSuccessResult(DEFAULT_CREDS, {
|
|
265
|
+
...DEFAULT_SUCCESS_RESULT,
|
|
267
266
|
botName: "DataBot",
|
|
268
267
|
botOpenId: "ou_data",
|
|
269
268
|
});
|
package/src/reactions.ts
CHANGED
|
@@ -9,6 +9,20 @@ export type FeishuReaction = {
|
|
|
9
9
|
operatorId: string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
function resolveConfiguredFeishuClient(params: { cfg: ClawdbotConfig; accountId?: string }) {
|
|
13
|
+
const account = resolveFeishuAccount(params);
|
|
14
|
+
if (!account.configured) {
|
|
15
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
16
|
+
}
|
|
17
|
+
return createFeishuClient(account);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertFeishuReactionApiSuccess(response: { code?: number; msg?: string }, action: string) {
|
|
21
|
+
if (response.code !== 0) {
|
|
22
|
+
throw new Error(`Feishu ${action} failed: ${response.msg || `code ${response.code}`}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
/**
|
|
13
27
|
* Add a reaction (emoji) to a message.
|
|
14
28
|
* @param emojiType - Feishu emoji type, e.g., "SMILE", "THUMBSUP", "HEART"
|
|
@@ -21,12 +35,7 @@ export async function addReactionFeishu(params: {
|
|
|
21
35
|
accountId?: string;
|
|
22
36
|
}): Promise<{ reactionId: string }> {
|
|
23
37
|
const { cfg, messageId, emojiType, accountId } = params;
|
|
24
|
-
const
|
|
25
|
-
if (!account.configured) {
|
|
26
|
-
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const client = createFeishuClient(account);
|
|
38
|
+
const client = resolveConfiguredFeishuClient({ cfg, accountId });
|
|
30
39
|
|
|
31
40
|
const response = (await client.im.messageReaction.create({
|
|
32
41
|
path: { message_id: messageId },
|
|
@@ -41,9 +50,7 @@ export async function addReactionFeishu(params: {
|
|
|
41
50
|
data?: { reaction_id?: string };
|
|
42
51
|
};
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
throw new Error(`Feishu add reaction failed: ${response.msg || `code ${response.code}`}`);
|
|
46
|
-
}
|
|
53
|
+
assertFeishuReactionApiSuccess(response, "add reaction");
|
|
47
54
|
|
|
48
55
|
const reactionId = response.data?.reaction_id;
|
|
49
56
|
if (!reactionId) {
|
|
@@ -63,12 +70,7 @@ export async function removeReactionFeishu(params: {
|
|
|
63
70
|
accountId?: string;
|
|
64
71
|
}): Promise<void> {
|
|
65
72
|
const { cfg, messageId, reactionId, accountId } = params;
|
|
66
|
-
const
|
|
67
|
-
if (!account.configured) {
|
|
68
|
-
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const client = createFeishuClient(account);
|
|
73
|
+
const client = resolveConfiguredFeishuClient({ cfg, accountId });
|
|
72
74
|
|
|
73
75
|
const response = (await client.im.messageReaction.delete({
|
|
74
76
|
path: {
|
|
@@ -77,9 +79,7 @@ export async function removeReactionFeishu(params: {
|
|
|
77
79
|
},
|
|
78
80
|
})) as { code?: number; msg?: string };
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
throw new Error(`Feishu remove reaction failed: ${response.msg || `code ${response.code}`}`);
|
|
82
|
-
}
|
|
82
|
+
assertFeishuReactionApiSuccess(response, "remove reaction");
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -92,12 +92,7 @@ export async function listReactionsFeishu(params: {
|
|
|
92
92
|
accountId?: string;
|
|
93
93
|
}): Promise<FeishuReaction[]> {
|
|
94
94
|
const { cfg, messageId, emojiType, accountId } = params;
|
|
95
|
-
const
|
|
96
|
-
if (!account.configured) {
|
|
97
|
-
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const client = createFeishuClient(account);
|
|
95
|
+
const client = resolveConfiguredFeishuClient({ cfg, accountId });
|
|
101
96
|
|
|
102
97
|
const response = (await client.im.messageReaction.list({
|
|
103
98
|
path: { message_id: messageId },
|
|
@@ -115,9 +110,7 @@ export async function listReactionsFeishu(params: {
|
|
|
115
110
|
};
|
|
116
111
|
};
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
throw new Error(`Feishu list reactions failed: ${response.msg || `code ${response.code}`}`);
|
|
120
|
-
}
|
|
113
|
+
assertFeishuReactionApiSuccess(response, "list reactions");
|
|
121
114
|
|
|
122
115
|
const items = response.data?.items ?? [];
|
|
123
116
|
return items.map((item) => ({
|