@openclaw/feishu 2026.3.12 → 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 +115 -22
- 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 +798 -786
- 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 +77 -25
- 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 +76 -35
- 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 +413 -87
- package/src/media.ts +488 -154
- 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 +220 -313
- 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 +194 -92
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +24 -36
- 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 +297 -39
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +272 -0
- package/src/monitor.webhook-security.test.ts +125 -91
- package/src/monitor.webhook.test-helpers.ts +116 -0
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +627 -53
- 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 +122 -118
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +23 -60
- 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 +721 -168
- package/src/reply-dispatcher.ts +422 -172
- 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 +127 -42
- package/src/send.test.ts +386 -4
- package/src/send.ts +486 -164
- 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/media.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { Readable } from "stream";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
5
|
+
import { mediaKindFromMime } from "openclaw/plugin-sdk/media-mime";
|
|
6
|
+
import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS, runFfmpeg } from "openclaw/plugin-sdk/media-runtime";
|
|
7
|
+
import {
|
|
8
|
+
resolvePreferredOpenClawTmpDir,
|
|
9
|
+
withTempDownloadPath,
|
|
10
|
+
} from "openclaw/plugin-sdk/temp-path";
|
|
11
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
12
|
+
import type { ClawdbotConfig } from "../runtime-api.js";
|
|
13
|
+
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
6
14
|
import { createFeishuClient } from "./client.js";
|
|
7
15
|
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
|
8
16
|
import { getFeishuRuntime } from "./runtime.js";
|
|
@@ -10,6 +18,24 @@ import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result
|
|
|
10
18
|
import { resolveFeishuSendTarget } from "./send-target.js";
|
|
11
19
|
|
|
12
20
|
const FEISHU_MEDIA_HTTP_TIMEOUT_MS = 120_000;
|
|
21
|
+
const FEISHU_VOICE_FILE_NAME = "voice.ogg";
|
|
22
|
+
const FEISHU_VOICE_SAMPLE_RATE_HZ = 48_000;
|
|
23
|
+
const FEISHU_VOICE_BITRATE = "64k";
|
|
24
|
+
|
|
25
|
+
const FEISHU_TRANSCODABLE_AUDIO_EXTS = new Set([
|
|
26
|
+
".aac",
|
|
27
|
+
".aiff",
|
|
28
|
+
".alac",
|
|
29
|
+
".amr",
|
|
30
|
+
".caf",
|
|
31
|
+
".flac",
|
|
32
|
+
".m4a",
|
|
33
|
+
".mp3",
|
|
34
|
+
".oga",
|
|
35
|
+
".wav",
|
|
36
|
+
".webm",
|
|
37
|
+
".wma",
|
|
38
|
+
]);
|
|
13
39
|
|
|
14
40
|
export type DownloadImageResult = {
|
|
15
41
|
buffer: Buffer;
|
|
@@ -22,62 +48,253 @@ export type DownloadMessageResourceResult = {
|
|
|
22
48
|
fileName?: string;
|
|
23
49
|
};
|
|
24
50
|
|
|
51
|
+
function createConfiguredFeishuMediaClient(params: { cfg: ClawdbotConfig; accountId?: string }): {
|
|
52
|
+
account: ReturnType<typeof resolveFeishuRuntimeAccount>;
|
|
53
|
+
client: ReturnType<typeof createFeishuClient>;
|
|
54
|
+
} {
|
|
55
|
+
const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
56
|
+
if (!account.configured) {
|
|
57
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
account,
|
|
62
|
+
client: createFeishuClient({
|
|
63
|
+
...account,
|
|
64
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type FeishuUploadResponse =
|
|
70
|
+
| Awaited<ReturnType<Lark.Client["im"]["image"]["create"]>>
|
|
71
|
+
| Awaited<ReturnType<Lark.Client["im"]["file"]["create"]>>;
|
|
72
|
+
|
|
73
|
+
type FeishuDownloadResponse =
|
|
74
|
+
| Awaited<ReturnType<Lark.Client["im"]["image"]["get"]>>
|
|
75
|
+
| Awaited<ReturnType<Lark.Client["im"]["file"]["get"]>>
|
|
76
|
+
| Awaited<ReturnType<Lark.Client["im"]["messageResource"]["get"]>>;
|
|
77
|
+
|
|
78
|
+
type FeishuHeaderMap = Record<string, string | string[]>;
|
|
79
|
+
type FeishuMessageResourceDownloadType = "image" | "file" | "media";
|
|
80
|
+
|
|
81
|
+
function asHeaderMap(value: object | undefined): FeishuHeaderMap | undefined {
|
|
82
|
+
if (!value) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const entries = Object.entries(value);
|
|
86
|
+
if (entries.every(([, entry]) => typeof entry === "string" || Array.isArray(entry))) {
|
|
87
|
+
return Object.fromEntries(entries) as FeishuHeaderMap;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractFeishuUploadKey(
|
|
93
|
+
response: FeishuUploadResponse,
|
|
94
|
+
params: {
|
|
95
|
+
key: "image_key" | "file_key";
|
|
96
|
+
errorPrefix: string;
|
|
97
|
+
},
|
|
98
|
+
): string {
|
|
99
|
+
if (!response) {
|
|
100
|
+
throw new Error(`${params.errorPrefix}: empty response`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const wrappedResponse = response as {
|
|
104
|
+
image_key?: string;
|
|
105
|
+
file_key?: string;
|
|
106
|
+
code?: number;
|
|
107
|
+
msg?: string;
|
|
108
|
+
data?: Partial<Record<"image_key" | "file_key", string>>;
|
|
109
|
+
};
|
|
110
|
+
if (wrappedResponse.code !== undefined && wrappedResponse.code !== 0) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`${params.errorPrefix}: ${wrappedResponse.msg || `code ${wrappedResponse.code}`}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const key =
|
|
117
|
+
params.key === "image_key"
|
|
118
|
+
? (wrappedResponse.image_key ?? wrappedResponse.data?.image_key)
|
|
119
|
+
: (wrappedResponse.file_key ?? wrappedResponse.data?.file_key);
|
|
120
|
+
if (!key) {
|
|
121
|
+
throw new Error(`${params.errorPrefix}: no ${params.key} returned`);
|
|
122
|
+
}
|
|
123
|
+
return key;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readHeaderValue(
|
|
127
|
+
headers: Record<string, unknown> | undefined,
|
|
128
|
+
name: string,
|
|
129
|
+
): string | undefined {
|
|
130
|
+
if (!headers) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
134
|
+
if (normalizeLowercaseStringOrEmpty(key) !== normalizeLowercaseStringOrEmpty(name)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "string" && value.trim()) {
|
|
138
|
+
return value.trim();
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
const first = value.find((entry) => typeof entry === "string" && entry.trim());
|
|
142
|
+
if (typeof first === "string") {
|
|
143
|
+
return first.trim();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readHttpStatusFromError(error: unknown): number | undefined {
|
|
151
|
+
if (!error || typeof error !== "object") {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const response = (error as { response?: unknown }).response;
|
|
156
|
+
if (response && typeof response === "object") {
|
|
157
|
+
const status = (response as { status?: unknown }).status;
|
|
158
|
+
if (typeof status === "number") {
|
|
159
|
+
return status;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const status = (error as { status?: unknown }).status;
|
|
164
|
+
return typeof status === "number" ? status : undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isHttpStatusError(error: unknown, status: number): boolean {
|
|
168
|
+
return readHttpStatusFromError(error) === status;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function containsEastAsianScript(value: string): boolean {
|
|
172
|
+
return /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function recoverUtf8FileNameFromLatin1Header(value: string): string {
|
|
176
|
+
const recovered = Buffer.from(value, "latin1").toString("utf8");
|
|
177
|
+
if (recovered !== value && !recovered.includes("\uFFFD") && containsEastAsianScript(recovered)) {
|
|
178
|
+
return recovered;
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function decodeDispositionFileName(value: string): string | undefined {
|
|
184
|
+
const utf8Match = value.match(/filename\*=UTF-8''([^;]+)/i);
|
|
185
|
+
if (utf8Match?.[1]) {
|
|
186
|
+
try {
|
|
187
|
+
return decodeURIComponent(utf8Match[1].trim().replace(/^"(.*)"$/, "$1"));
|
|
188
|
+
} catch {
|
|
189
|
+
return utf8Match[1].trim().replace(/^"(.*)"$/, "$1");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const plainMatch = value.match(/filename="?([^";]+)"?/i);
|
|
194
|
+
const plainFileName = plainMatch?.[1]?.trim();
|
|
195
|
+
return plainFileName ? recoverUtf8FileNameFromLatin1Header(plainFileName) : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractFeishuDownloadMetadata(response: FeishuDownloadResponse): {
|
|
199
|
+
contentType?: string;
|
|
200
|
+
fileName?: string;
|
|
201
|
+
} {
|
|
202
|
+
const responseWithOptionalFields = response as FeishuDownloadResponse & {
|
|
203
|
+
header?: object;
|
|
204
|
+
contentType?: string;
|
|
205
|
+
mime_type?: string;
|
|
206
|
+
data?: {
|
|
207
|
+
contentType?: string;
|
|
208
|
+
mime_type?: string;
|
|
209
|
+
file_name?: string;
|
|
210
|
+
fileName?: string;
|
|
211
|
+
};
|
|
212
|
+
file_name?: string;
|
|
213
|
+
fileName?: string;
|
|
214
|
+
};
|
|
215
|
+
const headers =
|
|
216
|
+
asHeaderMap(responseWithOptionalFields.headers) ??
|
|
217
|
+
asHeaderMap(responseWithOptionalFields.header);
|
|
218
|
+
|
|
219
|
+
const contentType =
|
|
220
|
+
readHeaderValue(headers, "content-type") ??
|
|
221
|
+
responseWithOptionalFields.contentType ??
|
|
222
|
+
responseWithOptionalFields.mime_type ??
|
|
223
|
+
responseWithOptionalFields.data?.contentType ??
|
|
224
|
+
responseWithOptionalFields.data?.mime_type;
|
|
225
|
+
|
|
226
|
+
const disposition = readHeaderValue(headers, "content-disposition");
|
|
227
|
+
const fileName =
|
|
228
|
+
(disposition ? decodeDispositionFileName(disposition) : undefined) ??
|
|
229
|
+
responseWithOptionalFields.file_name ??
|
|
230
|
+
responseWithOptionalFields.fileName ??
|
|
231
|
+
responseWithOptionalFields.data?.file_name ??
|
|
232
|
+
responseWithOptionalFields.data?.fileName;
|
|
233
|
+
|
|
234
|
+
return { contentType, fileName };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function readReadableBuffer(stream: Readable): Promise<Buffer> {
|
|
238
|
+
const chunks: Buffer[] = [];
|
|
239
|
+
for await (const chunk of stream) {
|
|
240
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
241
|
+
}
|
|
242
|
+
return Buffer.concat(chunks);
|
|
243
|
+
}
|
|
244
|
+
|
|
25
245
|
async function readFeishuResponseBuffer(params: {
|
|
26
|
-
response:
|
|
246
|
+
response: FeishuDownloadResponse;
|
|
27
247
|
tmpDirPrefix: string;
|
|
28
248
|
errorPrefix: string;
|
|
29
249
|
}): Promise<Buffer> {
|
|
30
250
|
const { response } = params;
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
|
|
32
|
-
const responseAny = response as any;
|
|
33
|
-
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
|
34
|
-
throw new Error(`${params.errorPrefix}: ${responseAny.msg || `code ${responseAny.code}`}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
251
|
if (Buffer.isBuffer(response)) {
|
|
38
252
|
return response;
|
|
39
253
|
}
|
|
40
254
|
if (response instanceof ArrayBuffer) {
|
|
41
255
|
return Buffer.from(response);
|
|
42
256
|
}
|
|
43
|
-
|
|
44
|
-
|
|
257
|
+
const responseWithOptionalFields = response as FeishuDownloadResponse & {
|
|
258
|
+
code?: number;
|
|
259
|
+
msg?: string;
|
|
260
|
+
data?: Buffer | ArrayBuffer;
|
|
261
|
+
[Symbol.asyncIterator]?: () => AsyncIterator<Buffer | Uint8Array | string>;
|
|
262
|
+
};
|
|
263
|
+
if (responseWithOptionalFields.code !== undefined && responseWithOptionalFields.code !== 0) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`${params.errorPrefix}: ${responseWithOptionalFields.msg || `code ${responseWithOptionalFields.code}`}`,
|
|
266
|
+
);
|
|
45
267
|
}
|
|
46
|
-
|
|
47
|
-
|
|
268
|
+
|
|
269
|
+
if (responseWithOptionalFields.data && Buffer.isBuffer(responseWithOptionalFields.data)) {
|
|
270
|
+
return responseWithOptionalFields.data;
|
|
48
271
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return Buffer.concat(chunks);
|
|
272
|
+
if (responseWithOptionalFields.data instanceof ArrayBuffer) {
|
|
273
|
+
return Buffer.from(responseWithOptionalFields.data);
|
|
274
|
+
}
|
|
275
|
+
if (typeof response.getReadableStream === "function") {
|
|
276
|
+
return readReadableBuffer(response.getReadableStream());
|
|
56
277
|
}
|
|
57
|
-
if (typeof
|
|
278
|
+
if (typeof response.writeFile === "function") {
|
|
58
279
|
return await withTempDownloadPath({ prefix: params.tmpDirPrefix }, async (tmpPath) => {
|
|
59
|
-
await
|
|
280
|
+
await response.writeFile(tmpPath);
|
|
60
281
|
return await fs.promises.readFile(tmpPath);
|
|
61
282
|
});
|
|
62
283
|
}
|
|
63
|
-
if (
|
|
284
|
+
if (responseWithOptionalFields[Symbol.asyncIterator]) {
|
|
285
|
+
const asyncIterable = responseWithOptionalFields as AsyncIterable<Buffer | Uint8Array | string>;
|
|
64
286
|
const chunks: Buffer[] = [];
|
|
65
|
-
for await (const chunk of
|
|
287
|
+
for await (const chunk of asyncIterable) {
|
|
66
288
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
67
289
|
}
|
|
68
290
|
return Buffer.concat(chunks);
|
|
69
291
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
for await (const chunk of responseAny as Readable) {
|
|
73
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
74
|
-
}
|
|
75
|
-
return Buffer.concat(chunks);
|
|
292
|
+
if (response instanceof Readable) {
|
|
293
|
+
return readReadableBuffer(response);
|
|
76
294
|
}
|
|
77
295
|
|
|
78
|
-
const keys = Object.keys(
|
|
79
|
-
|
|
80
|
-
throw new Error(`${params.errorPrefix}: unexpected response format. Keys: [${types}]`);
|
|
296
|
+
const keys = Object.keys(response as object);
|
|
297
|
+
throw new Error(`${params.errorPrefix}: unexpected response format. Keys: [${keys.join(", ")}]`);
|
|
81
298
|
}
|
|
82
299
|
|
|
83
300
|
/**
|
|
@@ -94,15 +311,7 @@ export async function downloadImageFeishu(params: {
|
|
|
94
311
|
if (!normalizedImageKey) {
|
|
95
312
|
throw new Error("Feishu image download failed: invalid image_key");
|
|
96
313
|
}
|
|
97
|
-
const
|
|
98
|
-
if (!account.configured) {
|
|
99
|
-
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const client = createFeishuClient({
|
|
103
|
-
...account,
|
|
104
|
-
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
105
|
-
});
|
|
314
|
+
const { client } = createConfiguredFeishuMediaClient({ cfg, accountId });
|
|
106
315
|
|
|
107
316
|
const response = await client.im.image.get({
|
|
108
317
|
path: { image_key: normalizedImageKey },
|
|
@@ -113,7 +322,27 @@ export async function downloadImageFeishu(params: {
|
|
|
113
322
|
tmpDirPrefix: "openclaw-feishu-img-",
|
|
114
323
|
errorPrefix: "Feishu image download failed",
|
|
115
324
|
});
|
|
116
|
-
|
|
325
|
+
const meta = extractFeishuDownloadMetadata(response);
|
|
326
|
+
return { buffer, contentType: meta.contentType };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function downloadMessageResourceWithType(params: {
|
|
330
|
+
client: ReturnType<typeof createFeishuClient>;
|
|
331
|
+
messageId: string;
|
|
332
|
+
fileKey: string;
|
|
333
|
+
type: FeishuMessageResourceDownloadType;
|
|
334
|
+
}): Promise<DownloadMessageResourceResult> {
|
|
335
|
+
const response = await params.client.im.messageResource.get({
|
|
336
|
+
path: { message_id: params.messageId, file_key: params.fileKey },
|
|
337
|
+
params: { type: params.type },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const buffer = await readFeishuResponseBuffer({
|
|
341
|
+
response,
|
|
342
|
+
tmpDirPrefix: "openclaw-feishu-resource-",
|
|
343
|
+
errorPrefix: "Feishu message resource download failed",
|
|
344
|
+
});
|
|
345
|
+
return { buffer, ...extractFeishuDownloadMetadata(response) };
|
|
117
346
|
}
|
|
118
347
|
|
|
119
348
|
/**
|
|
@@ -132,27 +361,30 @@ export async function downloadMessageResourceFeishu(params: {
|
|
|
132
361
|
if (!normalizedFileKey) {
|
|
133
362
|
throw new Error("Feishu message resource download failed: invalid file_key");
|
|
134
363
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
364
|
+
const { client } = createConfiguredFeishuMediaClient({ cfg, accountId });
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
return await downloadMessageResourceWithType({
|
|
368
|
+
client,
|
|
369
|
+
messageId,
|
|
370
|
+
fileKey: normalizedFileKey,
|
|
371
|
+
type,
|
|
372
|
+
});
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (type !== "file" || !isHttpStatusError(err, 502)) {
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
return await downloadMessageResourceWithType({
|
|
379
|
+
client,
|
|
380
|
+
messageId,
|
|
381
|
+
fileKey: normalizedFileKey,
|
|
382
|
+
type: "media",
|
|
383
|
+
});
|
|
384
|
+
} catch {
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
138
387
|
}
|
|
139
|
-
|
|
140
|
-
const client = createFeishuClient({
|
|
141
|
-
...account,
|
|
142
|
-
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const response = await client.im.messageResource.get({
|
|
146
|
-
path: { message_id: messageId, file_key: normalizedFileKey },
|
|
147
|
-
params: { type },
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const buffer = await readFeishuResponseBuffer({
|
|
151
|
-
response,
|
|
152
|
-
tmpDirPrefix: "openclaw-feishu-resource-",
|
|
153
|
-
errorPrefix: "Feishu message resource download failed",
|
|
154
|
-
});
|
|
155
|
-
return { buffer };
|
|
156
388
|
}
|
|
157
389
|
|
|
158
390
|
export type UploadImageResult = {
|
|
@@ -179,15 +411,7 @@ export async function uploadImageFeishu(params: {
|
|
|
179
411
|
accountId?: string;
|
|
180
412
|
}): Promise<UploadImageResult> {
|
|
181
413
|
const { cfg, image, imageType = "message", accountId } = params;
|
|
182
|
-
const
|
|
183
|
-
if (!account.configured) {
|
|
184
|
-
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const client = createFeishuClient({
|
|
188
|
-
...account,
|
|
189
|
-
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
190
|
-
});
|
|
414
|
+
const { client } = createConfiguredFeishuMediaClient({ cfg, accountId });
|
|
191
415
|
|
|
192
416
|
// SDK accepts Buffer directly or fs.ReadStream for file paths
|
|
193
417
|
// Using Readable.from(buffer) causes issues with form-data library
|
|
@@ -197,43 +421,30 @@ export async function uploadImageFeishu(params: {
|
|
|
197
421
|
const response = await client.im.image.create({
|
|
198
422
|
data: {
|
|
199
423
|
image_type: imageType,
|
|
200
|
-
|
|
201
|
-
image: imageData as any,
|
|
424
|
+
image: imageData,
|
|
202
425
|
},
|
|
203
426
|
});
|
|
204
427
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const imageKey = responseAny.image_key ?? responseAny.data?.image_key;
|
|
214
|
-
if (!imageKey) {
|
|
215
|
-
throw new Error("Feishu image upload failed: no image_key returned");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return { imageKey };
|
|
428
|
+
return {
|
|
429
|
+
imageKey: extractFeishuUploadKey(response, {
|
|
430
|
+
key: "image_key",
|
|
431
|
+
errorPrefix: "Feishu image upload failed",
|
|
432
|
+
}),
|
|
433
|
+
};
|
|
219
434
|
}
|
|
220
435
|
|
|
221
436
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* the
|
|
225
|
-
*
|
|
226
|
-
*
|
|
437
|
+
* Sanitize a filename for safe use in Feishu multipart/form-data uploads.
|
|
438
|
+
* Strips control characters and multipart-injection vectors (CWE-93) while
|
|
439
|
+
* preserving the original UTF-8 display name (Chinese, emoji, etc.).
|
|
440
|
+
*
|
|
441
|
+
* Previous versions percent-encoded non-ASCII characters, but the Feishu
|
|
442
|
+
* `im.file.create` API uses `file_name` as a literal display name — it does
|
|
443
|
+
* NOT decode percent-encoding — so encoded filenames appeared as garbled text
|
|
444
|
+
* in chat (regression in v2026.3.2).
|
|
227
445
|
*/
|
|
228
446
|
export function sanitizeFileNameForUpload(fileName: string): string {
|
|
229
|
-
|
|
230
|
-
if (ASCII_ONLY.test(fileName)) {
|
|
231
|
-
return fileName;
|
|
232
|
-
}
|
|
233
|
-
return encodeURIComponent(fileName)
|
|
234
|
-
.replace(/'/g, "%27")
|
|
235
|
-
.replace(/\(/g, "%28")
|
|
236
|
-
.replace(/\)/g, "%29");
|
|
447
|
+
return fileName.replace(/[\p{Cc}"\\]/gu, "_");
|
|
237
448
|
}
|
|
238
449
|
|
|
239
450
|
/**
|
|
@@ -249,15 +460,7 @@ export async function uploadFileFeishu(params: {
|
|
|
249
460
|
accountId?: string;
|
|
250
461
|
}): Promise<UploadFileResult> {
|
|
251
462
|
const { cfg, file, fileName, fileType, duration, accountId } = params;
|
|
252
|
-
const
|
|
253
|
-
if (!account.configured) {
|
|
254
|
-
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const client = createFeishuClient({
|
|
258
|
-
...account,
|
|
259
|
-
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
260
|
-
});
|
|
463
|
+
const { client } = createConfiguredFeishuMediaClient({ cfg, accountId });
|
|
261
464
|
|
|
262
465
|
// SDK accepts Buffer directly or fs.ReadStream for file paths
|
|
263
466
|
// Using Readable.from(buffer) causes issues with form-data library
|
|
@@ -270,25 +473,17 @@ export async function uploadFileFeishu(params: {
|
|
|
270
473
|
data: {
|
|
271
474
|
file_type: fileType,
|
|
272
475
|
file_name: safeFileName,
|
|
273
|
-
|
|
274
|
-
file: fileData as any,
|
|
476
|
+
file: fileData,
|
|
275
477
|
...(duration !== undefined && { duration }),
|
|
276
478
|
},
|
|
277
479
|
});
|
|
278
480
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const fileKey = responseAny.file_key ?? responseAny.data?.file_key;
|
|
287
|
-
if (!fileKey) {
|
|
288
|
-
throw new Error("Feishu file upload failed: no file_key returned");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return { fileKey };
|
|
481
|
+
return {
|
|
482
|
+
fileKey: extractFeishuUploadKey(response, {
|
|
483
|
+
key: "file_key",
|
|
484
|
+
errorPrefix: "Feishu file upload failed",
|
|
485
|
+
}),
|
|
486
|
+
};
|
|
292
487
|
}
|
|
293
488
|
|
|
294
489
|
/**
|
|
@@ -388,7 +583,7 @@ export async function sendFileFeishu(params: {
|
|
|
388
583
|
export function detectFileType(
|
|
389
584
|
fileName: string,
|
|
390
585
|
): "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream" {
|
|
391
|
-
const ext = path.extname(fileName)
|
|
586
|
+
const ext = normalizeLowercaseStringOrEmpty(path.extname(fileName));
|
|
392
587
|
switch (ext) {
|
|
393
588
|
case ".opus":
|
|
394
589
|
case ".ogg":
|
|
@@ -413,6 +608,136 @@ export function detectFileType(
|
|
|
413
608
|
}
|
|
414
609
|
}
|
|
415
610
|
|
|
611
|
+
function resolveFeishuOutboundMediaKind(params: { fileName: string; contentType?: string }): {
|
|
612
|
+
fileType?: "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream";
|
|
613
|
+
msgType: "image" | "file" | "audio" | "media";
|
|
614
|
+
} {
|
|
615
|
+
const { fileName, contentType } = params;
|
|
616
|
+
const ext = normalizeLowercaseStringOrEmpty(path.extname(fileName));
|
|
617
|
+
const mimeKind = mediaKindFromMime(contentType);
|
|
618
|
+
|
|
619
|
+
const isImageExt = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes(
|
|
620
|
+
ext,
|
|
621
|
+
);
|
|
622
|
+
if (isImageExt || mimeKind === "image") {
|
|
623
|
+
return { msgType: "image" };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (
|
|
627
|
+
ext === ".opus" ||
|
|
628
|
+
ext === ".ogg" ||
|
|
629
|
+
contentType === "audio/ogg" ||
|
|
630
|
+
contentType === "audio/opus"
|
|
631
|
+
) {
|
|
632
|
+
return { fileType: "opus", msgType: "audio" };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (
|
|
636
|
+
[".mp4", ".mov", ".avi"].includes(ext) ||
|
|
637
|
+
contentType === "video/mp4" ||
|
|
638
|
+
contentType === "video/quicktime" ||
|
|
639
|
+
contentType === "video/x-msvideo"
|
|
640
|
+
) {
|
|
641
|
+
return { fileType: "mp4", msgType: "media" };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const fileType = detectFileType(fileName);
|
|
645
|
+
return {
|
|
646
|
+
fileType,
|
|
647
|
+
msgType:
|
|
648
|
+
fileType === "stream"
|
|
649
|
+
? "file"
|
|
650
|
+
: fileType === "opus"
|
|
651
|
+
? "audio"
|
|
652
|
+
: fileType === "mp4"
|
|
653
|
+
? "media"
|
|
654
|
+
: "file",
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function isFeishuNativeVoiceAudio(params: { fileName: string; contentType?: string }): boolean {
|
|
659
|
+
const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName));
|
|
660
|
+
const contentType = normalizeLowercaseStringOrEmpty(params.contentType);
|
|
661
|
+
return (
|
|
662
|
+
ext === ".opus" || ext === ".ogg" || contentType === "audio/ogg" || contentType === "audio/opus"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function isLikelyTranscodableAudio(params: { fileName: string; contentType?: string }): boolean {
|
|
667
|
+
const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName));
|
|
668
|
+
const contentType = normalizeLowercaseStringOrEmpty(params.contentType);
|
|
669
|
+
return FEISHU_TRANSCODABLE_AUDIO_EXTS.has(ext) || mediaKindFromMime(contentType) === "audio";
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function transcodeToFeishuVoiceOpus(params: {
|
|
673
|
+
buffer: Buffer;
|
|
674
|
+
fileName: string;
|
|
675
|
+
contentType?: string;
|
|
676
|
+
}): Promise<{ buffer: Buffer; fileName: string; contentType: string }> {
|
|
677
|
+
const tempRoot = resolvePreferredOpenClawTmpDir();
|
|
678
|
+
await fs.promises.mkdir(tempRoot, { recursive: true, mode: 0o700 });
|
|
679
|
+
const tempDir = await fs.promises.mkdtemp(path.join(tempRoot, "feishu-voice-"));
|
|
680
|
+
try {
|
|
681
|
+
const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName));
|
|
682
|
+
const inputExt = ext && ext.length <= 12 ? ext : ".audio";
|
|
683
|
+
const inputPath = path.join(tempDir, `input${inputExt}`);
|
|
684
|
+
const outputPath = path.join(tempDir, FEISHU_VOICE_FILE_NAME);
|
|
685
|
+
await fs.promises.writeFile(inputPath, params.buffer, { mode: 0o600 });
|
|
686
|
+
await runFfmpeg([
|
|
687
|
+
"-hide_banner",
|
|
688
|
+
"-loglevel",
|
|
689
|
+
"error",
|
|
690
|
+
"-y",
|
|
691
|
+
"-i",
|
|
692
|
+
inputPath,
|
|
693
|
+
"-vn",
|
|
694
|
+
"-sn",
|
|
695
|
+
"-dn",
|
|
696
|
+
"-t",
|
|
697
|
+
String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS),
|
|
698
|
+
"-ar",
|
|
699
|
+
String(FEISHU_VOICE_SAMPLE_RATE_HZ),
|
|
700
|
+
"-ac",
|
|
701
|
+
"1",
|
|
702
|
+
"-c:a",
|
|
703
|
+
"libopus",
|
|
704
|
+
"-b:a",
|
|
705
|
+
FEISHU_VOICE_BITRATE,
|
|
706
|
+
outputPath,
|
|
707
|
+
]);
|
|
708
|
+
return {
|
|
709
|
+
buffer: await fs.promises.readFile(outputPath),
|
|
710
|
+
fileName: FEISHU_VOICE_FILE_NAME,
|
|
711
|
+
contentType: "audio/ogg",
|
|
712
|
+
};
|
|
713
|
+
} finally {
|
|
714
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function prepareFeishuVoiceMedia(params: {
|
|
719
|
+
buffer: Buffer;
|
|
720
|
+
fileName: string;
|
|
721
|
+
contentType?: string;
|
|
722
|
+
audioAsVoice?: boolean;
|
|
723
|
+
}): Promise<{ buffer: Buffer; fileName: string; contentType?: string }> {
|
|
724
|
+
if (isFeishuNativeVoiceAudio(params)) {
|
|
725
|
+
return params;
|
|
726
|
+
}
|
|
727
|
+
if (params.audioAsVoice !== true || !isLikelyTranscodableAudio(params)) {
|
|
728
|
+
return params;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
return await transcodeToFeishuVoiceOpus(params);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
console.warn(
|
|
734
|
+
`[feishu] audioAsVoice transcode failed; sending ${params.fileName} as a file attachment:`,
|
|
735
|
+
err,
|
|
736
|
+
);
|
|
737
|
+
return params;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
416
741
|
/**
|
|
417
742
|
* Upload and send media (image or file) from URL, local path, or buffer.
|
|
418
743
|
* When mediaUrl is a local path, mediaLocalRoots (from core outbound context)
|
|
@@ -429,6 +754,8 @@ export async function sendMediaFeishu(params: {
|
|
|
429
754
|
accountId?: string;
|
|
430
755
|
/** Allowed roots for local path reads; required for local filePath to work. */
|
|
431
756
|
mediaLocalRoots?: readonly string[];
|
|
757
|
+
/** When true, transcode compatible audio to Feishu native Ogg/Opus voice bubbles. */
|
|
758
|
+
audioAsVoice?: boolean;
|
|
432
759
|
}): Promise<SendMediaResult> {
|
|
433
760
|
const {
|
|
434
761
|
cfg,
|
|
@@ -440,8 +767,9 @@ export async function sendMediaFeishu(params: {
|
|
|
440
767
|
replyInThread,
|
|
441
768
|
accountId,
|
|
442
769
|
mediaLocalRoots,
|
|
770
|
+
audioAsVoice,
|
|
443
771
|
} = params;
|
|
444
|
-
const account =
|
|
772
|
+
const account = resolveFeishuRuntimeAccount({ cfg, accountId });
|
|
445
773
|
if (!account.configured) {
|
|
446
774
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
447
775
|
}
|
|
@@ -449,6 +777,7 @@ export async function sendMediaFeishu(params: {
|
|
|
449
777
|
|
|
450
778
|
let buffer: Buffer;
|
|
451
779
|
let name: string;
|
|
780
|
+
let contentType: string | undefined;
|
|
452
781
|
|
|
453
782
|
if (mediaBuffer) {
|
|
454
783
|
buffer = mediaBuffer;
|
|
@@ -461,36 +790,41 @@ export async function sendMediaFeishu(params: {
|
|
|
461
790
|
});
|
|
462
791
|
buffer = loaded.buffer;
|
|
463
792
|
name = fileName ?? loaded.fileName ?? "file";
|
|
793
|
+
contentType = loaded.contentType;
|
|
464
794
|
} else {
|
|
465
795
|
throw new Error("Either mediaUrl or mediaBuffer must be provided");
|
|
466
796
|
}
|
|
467
797
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
798
|
+
const prepared = await prepareFeishuVoiceMedia({
|
|
799
|
+
buffer,
|
|
800
|
+
fileName: name,
|
|
801
|
+
contentType,
|
|
802
|
+
audioAsVoice,
|
|
803
|
+
});
|
|
804
|
+
buffer = prepared.buffer;
|
|
805
|
+
name = prepared.fileName;
|
|
806
|
+
contentType = prepared.contentType;
|
|
807
|
+
|
|
808
|
+
const routing = resolveFeishuOutboundMediaKind({ fileName: name, contentType });
|
|
471
809
|
|
|
472
|
-
if (
|
|
810
|
+
if (routing.msgType === "image") {
|
|
473
811
|
const { imageKey } = await uploadImageFeishu({ cfg, image: buffer, accountId });
|
|
474
812
|
return sendImageFeishu({ cfg, to, imageKey, replyToMessageId, replyInThread, accountId });
|
|
475
|
-
} else {
|
|
476
|
-
const fileType = detectFileType(name);
|
|
477
|
-
const { fileKey } = await uploadFileFeishu({
|
|
478
|
-
cfg,
|
|
479
|
-
file: buffer,
|
|
480
|
-
fileName: name,
|
|
481
|
-
fileType,
|
|
482
|
-
accountId,
|
|
483
|
-
});
|
|
484
|
-
// Feishu API: opus -> "audio", mp4/video -> "media" (playable), others -> "file"
|
|
485
|
-
const msgType = fileType === "opus" ? "audio" : fileType === "mp4" ? "media" : "file";
|
|
486
|
-
return sendFileFeishu({
|
|
487
|
-
cfg,
|
|
488
|
-
to,
|
|
489
|
-
fileKey,
|
|
490
|
-
msgType,
|
|
491
|
-
replyToMessageId,
|
|
492
|
-
replyInThread,
|
|
493
|
-
accountId,
|
|
494
|
-
});
|
|
495
813
|
}
|
|
814
|
+
const { fileKey } = await uploadFileFeishu({
|
|
815
|
+
cfg,
|
|
816
|
+
file: buffer,
|
|
817
|
+
fileName: name,
|
|
818
|
+
fileType: routing.fileType ?? "stream",
|
|
819
|
+
accountId,
|
|
820
|
+
});
|
|
821
|
+
return sendFileFeishu({
|
|
822
|
+
cfg,
|
|
823
|
+
to,
|
|
824
|
+
fileKey,
|
|
825
|
+
msgType: routing.msgType,
|
|
826
|
+
replyToMessageId,
|
|
827
|
+
replyInThread,
|
|
828
|
+
accountId,
|
|
829
|
+
});
|
|
496
830
|
}
|