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