@openclaw/feishu 2026.3.2 → 2026.3.7
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/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +199 -13
- package/src/accounts.ts +45 -17
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +8 -0
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +516 -9
- package/src/bot.ts +366 -109
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +52 -64
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +207 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +14 -6
- package/src/config-schema.ts +5 -1
- package/src/dedup.ts +1 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +3 -3
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +60 -13
- package/src/media.ts +23 -9
- package/src/monitor.account.ts +19 -8
- package/src/monitor.reaction.test.ts +111 -105
- package/src/monitor.startup.test.ts +11 -10
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.ts +4 -1
- package/src/monitor.test-mocks.ts +42 -9
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +8 -23
- package/src/onboarding.status.test.ts +1 -1
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +86 -71
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +18 -18
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +175 -0
- package/src/reply-dispatcher.ts +69 -21
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +8 -14
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +1 -1
- package/src/send-target.ts +1 -1
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.ts +5 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +2 -3
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
package/src/send.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
3
3
|
import { createFeishuClient } from "./client.js";
|
|
4
4
|
import type { MentionTarget } from "./mention.js";
|
|
@@ -19,6 +19,61 @@ function shouldFallbackFromReplyTarget(response: { code?: number; msg?: string }
|
|
|
19
19
|
return msg.includes("withdrawn") || msg.includes("not found");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Check whether a thrown error indicates a withdrawn/not-found reply target. */
|
|
23
|
+
function isWithdrawnReplyError(err: unknown): boolean {
|
|
24
|
+
if (typeof err !== "object" || err === null) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
// SDK error shape: err.code
|
|
28
|
+
const code = (err as { code?: number }).code;
|
|
29
|
+
if (typeof code === "number" && WITHDRAWN_REPLY_ERROR_CODES.has(code)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
// AxiosError shape: err.response.data.code
|
|
33
|
+
const response = (err as { response?: { data?: { code?: number; msg?: string } } }).response;
|
|
34
|
+
if (
|
|
35
|
+
typeof response?.data?.code === "number" &&
|
|
36
|
+
WITHDRAWN_REPLY_ERROR_CODES.has(response.data.code)
|
|
37
|
+
) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type FeishuCreateMessageClient = {
|
|
44
|
+
im: {
|
|
45
|
+
message: {
|
|
46
|
+
create: (opts: {
|
|
47
|
+
params: { receive_id_type: "chat_id" | "email" | "open_id" | "union_id" | "user_id" };
|
|
48
|
+
data: { receive_id: string; content: string; msg_type: string };
|
|
49
|
+
}) => Promise<{ code?: number; msg?: string; data?: { message_id?: string } }>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Send a direct message as a fallback when a reply target is unavailable. */
|
|
55
|
+
async function sendFallbackDirect(
|
|
56
|
+
client: FeishuCreateMessageClient,
|
|
57
|
+
params: {
|
|
58
|
+
receiveId: string;
|
|
59
|
+
receiveIdType: "chat_id" | "email" | "open_id" | "union_id" | "user_id";
|
|
60
|
+
content: string;
|
|
61
|
+
msgType: string;
|
|
62
|
+
},
|
|
63
|
+
errorPrefix: string,
|
|
64
|
+
): Promise<FeishuSendResult> {
|
|
65
|
+
const response = await client.im.message.create({
|
|
66
|
+
params: { receive_id_type: params.receiveIdType },
|
|
67
|
+
data: {
|
|
68
|
+
receive_id: params.receiveId,
|
|
69
|
+
content: params.content,
|
|
70
|
+
msg_type: params.msgType,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
assertFeishuMessageApiSuccess(response, errorPrefix);
|
|
74
|
+
return toFeishuSendResult(response, params.receiveId);
|
|
75
|
+
}
|
|
76
|
+
|
|
22
77
|
export type FeishuMessageInfo = {
|
|
23
78
|
messageId: string;
|
|
24
79
|
chatId: string;
|
|
@@ -239,41 +294,33 @@ export async function sendMessageFeishu(
|
|
|
239
294
|
|
|
240
295
|
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
|
241
296
|
|
|
297
|
+
const directParams = { receiveId, receiveIdType, content, msgType };
|
|
298
|
+
|
|
242
299
|
if (replyToMessageId) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
msg_type: msgType,
|
|
248
|
-
...(replyInThread ? { reply_in_thread: true } : {}),
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
if (shouldFallbackFromReplyTarget(response)) {
|
|
252
|
-
const fallback = await client.im.message.create({
|
|
253
|
-
params: { receive_id_type: receiveIdType },
|
|
300
|
+
let response: { code?: number; msg?: string; data?: { message_id?: string } };
|
|
301
|
+
try {
|
|
302
|
+
response = await client.im.message.reply({
|
|
303
|
+
path: { message_id: replyToMessageId },
|
|
254
304
|
data: {
|
|
255
|
-
receive_id: receiveId,
|
|
256
305
|
content,
|
|
257
306
|
msg_type: msgType,
|
|
307
|
+
...(replyInThread ? { reply_in_thread: true } : {}),
|
|
258
308
|
},
|
|
259
309
|
});
|
|
260
|
-
|
|
261
|
-
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if (!isWithdrawnReplyError(err)) {
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
return sendFallbackDirect(client, directParams, "Feishu send failed");
|
|
315
|
+
}
|
|
316
|
+
if (shouldFallbackFromReplyTarget(response)) {
|
|
317
|
+
return sendFallbackDirect(client, directParams, "Feishu send failed");
|
|
262
318
|
}
|
|
263
319
|
assertFeishuMessageApiSuccess(response, "Feishu reply failed");
|
|
264
320
|
return toFeishuSendResult(response, receiveId);
|
|
265
321
|
}
|
|
266
322
|
|
|
267
|
-
|
|
268
|
-
params: { receive_id_type: receiveIdType },
|
|
269
|
-
data: {
|
|
270
|
-
receive_id: receiveId,
|
|
271
|
-
content,
|
|
272
|
-
msg_type: msgType,
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
assertFeishuMessageApiSuccess(response, "Feishu send failed");
|
|
276
|
-
return toFeishuSendResult(response, receiveId);
|
|
323
|
+
return sendFallbackDirect(client, directParams, "Feishu send failed");
|
|
277
324
|
}
|
|
278
325
|
|
|
279
326
|
export type SendFeishuCardParams = {
|
|
@@ -291,41 +338,33 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
|
|
|
291
338
|
const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId });
|
|
292
339
|
const content = JSON.stringify(card);
|
|
293
340
|
|
|
341
|
+
const directParams = { receiveId, receiveIdType, content, msgType: "interactive" };
|
|
342
|
+
|
|
294
343
|
if (replyToMessageId) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
msg_type: "interactive",
|
|
300
|
-
...(replyInThread ? { reply_in_thread: true } : {}),
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
if (shouldFallbackFromReplyTarget(response)) {
|
|
304
|
-
const fallback = await client.im.message.create({
|
|
305
|
-
params: { receive_id_type: receiveIdType },
|
|
344
|
+
let response: { code?: number; msg?: string; data?: { message_id?: string } };
|
|
345
|
+
try {
|
|
346
|
+
response = await client.im.message.reply({
|
|
347
|
+
path: { message_id: replyToMessageId },
|
|
306
348
|
data: {
|
|
307
|
-
receive_id: receiveId,
|
|
308
349
|
content,
|
|
309
350
|
msg_type: "interactive",
|
|
351
|
+
...(replyInThread ? { reply_in_thread: true } : {}),
|
|
310
352
|
},
|
|
311
353
|
});
|
|
312
|
-
|
|
313
|
-
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (!isWithdrawnReplyError(err)) {
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
return sendFallbackDirect(client, directParams, "Feishu card send failed");
|
|
359
|
+
}
|
|
360
|
+
if (shouldFallbackFromReplyTarget(response)) {
|
|
361
|
+
return sendFallbackDirect(client, directParams, "Feishu card send failed");
|
|
314
362
|
}
|
|
315
363
|
assertFeishuMessageApiSuccess(response, "Feishu card reply failed");
|
|
316
364
|
return toFeishuSendResult(response, receiveId);
|
|
317
365
|
}
|
|
318
366
|
|
|
319
|
-
|
|
320
|
-
params: { receive_id_type: receiveIdType },
|
|
321
|
-
data: {
|
|
322
|
-
receive_id: receiveId,
|
|
323
|
-
content,
|
|
324
|
-
msg_type: "interactive",
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
assertFeishuMessageApiSuccess(response, "Feishu card send failed");
|
|
328
|
-
return toFeishuSendResult(response, receiveId);
|
|
367
|
+
return sendFallbackDirect(client, directParams, "Feishu card send failed");
|
|
329
368
|
}
|
|
330
369
|
|
|
331
370
|
export async function updateCardFeishu(params: {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mergeStreamingText, resolveStreamingCardSendMode } from "./streaming-card.js";
|
|
3
|
+
|
|
4
|
+
describe("mergeStreamingText", () => {
|
|
5
|
+
it("prefers the latest full text when it already includes prior text", () => {
|
|
6
|
+
expect(mergeStreamingText("hello", "hello world")).toBe("hello world");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("keeps previous text when the next partial is empty or redundant", () => {
|
|
10
|
+
expect(mergeStreamingText("hello", "")).toBe("hello");
|
|
11
|
+
expect(mergeStreamingText("hello world", "hello")).toBe("hello world");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("appends fragmented chunks without injecting newlines", () => {
|
|
15
|
+
expect(mergeStreamingText("hello wor", "ld")).toBe("hello world");
|
|
16
|
+
expect(mergeStreamingText("line1", "line2")).toBe("line1line2");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("merges overlap between adjacent partial snapshots", () => {
|
|
20
|
+
expect(mergeStreamingText("好的,让我", "让我再读取一遍")).toBe("好的,让我再读取一遍");
|
|
21
|
+
expect(mergeStreamingText("revision_id: 552", "2,一点变化都没有")).toBe(
|
|
22
|
+
"revision_id: 552,一点变化都没有",
|
|
23
|
+
);
|
|
24
|
+
expect(mergeStreamingText("abc", "cabc")).toBe("cabc");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("resolveStreamingCardSendMode", () => {
|
|
29
|
+
it("prefers message.reply when reply target and root id both exist", () => {
|
|
30
|
+
expect(
|
|
31
|
+
resolveStreamingCardSendMode({
|
|
32
|
+
replyToMessageId: "om_parent",
|
|
33
|
+
rootId: "om_topic_root",
|
|
34
|
+
}),
|
|
35
|
+
).toBe("reply");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("falls back to root create when reply target is absent", () => {
|
|
39
|
+
expect(
|
|
40
|
+
resolveStreamingCardSendMode({
|
|
41
|
+
rootId: "om_topic_root",
|
|
42
|
+
}),
|
|
43
|
+
).toBe("root_create");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("uses create mode when no reply routing fields are provided", () => {
|
|
47
|
+
expect(resolveStreamingCardSendMode()).toBe("create");
|
|
48
|
+
expect(
|
|
49
|
+
resolveStreamingCardSendMode({
|
|
50
|
+
replyInThread: true,
|
|
51
|
+
}),
|
|
52
|
+
).toBe("create");
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/streaming-card.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Client } from "@larksuiteoapi/node-sdk";
|
|
6
|
-
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
|
|
6
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/feishu";
|
|
7
7
|
import type { FeishuDomain } from "./types.js";
|
|
8
8
|
|
|
9
9
|
type Credentials = { appId: string; appSecret: string; domain?: FeishuDomain };
|
|
@@ -16,6 +16,13 @@ export type StreamingCardHeader = {
|
|
|
16
16
|
template?: string;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
type StreamingStartOptions = {
|
|
20
|
+
replyToMessageId?: string;
|
|
21
|
+
replyInThread?: boolean;
|
|
22
|
+
rootId?: string;
|
|
23
|
+
header?: StreamingCardHeader;
|
|
24
|
+
};
|
|
25
|
+
|
|
19
26
|
// Token cache (keyed by domain + appId)
|
|
20
27
|
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
|
21
28
|
|
|
@@ -60,6 +67,10 @@ async function getToken(creds: Credentials): Promise<string> {
|
|
|
60
67
|
policy: { allowedHostnames: resolveAllowedHostnames(creds.domain) },
|
|
61
68
|
auditContext: "feishu.streaming-card.token",
|
|
62
69
|
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
await release();
|
|
72
|
+
throw new Error(`Token request failed with HTTP ${response.status}`);
|
|
73
|
+
}
|
|
63
74
|
const data = (await response.json()) as {
|
|
64
75
|
code: number;
|
|
65
76
|
msg: string;
|
|
@@ -85,6 +96,52 @@ function truncateSummary(text: string, max = 50): string {
|
|
|
85
96
|
return clean.length <= max ? clean : clean.slice(0, max - 3) + "...";
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
export function mergeStreamingText(
|
|
100
|
+
previousText: string | undefined,
|
|
101
|
+
nextText: string | undefined,
|
|
102
|
+
): string {
|
|
103
|
+
const previous = typeof previousText === "string" ? previousText : "";
|
|
104
|
+
const next = typeof nextText === "string" ? nextText : "";
|
|
105
|
+
if (!next) {
|
|
106
|
+
return previous;
|
|
107
|
+
}
|
|
108
|
+
if (!previous || next === previous) {
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
if (next.startsWith(previous)) {
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
if (previous.startsWith(next)) {
|
|
115
|
+
return previous;
|
|
116
|
+
}
|
|
117
|
+
if (next.includes(previous)) {
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
if (previous.includes(next)) {
|
|
121
|
+
return previous;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Merge partial overlaps, e.g. "这" + "这是" => "这是".
|
|
125
|
+
const maxOverlap = Math.min(previous.length, next.length);
|
|
126
|
+
for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
|
|
127
|
+
if (previous.slice(-overlap) === next.slice(0, overlap)) {
|
|
128
|
+
return `${previous}${next.slice(overlap)}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Fallback for fragmented partial chunks: append as-is to avoid losing tokens.
|
|
132
|
+
return `${previous}${next}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveStreamingCardSendMode(options?: StreamingStartOptions) {
|
|
136
|
+
if (options?.replyToMessageId) {
|
|
137
|
+
return "reply";
|
|
138
|
+
}
|
|
139
|
+
if (options?.rootId) {
|
|
140
|
+
return "root_create";
|
|
141
|
+
}
|
|
142
|
+
return "create";
|
|
143
|
+
}
|
|
144
|
+
|
|
88
145
|
/** Streaming card session manager */
|
|
89
146
|
export class FeishuStreamingSession {
|
|
90
147
|
private client: Client;
|
|
@@ -106,12 +163,7 @@ export class FeishuStreamingSession {
|
|
|
106
163
|
async start(
|
|
107
164
|
receiveId: string,
|
|
108
165
|
receiveIdType: "open_id" | "user_id" | "union_id" | "email" | "chat_id" = "chat_id",
|
|
109
|
-
options?:
|
|
110
|
-
replyToMessageId?: string;
|
|
111
|
-
replyInThread?: boolean;
|
|
112
|
-
rootId?: string;
|
|
113
|
-
header?: StreamingCardHeader;
|
|
114
|
-
},
|
|
166
|
+
options?: StreamingStartOptions,
|
|
115
167
|
): Promise<void> {
|
|
116
168
|
if (this.state) {
|
|
117
169
|
return;
|
|
@@ -123,7 +175,7 @@ export class FeishuStreamingSession {
|
|
|
123
175
|
config: {
|
|
124
176
|
streaming_mode: true,
|
|
125
177
|
summary: { content: "[Generating...]" },
|
|
126
|
-
streaming_config: { print_frequency_ms: { default: 50 }, print_step: { default:
|
|
178
|
+
streaming_config: { print_frequency_ms: { default: 50 }, print_step: { default: 1 } },
|
|
127
179
|
},
|
|
128
180
|
body: {
|
|
129
181
|
elements: [{ tag: "markdown", content: "⏳ Thinking...", element_id: "content" }],
|
|
@@ -150,6 +202,10 @@ export class FeishuStreamingSession {
|
|
|
150
202
|
policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
|
|
151
203
|
auditContext: "feishu.streaming-card.create",
|
|
152
204
|
});
|
|
205
|
+
if (!createRes.ok) {
|
|
206
|
+
await releaseCreate();
|
|
207
|
+
throw new Error(`Create card request failed with HTTP ${createRes.status}`);
|
|
208
|
+
}
|
|
153
209
|
const createData = (await createRes.json()) as {
|
|
154
210
|
code: number;
|
|
155
211
|
msg: string;
|
|
@@ -162,28 +218,31 @@ export class FeishuStreamingSession {
|
|
|
162
218
|
const cardId = createData.data.card_id;
|
|
163
219
|
const cardContent = JSON.stringify({ type: "card", data: { card_id: cardId } });
|
|
164
220
|
|
|
165
|
-
//
|
|
221
|
+
// Prefer message.reply when we have a reply target — reply_in_thread
|
|
222
|
+
// reliably routes streaming cards into Feishu topics, whereas
|
|
223
|
+
// message.create with root_id may silently ignore root_id for card
|
|
224
|
+
// references (card_id format).
|
|
166
225
|
let sendRes;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
msg_type: "interactive",
|
|
171
|
-
content: cardContent,
|
|
172
|
-
root_id: options.rootId,
|
|
173
|
-
};
|
|
174
|
-
sendRes = await this.client.im.message.create({
|
|
175
|
-
params: { receive_id_type: receiveIdType },
|
|
176
|
-
data: createData,
|
|
177
|
-
});
|
|
178
|
-
} else if (options?.replyToMessageId) {
|
|
226
|
+
const sendOptions = options ?? {};
|
|
227
|
+
const sendMode = resolveStreamingCardSendMode(sendOptions);
|
|
228
|
+
if (sendMode === "reply") {
|
|
179
229
|
sendRes = await this.client.im.message.reply({
|
|
180
|
-
path: { message_id:
|
|
230
|
+
path: { message_id: sendOptions.replyToMessageId! },
|
|
181
231
|
data: {
|
|
182
232
|
msg_type: "interactive",
|
|
183
233
|
content: cardContent,
|
|
184
|
-
...(
|
|
234
|
+
...(sendOptions.replyInThread ? { reply_in_thread: true } : {}),
|
|
185
235
|
},
|
|
186
236
|
});
|
|
237
|
+
} else if (sendMode === "root_create") {
|
|
238
|
+
// root_id is undeclared in the SDK types but accepted at runtime
|
|
239
|
+
sendRes = await this.client.im.message.create({
|
|
240
|
+
params: { receive_id_type: receiveIdType },
|
|
241
|
+
data: Object.assign(
|
|
242
|
+
{ receive_id: receiveId, msg_type: "interactive", content: cardContent },
|
|
243
|
+
{ root_id: sendOptions.rootId },
|
|
244
|
+
),
|
|
245
|
+
});
|
|
187
246
|
} else {
|
|
188
247
|
sendRes = await this.client.im.message.create({
|
|
189
248
|
params: { receive_id_type: receiveIdType },
|
|
@@ -235,10 +294,15 @@ export class FeishuStreamingSession {
|
|
|
235
294
|
if (!this.state || this.closed) {
|
|
236
295
|
return;
|
|
237
296
|
}
|
|
297
|
+
const mergedInput = mergeStreamingText(this.pendingText ?? this.state.currentText, text);
|
|
298
|
+
if (!mergedInput || mergedInput === this.state.currentText) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
238
302
|
// Throttle: skip if updated recently, but remember pending text
|
|
239
303
|
const now = Date.now();
|
|
240
304
|
if (now - this.lastUpdateTime < this.updateThrottleMs) {
|
|
241
|
-
this.pendingText =
|
|
305
|
+
this.pendingText = mergedInput;
|
|
242
306
|
return;
|
|
243
307
|
}
|
|
244
308
|
this.pendingText = null;
|
|
@@ -248,8 +312,12 @@ export class FeishuStreamingSession {
|
|
|
248
312
|
if (!this.state || this.closed) {
|
|
249
313
|
return;
|
|
250
314
|
}
|
|
251
|
-
this.state.currentText
|
|
252
|
-
|
|
315
|
+
const mergedText = mergeStreamingText(this.state.currentText, mergedInput);
|
|
316
|
+
if (!mergedText || mergedText === this.state.currentText) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
this.state.currentText = mergedText;
|
|
320
|
+
await this.updateCardContent(mergedText, (e) => this.log?.(`Update failed: ${String(e)}`));
|
|
253
321
|
});
|
|
254
322
|
await this.queue;
|
|
255
323
|
}
|
|
@@ -261,8 +329,8 @@ export class FeishuStreamingSession {
|
|
|
261
329
|
this.closed = true;
|
|
262
330
|
await this.queue;
|
|
263
331
|
|
|
264
|
-
|
|
265
|
-
const text = finalText
|
|
332
|
+
const pendingMerged = mergeStreamingText(this.state.currentText, this.pendingText ?? undefined);
|
|
333
|
+
const text = finalText ? mergeStreamingText(pendingMerged, finalText) : pendingMerged;
|
|
266
334
|
const apiBase = resolveApiBase(this.creds.domain);
|
|
267
335
|
|
|
268
336
|
// Only send final update if content differs from what's already displayed
|
package/src/targets.ts
CHANGED
|
@@ -66,7 +66,11 @@ export function formatFeishuTarget(id: string, type?: FeishuIdType): string {
|
|
|
66
66
|
export function resolveReceiveIdType(id: string): "chat_id" | "open_id" | "user_id" {
|
|
67
67
|
const trimmed = id.trim();
|
|
68
68
|
const lowered = trimmed.toLowerCase();
|
|
69
|
-
if (
|
|
69
|
+
if (
|
|
70
|
+
lowered.startsWith("chat:") ||
|
|
71
|
+
lowered.startsWith("group:") ||
|
|
72
|
+
lowered.startsWith("channel:")
|
|
73
|
+
) {
|
|
70
74
|
return "chat_id";
|
|
71
75
|
}
|
|
72
76
|
if (lowered.startsWith("open_id:")) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { registerFeishuBitableTools } from "./bitable.js";
|
|
4
4
|
import { registerFeishuDriveTools } from "./drive.js";
|
|
@@ -35,12 +35,12 @@ function createConfig(params: {
|
|
|
35
35
|
accounts: {
|
|
36
36
|
a: {
|
|
37
37
|
appId: "app-a",
|
|
38
|
-
appSecret: "sec-a",
|
|
38
|
+
appSecret: "sec-a", // pragma: allowlist secret
|
|
39
39
|
tools: params.toolsA,
|
|
40
40
|
},
|
|
41
41
|
b: {
|
|
42
42
|
appId: "app-b",
|
|
43
|
-
appSecret: "sec-b",
|
|
43
|
+
appSecret: "sec-b", // pragma: allowlist secret
|
|
44
44
|
tools: params.toolsB,
|
|
45
45
|
},
|
|
46
46
|
},
|
package/src/tool-account.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
3
3
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
4
4
|
import { createFeishuClient } from "./client.js";
|
|
5
5
|
import { resolveToolsConfig } from "./tools-config.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
jsonToolResult,
|
|
4
|
+
toolExecutionErrorResult,
|
|
5
|
+
unknownToolActionResult,
|
|
6
|
+
} from "./tool-result.js";
|
|
7
|
+
|
|
8
|
+
describe("jsonToolResult", () => {
|
|
9
|
+
it("formats tool result with text content and details", () => {
|
|
10
|
+
const payload = { ok: true, id: "abc" };
|
|
11
|
+
expect(jsonToolResult(payload)).toEqual({
|
|
12
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
13
|
+
details: payload,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("formats unknown action errors", () => {
|
|
18
|
+
expect(unknownToolActionResult("create")).toEqual({
|
|
19
|
+
content: [
|
|
20
|
+
{ type: "text", text: JSON.stringify({ error: "Unknown action: create" }, null, 2) },
|
|
21
|
+
],
|
|
22
|
+
details: { error: "Unknown action: create" },
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("formats execution errors", () => {
|
|
27
|
+
expect(toolExecutionErrorResult(new Error("boom"))).toEqual({
|
|
28
|
+
content: [{ type: "text", text: JSON.stringify({ error: "boom" }, null, 2) }],
|
|
29
|
+
details: { error: "boom" },
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function jsonToolResult(data: unknown) {
|
|
2
|
+
return {
|
|
3
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
4
|
+
details: data,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function unknownToolActionResult(action: unknown) {
|
|
9
|
+
return jsonToolResult({ error: `Unknown action: ${String(action)}` });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function toolExecutionErrorResult(error: unknown) {
|
|
13
|
+
return jsonToolResult({ error: error instanceof Error ? error.message : String(error) });
|
|
14
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaseProbeResult } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { BaseProbeResult } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import type {
|
|
3
3
|
FeishuConfigSchema,
|
|
4
4
|
FeishuGroupSchema,
|
|
@@ -45,6 +45,7 @@ export type FeishuMessageContext = {
|
|
|
45
45
|
senderName?: string;
|
|
46
46
|
chatType: "p2p" | "group" | "private";
|
|
47
47
|
mentionedBot: boolean;
|
|
48
|
+
hasAnyMention?: boolean;
|
|
48
49
|
rootId?: string;
|
|
49
50
|
parentId?: string;
|
|
50
51
|
threadId?: string;
|
|
@@ -52,8 +53,6 @@ export type FeishuMessageContext = {
|
|
|
52
53
|
contentType: string;
|
|
53
54
|
/** Mention forward targets (excluding the bot itself) */
|
|
54
55
|
mentionTargets?: MentionTarget[];
|
|
55
|
-
/** Extracted message body (after removing @ placeholders) */
|
|
56
|
-
mentionMessageBody?: string;
|
|
57
56
|
};
|
|
58
57
|
|
|
59
58
|
export type FeishuSendResult = {
|
package/src/typing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
3
3
|
import { createFeishuClient } from "./client.js";
|
|
4
4
|
import { getFeishuRuntime } from "./runtime.js";
|
package/src/wiki.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
3
3
|
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
4
|
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
5
|
+
import {
|
|
6
|
+
jsonToolResult,
|
|
7
|
+
toolExecutionErrorResult,
|
|
8
|
+
unknownToolActionResult,
|
|
9
|
+
} from "./tool-result.js";
|
|
5
10
|
import { FeishuWikiSchema, type FeishuWikiParams } from "./wiki-schema.js";
|
|
6
11
|
|
|
7
|
-
// ============ Helpers ============
|
|
8
|
-
|
|
9
|
-
function json(data: unknown) {
|
|
10
|
-
return {
|
|
11
|
-
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
12
|
-
details: data,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
12
|
type ObjType = "doc" | "sheet" | "mindnote" | "bitable" | "file" | "docx" | "slides";
|
|
17
13
|
|
|
18
14
|
// ============ Actions ============
|
|
@@ -194,22 +190,22 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
|
|
|
194
190
|
});
|
|
195
191
|
switch (p.action) {
|
|
196
192
|
case "spaces":
|
|
197
|
-
return
|
|
193
|
+
return jsonToolResult(await listSpaces(client));
|
|
198
194
|
case "nodes":
|
|
199
|
-
return
|
|
195
|
+
return jsonToolResult(await listNodes(client, p.space_id, p.parent_node_token));
|
|
200
196
|
case "get":
|
|
201
|
-
return
|
|
197
|
+
return jsonToolResult(await getNode(client, p.token));
|
|
202
198
|
case "search":
|
|
203
|
-
return
|
|
199
|
+
return jsonToolResult({
|
|
204
200
|
error:
|
|
205
201
|
"Search is not available. Use feishu_wiki with action: 'nodes' to browse or action: 'get' to lookup by token.",
|
|
206
202
|
});
|
|
207
203
|
case "create":
|
|
208
|
-
return
|
|
204
|
+
return jsonToolResult(
|
|
209
205
|
await createNode(client, p.space_id, p.title, p.obj_type, p.parent_node_token),
|
|
210
206
|
);
|
|
211
207
|
case "move":
|
|
212
|
-
return
|
|
208
|
+
return jsonToolResult(
|
|
213
209
|
await moveNode(
|
|
214
210
|
client,
|
|
215
211
|
p.space_id,
|
|
@@ -219,13 +215,13 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
|
|
|
219
215
|
),
|
|
220
216
|
);
|
|
221
217
|
case "rename":
|
|
222
|
-
return
|
|
218
|
+
return jsonToolResult(await renameNode(client, p.space_id, p.node_token, p.title));
|
|
223
219
|
default:
|
|
224
220
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
|
|
225
|
-
return
|
|
221
|
+
return unknownToolActionResult((p as { action?: unknown }).action);
|
|
226
222
|
}
|
|
227
223
|
} catch (err) {
|
|
228
|
-
return
|
|
224
|
+
return toolExecutionErrorResult(err);
|
|
229
225
|
}
|
|
230
226
|
},
|
|
231
227
|
};
|