@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/send.ts
CHANGED
|
@@ -1,21 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
2
|
+
import {
|
|
3
|
+
convertMarkdownTables,
|
|
4
|
+
normalizeLowercaseStringOrEmpty,
|
|
5
|
+
normalizeOptionalLowercaseString,
|
|
6
|
+
} from "openclaw/plugin-sdk/text-runtime";
|
|
7
|
+
import type { ClawdbotConfig } from "../runtime-api.js";
|
|
8
|
+
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
3
9
|
import { createFeishuClient } from "./client.js";
|
|
4
|
-
import type { MentionTarget } from "./mention.js";
|
|
5
|
-
import {
|
|
10
|
+
import type { MentionTarget } from "./mention-target.types.js";
|
|
11
|
+
import { buildMentionedCardContent, buildMentionedMessage } from "./mention.js";
|
|
6
12
|
import { parsePostContent } from "./post.js";
|
|
7
|
-
import { getFeishuRuntime } from "./runtime.js";
|
|
8
13
|
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
|
|
9
14
|
import { resolveFeishuSendTarget } from "./send-target.js";
|
|
10
15
|
import type { FeishuChatType, FeishuMessageInfo, FeishuSendResult } from "./types.js";
|
|
11
16
|
|
|
12
17
|
const WITHDRAWN_REPLY_ERROR_CODES = new Set([230011, 231003]);
|
|
18
|
+
const INTERACTIVE_CARD_FALLBACK_TEXT = "[Interactive Card]";
|
|
19
|
+
const POST_FALLBACK_TEXT = "[Rich text message]";
|
|
20
|
+
const FEISHU_CARD_TEMPLATES = new Set([
|
|
21
|
+
"blue",
|
|
22
|
+
"green",
|
|
23
|
+
"red",
|
|
24
|
+
"orange",
|
|
25
|
+
"purple",
|
|
26
|
+
"indigo",
|
|
27
|
+
"wathet",
|
|
28
|
+
"turquoise",
|
|
29
|
+
"yellow",
|
|
30
|
+
"grey",
|
|
31
|
+
"carmine",
|
|
32
|
+
"violet",
|
|
33
|
+
"lime",
|
|
34
|
+
]);
|
|
13
35
|
|
|
14
36
|
function shouldFallbackFromReplyTarget(response: { code?: number; msg?: string }): boolean {
|
|
15
37
|
if (response.code !== undefined && WITHDRAWN_REPLY_ERROR_CODES.has(response.code)) {
|
|
16
38
|
return true;
|
|
17
39
|
}
|
|
18
|
-
const msg = response.msg
|
|
40
|
+
const msg = normalizeLowercaseStringOrEmpty(response.msg);
|
|
19
41
|
return msg.includes("withdrawn") || msg.includes("not found");
|
|
20
42
|
}
|
|
21
43
|
|
|
@@ -40,6 +62,10 @@ function isWithdrawnReplyError(err: unknown): boolean {
|
|
|
40
62
|
return false;
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
type FeishuCreateMessageClient = {
|
|
44
70
|
im: {
|
|
45
71
|
message: {
|
|
@@ -65,6 +91,7 @@ type FeishuMessageGetItem = {
|
|
|
65
91
|
message_id?: string;
|
|
66
92
|
chat_id?: string;
|
|
67
93
|
chat_type?: FeishuChatType;
|
|
94
|
+
thread_id?: string;
|
|
68
95
|
msg_type?: string;
|
|
69
96
|
body?: { content?: string };
|
|
70
97
|
sender?: FeishuMessageSender;
|
|
@@ -123,6 +150,12 @@ async function sendReplyOrFallbackDirect(
|
|
|
123
150
|
return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
|
|
124
151
|
}
|
|
125
152
|
|
|
153
|
+
const threadReplyFallbackError = params.replyInThread
|
|
154
|
+
? new Error(
|
|
155
|
+
"Feishu thread reply failed: reply target is unavailable and cannot safely fall back to a top-level send.",
|
|
156
|
+
)
|
|
157
|
+
: null;
|
|
158
|
+
|
|
126
159
|
let response: { code?: number; msg?: string; data?: { message_id?: string } };
|
|
127
160
|
try {
|
|
128
161
|
response = await client.im.message.reply({
|
|
@@ -137,47 +170,139 @@ async function sendReplyOrFallbackDirect(
|
|
|
137
170
|
if (!isWithdrawnReplyError(err)) {
|
|
138
171
|
throw err;
|
|
139
172
|
}
|
|
173
|
+
if (threadReplyFallbackError) {
|
|
174
|
+
throw threadReplyFallbackError;
|
|
175
|
+
}
|
|
140
176
|
return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
|
|
141
177
|
}
|
|
142
178
|
if (shouldFallbackFromReplyTarget(response)) {
|
|
179
|
+
if (threadReplyFallbackError) {
|
|
180
|
+
throw threadReplyFallbackError;
|
|
181
|
+
}
|
|
143
182
|
return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
|
|
144
183
|
}
|
|
145
184
|
assertFeishuMessageApiSuccess(response, params.replyErrorPrefix);
|
|
146
185
|
return toFeishuSendResult(response, params.directParams.receiveId);
|
|
147
186
|
}
|
|
148
187
|
|
|
149
|
-
function
|
|
150
|
-
if (
|
|
151
|
-
return
|
|
188
|
+
function normalizeCardTemplateVariable(value: unknown): string | undefined {
|
|
189
|
+
if (typeof value === "string") {
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
193
|
+
return String(value);
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readCardTemplateVariables(parsed: Record<string, unknown>): Map<string, string> {
|
|
199
|
+
const variables = new Map<string, string>();
|
|
200
|
+
for (const source of [parsed.template_variable, parsed.template_variables]) {
|
|
201
|
+
if (!isRecord(source)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
for (const [key, value] of Object.entries(source)) {
|
|
205
|
+
const normalized = normalizeCardTemplateVariable(value);
|
|
206
|
+
if (normalized !== undefined) {
|
|
207
|
+
variables.set(key, normalized);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
152
210
|
}
|
|
211
|
+
return variables;
|
|
212
|
+
}
|
|
153
213
|
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
return
|
|
214
|
+
function applyCardTemplateVariables(text: string, variables: Map<string, string>): string {
|
|
215
|
+
if (variables.size === 0) {
|
|
216
|
+
return text;
|
|
217
|
+
}
|
|
218
|
+
return text.replace(/\$\{([A-Za-z0-9_.-]+)\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (match, a, b) => {
|
|
219
|
+
const variableName = typeof a === "string" ? a : b;
|
|
220
|
+
return variables.get(variableName) ?? match;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function extractInteractiveElementText(
|
|
225
|
+
element: unknown,
|
|
226
|
+
variables: Map<string, string>,
|
|
227
|
+
): string | undefined {
|
|
228
|
+
if (!isRecord(element)) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const tag = typeof element.tag === "string" ? element.tag : "";
|
|
232
|
+
const text = isRecord(element.text) ? element.text : undefined;
|
|
233
|
+
|
|
234
|
+
if (tag === "div" && typeof text?.content === "string") {
|
|
235
|
+
return applyCardTemplateVariables(text.content, variables);
|
|
236
|
+
}
|
|
237
|
+
if ((tag === "markdown" || tag === "lark_md") && typeof element.content === "string") {
|
|
238
|
+
return applyCardTemplateVariables(element.content, variables);
|
|
239
|
+
}
|
|
240
|
+
if (tag === "plain_text" && typeof element.content === "string") {
|
|
241
|
+
return applyCardTemplateVariables(element.content, variables);
|
|
157
242
|
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
158
245
|
|
|
246
|
+
function extractInteractiveElementsText(
|
|
247
|
+
elements: unknown[],
|
|
248
|
+
variables: Map<string, string>,
|
|
249
|
+
): string {
|
|
159
250
|
const texts: string[] = [];
|
|
160
|
-
for (const element of
|
|
161
|
-
|
|
162
|
-
|
|
251
|
+
for (const element of elements) {
|
|
252
|
+
const text = extractInteractiveElementText(element, variables);
|
|
253
|
+
if (text !== undefined) {
|
|
254
|
+
texts.push(text);
|
|
163
255
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
256
|
+
}
|
|
257
|
+
return texts.join("\n").trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readInteractiveElementArrays(parsed: Record<string, unknown>): unknown[][] {
|
|
261
|
+
const body = isRecord(parsed.body) ? parsed.body : undefined;
|
|
262
|
+
const elementArrays: unknown[][] = [];
|
|
263
|
+
|
|
264
|
+
for (const candidate of [parsed.elements, body?.elements]) {
|
|
265
|
+
if (Array.isArray(candidate)) {
|
|
266
|
+
elementArrays.push(candidate);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const candidate of [parsed.i18n_elements, body?.i18n_elements]) {
|
|
271
|
+
if (!isRecord(candidate)) {
|
|
171
272
|
continue;
|
|
172
273
|
}
|
|
173
|
-
|
|
174
|
-
|
|
274
|
+
for (const localeElements of Object.values(candidate)) {
|
|
275
|
+
if (Array.isArray(localeElements)) {
|
|
276
|
+
elementArrays.push(localeElements);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return elementArrays;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseInteractivePostFallback(parsed: unknown): string | undefined {
|
|
285
|
+
const textContent = parsePostContent(JSON.stringify(parsed)).textContent.trim();
|
|
286
|
+
return textContent && textContent !== POST_FALLBACK_TEXT ? textContent : undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseInteractiveCardContent(parsed: unknown): string {
|
|
290
|
+
if (!isRecord(parsed)) {
|
|
291
|
+
return INTERACTIVE_CARD_FALLBACK_TEXT;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const variables = readCardTemplateVariables(parsed);
|
|
295
|
+
for (const elements of readInteractiveElementArrays(parsed)) {
|
|
296
|
+
const text = extractInteractiveElementsText(elements, variables);
|
|
297
|
+
if (text) {
|
|
298
|
+
return text;
|
|
175
299
|
}
|
|
176
300
|
}
|
|
177
|
-
|
|
301
|
+
|
|
302
|
+
return parseInteractivePostFallback(parsed) ?? INTERACTIVE_CARD_FALLBACK_TEXT;
|
|
178
303
|
}
|
|
179
304
|
|
|
180
|
-
function
|
|
305
|
+
function parseFeishuMessageContent(rawContent: string, msgType: string): string {
|
|
181
306
|
if (!rawContent) {
|
|
182
307
|
return "";
|
|
183
308
|
}
|
|
@@ -218,6 +343,33 @@ function parseQuotedMessageContent(rawContent: string, msgType: string): string
|
|
|
218
343
|
return `[${msgType || "unknown"} message]`;
|
|
219
344
|
}
|
|
220
345
|
|
|
346
|
+
function parseFeishuMessageItem(
|
|
347
|
+
item: FeishuMessageGetItem,
|
|
348
|
+
fallbackMessageId?: string,
|
|
349
|
+
): FeishuMessageInfo {
|
|
350
|
+
const msgType = item.msg_type ?? "text";
|
|
351
|
+
const rawContent = item.body?.content ?? "";
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
messageId: item.message_id ?? fallbackMessageId ?? "",
|
|
355
|
+
chatId: item.chat_id ?? "",
|
|
356
|
+
chatType:
|
|
357
|
+
item.chat_type === "group" ||
|
|
358
|
+
item.chat_type === "topic_group" ||
|
|
359
|
+
item.chat_type === "private" ||
|
|
360
|
+
item.chat_type === "p2p"
|
|
361
|
+
? item.chat_type
|
|
362
|
+
: undefined,
|
|
363
|
+
senderId: item.sender?.id,
|
|
364
|
+
senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
|
|
365
|
+
senderType: item.sender?.sender_type,
|
|
366
|
+
content: parseFeishuMessageContent(rawContent, msgType),
|
|
367
|
+
contentType: msgType,
|
|
368
|
+
createTime: item.create_time ? Number.parseInt(item.create_time, 10) : undefined,
|
|
369
|
+
threadId: item.thread_id || undefined,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
221
373
|
/**
|
|
222
374
|
* Get a message by its ID.
|
|
223
375
|
* Useful for fetching quoted/replied message content.
|
|
@@ -228,7 +380,7 @@ export async function getMessageFeishu(params: {
|
|
|
228
380
|
accountId?: string;
|
|
229
381
|
}): Promise<FeishuMessageInfo | null> {
|
|
230
382
|
const { cfg, messageId, accountId } = params;
|
|
231
|
-
const account =
|
|
383
|
+
const account = resolveFeishuRuntimeAccount({ cfg, accountId });
|
|
232
384
|
if (!account.configured) {
|
|
233
385
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
234
386
|
}
|
|
@@ -255,29 +407,104 @@ export async function getMessageFeishu(params: {
|
|
|
255
407
|
return null;
|
|
256
408
|
}
|
|
257
409
|
|
|
258
|
-
|
|
259
|
-
const rawContent = item.body?.content ?? "";
|
|
260
|
-
const content = parseQuotedMessageContent(rawContent, msgType);
|
|
261
|
-
|
|
262
|
-
return {
|
|
263
|
-
messageId: item.message_id ?? messageId,
|
|
264
|
-
chatId: item.chat_id ?? "",
|
|
265
|
-
chatType:
|
|
266
|
-
item.chat_type === "group" || item.chat_type === "private" || item.chat_type === "p2p"
|
|
267
|
-
? item.chat_type
|
|
268
|
-
: undefined,
|
|
269
|
-
senderId: item.sender?.id,
|
|
270
|
-
senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
|
|
271
|
-
senderType: item.sender?.sender_type,
|
|
272
|
-
content,
|
|
273
|
-
contentType: msgType,
|
|
274
|
-
createTime: item.create_time ? parseInt(String(item.create_time), 10) : undefined,
|
|
275
|
-
};
|
|
410
|
+
return parseFeishuMessageItem(item, messageId);
|
|
276
411
|
} catch {
|
|
277
412
|
return null;
|
|
278
413
|
}
|
|
279
414
|
}
|
|
280
415
|
|
|
416
|
+
export type FeishuThreadMessageInfo = {
|
|
417
|
+
messageId: string;
|
|
418
|
+
senderId?: string;
|
|
419
|
+
senderType?: string;
|
|
420
|
+
content: string;
|
|
421
|
+
contentType: string;
|
|
422
|
+
createTime?: number;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* List messages in a Feishu thread (topic).
|
|
427
|
+
* Uses container_id_type=thread to directly query thread messages,
|
|
428
|
+
* which includes both the root message and all replies (including bot replies).
|
|
429
|
+
*/
|
|
430
|
+
export async function listFeishuThreadMessages(params: {
|
|
431
|
+
cfg: ClawdbotConfig;
|
|
432
|
+
threadId: string;
|
|
433
|
+
currentMessageId?: string;
|
|
434
|
+
/** Exclude the root message (already provided separately as ThreadStarterBody). */
|
|
435
|
+
rootMessageId?: string;
|
|
436
|
+
limit?: number;
|
|
437
|
+
accountId?: string;
|
|
438
|
+
}): Promise<FeishuThreadMessageInfo[]> {
|
|
439
|
+
const { cfg, threadId, currentMessageId, rootMessageId, limit = 20, accountId } = params;
|
|
440
|
+
const account = resolveFeishuRuntimeAccount({ cfg, accountId });
|
|
441
|
+
if (!account.configured) {
|
|
442
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const client = createFeishuClient(account);
|
|
446
|
+
|
|
447
|
+
const response = (await client.im.message.list({
|
|
448
|
+
params: {
|
|
449
|
+
container_id_type: "thread",
|
|
450
|
+
container_id: threadId,
|
|
451
|
+
// Fetch newest messages first so long threads keep the most recent turns.
|
|
452
|
+
// Results are reversed below to restore chronological order.
|
|
453
|
+
sort_type: "ByCreateTimeDesc",
|
|
454
|
+
page_size: Math.min(limit + 1, 50),
|
|
455
|
+
},
|
|
456
|
+
})) as {
|
|
457
|
+
code?: number;
|
|
458
|
+
msg?: string;
|
|
459
|
+
data?: {
|
|
460
|
+
items?: Array<
|
|
461
|
+
{
|
|
462
|
+
message_id?: string;
|
|
463
|
+
root_id?: string;
|
|
464
|
+
parent_id?: string;
|
|
465
|
+
} & FeishuMessageGetItem
|
|
466
|
+
>;
|
|
467
|
+
};
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
if (response.code !== 0) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Feishu thread list failed: code=${response.code} msg=${response.msg ?? "unknown"}`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const items = response.data?.items ?? [];
|
|
477
|
+
const results: FeishuThreadMessageInfo[] = [];
|
|
478
|
+
|
|
479
|
+
for (const item of items) {
|
|
480
|
+
if (currentMessageId && item.message_id === currentMessageId) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (rootMessageId && item.message_id === rootMessageId) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const parsed = parseFeishuMessageItem(item);
|
|
488
|
+
|
|
489
|
+
results.push({
|
|
490
|
+
messageId: parsed.messageId,
|
|
491
|
+
senderId: parsed.senderId,
|
|
492
|
+
senderType: parsed.senderType,
|
|
493
|
+
content: parsed.content,
|
|
494
|
+
contentType: parsed.contentType,
|
|
495
|
+
createTime: parsed.createTime,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (results.length >= limit) {
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Restore chronological order (oldest first) since we fetched newest-first.
|
|
504
|
+
results.reverse();
|
|
505
|
+
return results;
|
|
506
|
+
}
|
|
507
|
+
|
|
281
508
|
export type SendFeishuMessageParams = {
|
|
282
509
|
cfg: ClawdbotConfig;
|
|
283
510
|
to: string;
|
|
@@ -291,7 +518,7 @@ export type SendFeishuMessageParams = {
|
|
|
291
518
|
accountId?: string;
|
|
292
519
|
};
|
|
293
520
|
|
|
294
|
-
function buildFeishuPostMessagePayload(params: { messageText: string }): {
|
|
521
|
+
export function buildFeishuPostMessagePayload(params: { messageText: string }): {
|
|
295
522
|
content: string;
|
|
296
523
|
msgType: string;
|
|
297
524
|
} {
|
|
@@ -318,7 +545,7 @@ export async function sendMessageFeishu(
|
|
|
318
545
|
): Promise<FeishuSendResult> {
|
|
319
546
|
const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
|
|
320
547
|
const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId });
|
|
321
|
-
const tableMode =
|
|
548
|
+
const tableMode = resolveMarkdownTableMode({
|
|
322
549
|
cfg,
|
|
323
550
|
channel: "feishu",
|
|
324
551
|
});
|
|
@@ -328,7 +555,7 @@ export async function sendMessageFeishu(
|
|
|
328
555
|
if (mentions && mentions.length > 0) {
|
|
329
556
|
rawText = buildMentionedMessage(mentions, rawText);
|
|
330
557
|
}
|
|
331
|
-
const messageText =
|
|
558
|
+
const messageText = convertMarkdownTables(rawText, tableMode);
|
|
332
559
|
|
|
333
560
|
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
|
334
561
|
|
|
@@ -371,6 +598,59 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
|
|
|
371
598
|
});
|
|
372
599
|
}
|
|
373
600
|
|
|
601
|
+
export async function editMessageFeishu(params: {
|
|
602
|
+
cfg: ClawdbotConfig;
|
|
603
|
+
messageId: string;
|
|
604
|
+
text?: string;
|
|
605
|
+
card?: Record<string, unknown>;
|
|
606
|
+
accountId?: string;
|
|
607
|
+
}): Promise<{ messageId: string; contentType: "post" | "interactive" }> {
|
|
608
|
+
const { cfg, messageId, text, card, accountId } = params;
|
|
609
|
+
const account = resolveFeishuRuntimeAccount({ cfg, accountId });
|
|
610
|
+
if (!account.configured) {
|
|
611
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const hasText = typeof text === "string" && text.trim().length > 0;
|
|
615
|
+
const hasCard = Boolean(card);
|
|
616
|
+
if (hasText === hasCard) {
|
|
617
|
+
throw new Error("Feishu edit requires exactly one of text or card.");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const client = createFeishuClient(account);
|
|
621
|
+
|
|
622
|
+
if (card) {
|
|
623
|
+
const content = JSON.stringify(card);
|
|
624
|
+
const response = await client.im.message.patch({
|
|
625
|
+
path: { message_id: messageId },
|
|
626
|
+
data: { content },
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
if (response.code !== 0) {
|
|
630
|
+
throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return { messageId, contentType: "interactive" };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const tableMode = resolveMarkdownTableMode({
|
|
637
|
+
cfg,
|
|
638
|
+
channel: "feishu",
|
|
639
|
+
});
|
|
640
|
+
const messageText = convertMarkdownTables(text!, tableMode);
|
|
641
|
+
const payload = buildFeishuPostMessagePayload({ messageText });
|
|
642
|
+
const response = await client.im.message.patch({
|
|
643
|
+
path: { message_id: messageId },
|
|
644
|
+
data: { content: payload.content },
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
if (response.code !== 0) {
|
|
648
|
+
throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return { messageId, contentType: "post" };
|
|
652
|
+
}
|
|
653
|
+
|
|
374
654
|
export async function updateCardFeishu(params: {
|
|
375
655
|
cfg: ClawdbotConfig;
|
|
376
656
|
messageId: string;
|
|
@@ -378,7 +658,7 @@ export async function updateCardFeishu(params: {
|
|
|
378
658
|
accountId?: string;
|
|
379
659
|
}): Promise<void> {
|
|
380
660
|
const { cfg, messageId, card, accountId } = params;
|
|
381
|
-
const account =
|
|
661
|
+
const account = resolveFeishuRuntimeAccount({ cfg, accountId });
|
|
382
662
|
if (!account.configured) {
|
|
383
663
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
384
664
|
}
|
|
@@ -405,7 +685,7 @@ export function buildMarkdownCard(text: string): Record<string, unknown> {
|
|
|
405
685
|
return {
|
|
406
686
|
schema: "2.0",
|
|
407
687
|
config: {
|
|
408
|
-
|
|
688
|
+
width_mode: "fill",
|
|
409
689
|
},
|
|
410
690
|
body: {
|
|
411
691
|
elements: [
|
|
@@ -418,64 +698,97 @@ export function buildMarkdownCard(text: string): Record<string, unknown> {
|
|
|
418
698
|
};
|
|
419
699
|
}
|
|
420
700
|
|
|
701
|
+
/** Header configuration for structured Feishu cards. */
|
|
702
|
+
export type CardHeaderConfig = {
|
|
703
|
+
/** Header title text, e.g. "💻 Coder" */
|
|
704
|
+
title: string;
|
|
705
|
+
/** Feishu header color template (blue, green, red, orange, purple, grey, etc.). Defaults to "blue". */
|
|
706
|
+
template?: string;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
export function resolveFeishuCardTemplate(template?: string): string | undefined {
|
|
710
|
+
const normalized = normalizeOptionalLowercaseString(template);
|
|
711
|
+
if (!normalized || !FEISHU_CARD_TEMPLATES.has(normalized)) {
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
return normalized;
|
|
715
|
+
}
|
|
716
|
+
|
|
421
717
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
718
|
+
* Build a Feishu interactive card with optional header and note footer.
|
|
719
|
+
* When header/note are omitted, behaves identically to buildMarkdownCard.
|
|
424
720
|
*/
|
|
425
|
-
export
|
|
721
|
+
export function buildStructuredCard(
|
|
722
|
+
text: string,
|
|
723
|
+
options?: {
|
|
724
|
+
header?: CardHeaderConfig;
|
|
725
|
+
note?: string;
|
|
726
|
+
},
|
|
727
|
+
): Record<string, unknown> {
|
|
728
|
+
const elements: Record<string, unknown>[] = [{ tag: "markdown", content: text }];
|
|
729
|
+
if (options?.note) {
|
|
730
|
+
elements.push({ tag: "hr" });
|
|
731
|
+
elements.push({ tag: "markdown", content: `<font color='grey'>${options.note}</font>` });
|
|
732
|
+
}
|
|
733
|
+
const card: Record<string, unknown> = {
|
|
734
|
+
schema: "2.0",
|
|
735
|
+
config: { width_mode: "fill" },
|
|
736
|
+
body: { elements },
|
|
737
|
+
};
|
|
738
|
+
if (options?.header) {
|
|
739
|
+
card.header = {
|
|
740
|
+
title: { tag: "plain_text", content: options.header.title },
|
|
741
|
+
template: resolveFeishuCardTemplate(options.header.template) ?? "blue",
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
return card;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Send a message as a structured card with optional header and note.
|
|
749
|
+
*/
|
|
750
|
+
export async function sendStructuredCardFeishu(params: {
|
|
426
751
|
cfg: ClawdbotConfig;
|
|
427
752
|
to: string;
|
|
428
753
|
text: string;
|
|
429
754
|
replyToMessageId?: string;
|
|
430
755
|
/** When true, reply creates a Feishu topic thread instead of an inline reply */
|
|
431
756
|
replyInThread?: boolean;
|
|
432
|
-
/** Mention target users */
|
|
433
757
|
mentions?: MentionTarget[];
|
|
434
758
|
accountId?: string;
|
|
759
|
+
header?: CardHeaderConfig;
|
|
760
|
+
note?: string;
|
|
435
761
|
}): Promise<FeishuSendResult> {
|
|
436
|
-
const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } =
|
|
762
|
+
const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId, header, note } =
|
|
763
|
+
params;
|
|
437
764
|
let cardText = text;
|
|
438
765
|
if (mentions && mentions.length > 0) {
|
|
439
766
|
cardText = buildMentionedCardContent(mentions, text);
|
|
440
767
|
}
|
|
441
|
-
const card =
|
|
768
|
+
const card = buildStructuredCard(cardText, { header, note });
|
|
442
769
|
return sendCardFeishu({ cfg, to, card, replyToMessageId, replyInThread, accountId });
|
|
443
770
|
}
|
|
444
771
|
|
|
445
772
|
/**
|
|
446
|
-
*
|
|
447
|
-
*
|
|
773
|
+
* Send a message as a markdown card (interactive message).
|
|
774
|
+
* This renders markdown properly in Feishu (code blocks, tables, bold/italic, etc.)
|
|
448
775
|
*/
|
|
449
|
-
export async function
|
|
776
|
+
export async function sendMarkdownCardFeishu(params: {
|
|
450
777
|
cfg: ClawdbotConfig;
|
|
451
|
-
|
|
778
|
+
to: string;
|
|
452
779
|
text: string;
|
|
780
|
+
replyToMessageId?: string;
|
|
781
|
+
/** When true, reply creates a Feishu topic thread instead of an inline reply */
|
|
782
|
+
replyInThread?: boolean;
|
|
783
|
+
/** Mention target users */
|
|
784
|
+
mentions?: MentionTarget[];
|
|
453
785
|
accountId?: string;
|
|
454
|
-
}): Promise<
|
|
455
|
-
const { cfg,
|
|
456
|
-
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const client = createFeishuClient(account);
|
|
462
|
-
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
|
463
|
-
cfg,
|
|
464
|
-
channel: "feishu",
|
|
465
|
-
});
|
|
466
|
-
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
|
|
467
|
-
|
|
468
|
-
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
|
469
|
-
|
|
470
|
-
const response = await client.im.message.update({
|
|
471
|
-
path: { message_id: messageId },
|
|
472
|
-
data: {
|
|
473
|
-
msg_type: msgType,
|
|
474
|
-
content,
|
|
475
|
-
},
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
if (response.code !== 0) {
|
|
479
|
-
throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
|
|
786
|
+
}): Promise<FeishuSendResult> {
|
|
787
|
+
const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
|
|
788
|
+
let cardText = text;
|
|
789
|
+
if (mentions && mentions.length > 0) {
|
|
790
|
+
cardText = buildMentionedCardContent(mentions, text);
|
|
480
791
|
}
|
|
792
|
+
const card = buildMarkdownCard(cardText);
|
|
793
|
+
return sendCardFeishu({ cfg, to, card, replyToMessageId, replyInThread, accountId });
|
|
481
794
|
}
|