@openclaw-china/wecom-app 0.1.2 → 0.1.3
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/dist/index.d.ts +23 -0
- package/dist/index.js +231 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ type WecomAppAccountConfig = {
|
|
|
41
41
|
};
|
|
42
42
|
/** 媒体文件大小限制 (MB),默认 100 */
|
|
43
43
|
maxFileSizeMB?: number;
|
|
44
|
+
/**
|
|
45
|
+
* 语音发送转码策略(可选)
|
|
46
|
+
* enabled=true 时:当检测到 wav/mp3 等不支持的语音格式,
|
|
47
|
+
* - 若系统存在 ffmpeg:自动转码为 amr 再以 voice 发送
|
|
48
|
+
* - 若无 ffmpeg:降级为 file 发送
|
|
49
|
+
*/
|
|
50
|
+
voiceTranscode?: {
|
|
51
|
+
enabled?: boolean;
|
|
52
|
+
prefer?: "amr";
|
|
53
|
+
};
|
|
44
54
|
/** 欢迎文本 */
|
|
45
55
|
welcomeText?: string;
|
|
46
56
|
/** DM 策略 */
|
|
@@ -281,6 +291,19 @@ declare const wecomAppPlugin: {
|
|
|
281
291
|
};
|
|
282
292
|
};
|
|
283
293
|
};
|
|
294
|
+
voiceTranscode: {
|
|
295
|
+
type: string;
|
|
296
|
+
additionalProperties: boolean;
|
|
297
|
+
properties: {
|
|
298
|
+
enabled: {
|
|
299
|
+
type: string;
|
|
300
|
+
};
|
|
301
|
+
prefer: {
|
|
302
|
+
type: string;
|
|
303
|
+
enum: string[];
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
};
|
|
284
307
|
welcomeText: {
|
|
285
308
|
type: string;
|
|
286
309
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { homedir, tmpdir } from 'os';
|
|
2
|
+
import { join, extname, basename } from 'path';
|
|
1
3
|
import crypto from 'crypto';
|
|
2
|
-
import { mkdir, writeFile, unlink } from 'fs/promises';
|
|
3
|
-
import {
|
|
4
|
-
import { tmpdir } from 'os';
|
|
4
|
+
import { mkdir, writeFile, rename, unlink, readdir, stat } from 'fs/promises';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
5
6
|
|
|
6
7
|
var __defProp = Object.defineProperty;
|
|
7
8
|
var __export = (target, all) => {
|
|
@@ -4048,8 +4049,6 @@ var coerce = {
|
|
|
4048
4049
|
date: ((arg) => ZodDate.create({ ...arg, coerce: true }))
|
|
4049
4050
|
};
|
|
4050
4051
|
var NEVER = INVALID;
|
|
4051
|
-
|
|
4052
|
-
// src/config.ts
|
|
4053
4052
|
var DEFAULT_ACCOUNT_ID = "default";
|
|
4054
4053
|
var WecomAppAccountSchema = external_exports.object({
|
|
4055
4054
|
name: external_exports.string().optional(),
|
|
@@ -4071,6 +4070,13 @@ var WecomAppAccountSchema = external_exports.object({
|
|
|
4071
4070
|
maxBytes: external_exports.number().optional(),
|
|
4072
4071
|
keepDays: external_exports.number().optional()
|
|
4073
4072
|
}).optional(),
|
|
4073
|
+
// 语音发送策略(可选):当遇到不支持的格式(如 wav/mp3)时,
|
|
4074
|
+
// - enabled=true 且系统存在 ffmpeg:自动转码为 amr 后再发送 voice
|
|
4075
|
+
// - 否则:降级为 file 发送(并可配合 caption 提示)
|
|
4076
|
+
voiceTranscode: external_exports.object({
|
|
4077
|
+
enabled: external_exports.boolean().optional(),
|
|
4078
|
+
prefer: external_exports.enum(["amr"]).optional()
|
|
4079
|
+
}).optional(),
|
|
4074
4080
|
// 其他字段
|
|
4075
4081
|
welcomeText: external_exports.string().optional(),
|
|
4076
4082
|
dmPolicy: external_exports.enum(["open", "pairing", "allowlist", "disabled"]).optional(),
|
|
@@ -4107,6 +4113,14 @@ var WecomAppConfigJsonSchema = {
|
|
|
4107
4113
|
keepDays: { type: "number" }
|
|
4108
4114
|
}
|
|
4109
4115
|
},
|
|
4116
|
+
voiceTranscode: {
|
|
4117
|
+
type: "object",
|
|
4118
|
+
additionalProperties: false,
|
|
4119
|
+
properties: {
|
|
4120
|
+
enabled: { type: "boolean" },
|
|
4121
|
+
prefer: { type: "string", enum: ["amr"] }
|
|
4122
|
+
}
|
|
4123
|
+
},
|
|
4110
4124
|
welcomeText: { type: "string" },
|
|
4111
4125
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist", "disabled"] },
|
|
4112
4126
|
allowFrom: { type: "array", items: { type: "string" } },
|
|
@@ -4234,15 +4248,24 @@ function resolveAllowFrom(config) {
|
|
|
4234
4248
|
function resolveGroupAllowFrom(config) {
|
|
4235
4249
|
return config.groupAllowFrom ?? [];
|
|
4236
4250
|
}
|
|
4251
|
+
var DEFAULT_INBOUND_MEDIA_DIR = join(homedir(), ".openclaw", "media", "wecom-app", "inbound");
|
|
4237
4252
|
var DEFAULT_INBOUND_MEDIA_MAX_BYTES = 10 * 1024 * 1024;
|
|
4253
|
+
var DEFAULT_INBOUND_MEDIA_KEEP_DAYS = 7;
|
|
4238
4254
|
function resolveInboundMediaEnabled(config) {
|
|
4239
4255
|
if (typeof config.inboundMedia?.enabled === "boolean") return config.inboundMedia.enabled;
|
|
4240
4256
|
return true;
|
|
4241
4257
|
}
|
|
4258
|
+
function resolveInboundMediaDir(config) {
|
|
4259
|
+
return (config.inboundMedia?.dir ?? "").trim() || DEFAULT_INBOUND_MEDIA_DIR;
|
|
4260
|
+
}
|
|
4242
4261
|
function resolveInboundMediaMaxBytes(config) {
|
|
4243
4262
|
const v = config.inboundMedia?.maxBytes;
|
|
4244
4263
|
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : DEFAULT_INBOUND_MEDIA_MAX_BYTES;
|
|
4245
4264
|
}
|
|
4265
|
+
function resolveInboundMediaKeepDays(config) {
|
|
4266
|
+
const v = config.inboundMedia?.keepDays;
|
|
4267
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : DEFAULT_INBOUND_MEDIA_KEEP_DAYS;
|
|
4268
|
+
}
|
|
4246
4269
|
|
|
4247
4270
|
// ../../packages/shared/src/logger/logger.ts
|
|
4248
4271
|
function createLogger(prefix, opts) {
|
|
@@ -4449,6 +4472,83 @@ function encryptWecomAppPlaintext(params) {
|
|
|
4449
4472
|
return encrypted.toString("base64");
|
|
4450
4473
|
}
|
|
4451
4474
|
var DOWNLOAD_TIMEOUT = 12e4;
|
|
4475
|
+
function formatDateDir(d = /* @__PURE__ */ new Date()) {
|
|
4476
|
+
const yyyy = d.getFullYear();
|
|
4477
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
4478
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
4479
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
4480
|
+
}
|
|
4481
|
+
function isProbablyInWecomTmpDir(p) {
|
|
4482
|
+
try {
|
|
4483
|
+
const base = join(tmpdir(), "wecom-app-media");
|
|
4484
|
+
const norm = (s) => s.replace(/\\/g, "/").toLowerCase();
|
|
4485
|
+
return norm(p).includes(norm(base));
|
|
4486
|
+
} catch {
|
|
4487
|
+
return false;
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
async function finalizeInboundMedia(account, filePath) {
|
|
4491
|
+
const p = String(filePath ?? "").trim();
|
|
4492
|
+
if (!p) return p;
|
|
4493
|
+
if (!isProbablyInWecomTmpDir(p)) return p;
|
|
4494
|
+
const baseDir = resolveInboundMediaDir(account.config ?? {});
|
|
4495
|
+
const datedDir = join(baseDir, formatDateDir());
|
|
4496
|
+
await mkdir(datedDir, { recursive: true });
|
|
4497
|
+
const name = basename(p);
|
|
4498
|
+
const dest = join(datedDir, name);
|
|
4499
|
+
try {
|
|
4500
|
+
await rename(p, dest);
|
|
4501
|
+
return dest;
|
|
4502
|
+
} catch {
|
|
4503
|
+
try {
|
|
4504
|
+
await unlink(p);
|
|
4505
|
+
} catch {
|
|
4506
|
+
}
|
|
4507
|
+
return p;
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
async function pruneInboundMediaDir(account) {
|
|
4511
|
+
const baseDir = resolveInboundMediaDir(account.config ?? {});
|
|
4512
|
+
const keepDays = resolveInboundMediaKeepDays(account.config ?? {});
|
|
4513
|
+
if (keepDays < 0) return;
|
|
4514
|
+
const now = Date.now();
|
|
4515
|
+
const cutoff = now - keepDays * 24 * 60 * 60 * 1e3;
|
|
4516
|
+
let entries;
|
|
4517
|
+
try {
|
|
4518
|
+
entries = await readdir(baseDir);
|
|
4519
|
+
} catch {
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
for (const entry of entries) {
|
|
4523
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry)) continue;
|
|
4524
|
+
const dirPath = join(baseDir, entry);
|
|
4525
|
+
let st;
|
|
4526
|
+
try {
|
|
4527
|
+
st = await stat(dirPath);
|
|
4528
|
+
} catch {
|
|
4529
|
+
continue;
|
|
4530
|
+
}
|
|
4531
|
+
if (!st.isDirectory()) continue;
|
|
4532
|
+
const dirTime = st.mtimeMs || st.ctimeMs || 0;
|
|
4533
|
+
if (dirTime >= cutoff) continue;
|
|
4534
|
+
let files = [];
|
|
4535
|
+
try {
|
|
4536
|
+
files = await readdir(dirPath);
|
|
4537
|
+
} catch {
|
|
4538
|
+
continue;
|
|
4539
|
+
}
|
|
4540
|
+
for (const f of files) {
|
|
4541
|
+
const fp = join(dirPath, f);
|
|
4542
|
+
try {
|
|
4543
|
+
const fst = await stat(fp);
|
|
4544
|
+
if (fst.isFile() && (fst.mtimeMs || fst.ctimeMs || 0) < cutoff) {
|
|
4545
|
+
await unlink(fp);
|
|
4546
|
+
}
|
|
4547
|
+
} catch {
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4452
4552
|
var FileSizeLimitError2 = class _FileSizeLimitError extends Error {
|
|
4453
4553
|
actualSize;
|
|
4454
4554
|
limitSize;
|
|
@@ -4584,12 +4684,6 @@ function parseContentDispositionFilename(headerValue) {
|
|
|
4584
4684
|
if (m2?.[1]) return m2[1].trim().replace(/^"|"$/g, "");
|
|
4585
4685
|
return void 0;
|
|
4586
4686
|
}
|
|
4587
|
-
async function cleanupFile(filePath) {
|
|
4588
|
-
try {
|
|
4589
|
-
await unlink(filePath);
|
|
4590
|
-
} catch {
|
|
4591
|
-
}
|
|
4592
|
-
}
|
|
4593
4687
|
function getWecomTempDir() {
|
|
4594
4688
|
return join(tmpdir(), "wecom-app-media");
|
|
4595
4689
|
}
|
|
@@ -5036,6 +5130,10 @@ async function downloadVoice(voiceUrl) {
|
|
|
5036
5130
|
async function downloadAndSendVoice(account, target, voiceUrl) {
|
|
5037
5131
|
try {
|
|
5038
5132
|
console.log(`[wecom-app] Downloading voice from: ${voiceUrl}`);
|
|
5133
|
+
const voiceExt = (voiceUrl.split("?")[0].match(/\.([^.]+)$/)?.[1] || "").toLowerCase();
|
|
5134
|
+
if (voiceExt === "wav") {
|
|
5135
|
+
console.warn(`[wecom-app] Voice format is .wav; WeCom usually expects .amr/.speex. Consider converting to .amr before sending.`);
|
|
5136
|
+
}
|
|
5039
5137
|
const { buffer: voiceBuffer, contentType } = await downloadVoice(voiceUrl);
|
|
5040
5138
|
console.log(`[wecom-app] Voice downloaded, size: ${voiceBuffer.length} bytes, contentType: ${contentType || "unknown"}`);
|
|
5041
5139
|
const extMatch = voiceUrl.match(/\.([^.]+)$/);
|
|
@@ -5050,10 +5148,13 @@ async function downloadAndSendVoice(account, target, voiceUrl) {
|
|
|
5050
5148
|
return result;
|
|
5051
5149
|
} catch (err) {
|
|
5052
5150
|
console.error(`[wecom-app] downloadAndSendVoice error:`, err);
|
|
5151
|
+
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
5152
|
+
const voiceExt = (voiceUrl.split("?")[0].match(/\.([^.]+)$/)?.[1] || "").toLowerCase();
|
|
5153
|
+
const hint = voiceExt === "wav" ? "WeCom voice usually requires .amr/.speex. Your file is .wav; please convert to .amr and retry. (e.g., ffmpeg -i in.wav -ar 8000 -ac 1 -c:a amr_nb out.amr)" : "";
|
|
5053
5154
|
return {
|
|
5054
5155
|
ok: false,
|
|
5055
5156
|
errcode: -1,
|
|
5056
|
-
errmsg:
|
|
5157
|
+
errmsg: hint ? `${rawMsg} | hint: ${hint}` : rawMsg
|
|
5057
5158
|
};
|
|
5058
5159
|
}
|
|
5059
5160
|
}
|
|
@@ -5166,9 +5267,26 @@ async function downloadAndSendFile(account, target, fileUrl) {
|
|
|
5166
5267
|
console.log(`[wecom-app] Downloading file from: ${fileUrl}`);
|
|
5167
5268
|
const { buffer: fileBuffer, contentType } = await downloadFile(fileUrl);
|
|
5168
5269
|
console.log(`[wecom-app] File downloaded, size: ${fileBuffer.length} bytes, contentType: ${contentType || "unknown"}`);
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5270
|
+
let filename = "file.bin";
|
|
5271
|
+
try {
|
|
5272
|
+
if (!fileUrl.startsWith("http://") && !fileUrl.startsWith("https://")) {
|
|
5273
|
+
const path = await import('path');
|
|
5274
|
+
const base = path.basename(fileUrl);
|
|
5275
|
+
if (base && base !== "." && base !== "/") {
|
|
5276
|
+
filename = base;
|
|
5277
|
+
}
|
|
5278
|
+
} else {
|
|
5279
|
+
const u = new URL(fileUrl);
|
|
5280
|
+
const base = u.pathname.split("/").filter(Boolean).pop();
|
|
5281
|
+
if (base) filename = base;
|
|
5282
|
+
}
|
|
5283
|
+
} catch {
|
|
5284
|
+
}
|
|
5285
|
+
if (!/\.[A-Za-z0-9]{1,10}$/.test(filename)) {
|
|
5286
|
+
const extMatch = fileUrl.split("?")[0].match(/\.([^.]+)$/);
|
|
5287
|
+
const ext = extMatch ? `.${extMatch[1]}` : ".bin";
|
|
5288
|
+
filename = `file${ext}`;
|
|
5289
|
+
}
|
|
5172
5290
|
console.log(`[wecom-app] Uploading file to WeCom media API, filename: ${filename}`);
|
|
5173
5291
|
const mediaId = await uploadMedia(account, fileBuffer, filename, contentType, "file");
|
|
5174
5292
|
console.log(`[wecom-app] File uploaded, media_id: ${mediaId}`);
|
|
@@ -5242,8 +5360,9 @@ async function enrichInboundContentWithMedia(params) {
|
|
|
5242
5360
|
text,
|
|
5243
5361
|
mediaPaths,
|
|
5244
5362
|
cleanup: async () => {
|
|
5245
|
-
|
|
5246
|
-
await
|
|
5363
|
+
try {
|
|
5364
|
+
await pruneInboundMediaDir(account);
|
|
5365
|
+
} catch {
|
|
5247
5366
|
}
|
|
5248
5367
|
}
|
|
5249
5368
|
});
|
|
@@ -5256,8 +5375,9 @@ async function enrichInboundContentWithMedia(params) {
|
|
|
5256
5375
|
if (mediaId) {
|
|
5257
5376
|
const saved = await downloadWecomMediaToFile(account, mediaId, { maxBytes, prefix: "img" });
|
|
5258
5377
|
if (saved.ok && saved.path) {
|
|
5259
|
-
|
|
5260
|
-
|
|
5378
|
+
const finalPath = await finalizeInboundMedia(account, saved.path);
|
|
5379
|
+
mediaPaths.push(finalPath);
|
|
5380
|
+
return makeResult(`[image] saved:${finalPath}`);
|
|
5261
5381
|
}
|
|
5262
5382
|
return makeResult(`[image] (save failed) ${saved.error ?? ""}`.trim());
|
|
5263
5383
|
}
|
|
@@ -5266,8 +5386,9 @@ async function enrichInboundContentWithMedia(params) {
|
|
|
5266
5386
|
try {
|
|
5267
5387
|
const saved = await downloadWecomMediaToFile(account, url, { maxBytes, prefix: "img" });
|
|
5268
5388
|
if (saved.ok && saved.path) {
|
|
5269
|
-
|
|
5270
|
-
|
|
5389
|
+
const finalPath = await finalizeInboundMedia(account, saved.path);
|
|
5390
|
+
mediaPaths.push(finalPath);
|
|
5391
|
+
return makeResult(`[image] saved:${finalPath}`);
|
|
5271
5392
|
}
|
|
5272
5393
|
} catch {
|
|
5273
5394
|
}
|
|
@@ -5284,8 +5405,9 @@ async function enrichInboundContentWithMedia(params) {
|
|
|
5284
5405
|
if (mediaId) {
|
|
5285
5406
|
const saved = await downloadWecomMediaToFile(account, mediaId, { maxBytes, prefix: "file" });
|
|
5286
5407
|
if (saved.ok && saved.path) {
|
|
5287
|
-
|
|
5288
|
-
|
|
5408
|
+
const finalPath = await finalizeInboundMedia(account, saved.path);
|
|
5409
|
+
mediaPaths.push(finalPath);
|
|
5410
|
+
return makeResult(`[file] saved:${finalPath}`);
|
|
5289
5411
|
}
|
|
5290
5412
|
return makeResult(`[file] (save failed) ${saved.error ?? ""}`.trim());
|
|
5291
5413
|
}
|
|
@@ -5302,12 +5424,13 @@ async function enrichInboundContentWithMedia(params) {
|
|
|
5302
5424
|
if (mediaId) {
|
|
5303
5425
|
const saved = await downloadWecomMediaToFile(account, mediaId, { maxBytes, prefix: "voice" });
|
|
5304
5426
|
if (saved.ok && saved.path) {
|
|
5305
|
-
|
|
5427
|
+
const finalPath = await finalizeInboundMedia(account, saved.path);
|
|
5428
|
+
mediaPaths.push(finalPath);
|
|
5306
5429
|
if (recognition) {
|
|
5307
|
-
return makeResult(`[voice] saved:${
|
|
5430
|
+
return makeResult(`[voice] saved:${finalPath}
|
|
5308
5431
|
[recognition] ${recognition}`);
|
|
5309
5432
|
}
|
|
5310
|
-
return makeResult(`[voice] saved:${
|
|
5433
|
+
return makeResult(`[voice] saved:${finalPath}`);
|
|
5311
5434
|
}
|
|
5312
5435
|
if (recognition) {
|
|
5313
5436
|
return makeResult(`[voice] (save failed) ${saved.error ?? ""}
|
|
@@ -5350,8 +5473,9 @@ async function enrichInboundContentWithMedia(params) {
|
|
|
5350
5473
|
try {
|
|
5351
5474
|
const saved = await downloadWecomMediaToFile(account, mediaId, { maxBytes, prefix: "img" });
|
|
5352
5475
|
if (saved.ok && saved.path) {
|
|
5353
|
-
|
|
5354
|
-
|
|
5476
|
+
const finalPath = await finalizeInboundMedia(account, saved.path);
|
|
5477
|
+
mediaPaths.push(finalPath);
|
|
5478
|
+
parts.push(`[image] saved:${finalPath}`);
|
|
5355
5479
|
} else {
|
|
5356
5480
|
const url = String(typed.image?.url ?? "").trim();
|
|
5357
5481
|
parts.push(url ? `[image] ${url}` : "[image]");
|
|
@@ -6069,14 +6193,40 @@ async function handleWecomAppWebhookRequest(req, res) {
|
|
|
6069
6193
|
);
|
|
6070
6194
|
return true;
|
|
6071
6195
|
}
|
|
6196
|
+
async function hasFfmpeg() {
|
|
6197
|
+
return new Promise((resolve) => {
|
|
6198
|
+
const p = spawn("ffmpeg", ["-version"], { stdio: "ignore" });
|
|
6199
|
+
p.on("error", () => resolve(false));
|
|
6200
|
+
p.on("exit", (code) => resolve(code === 0));
|
|
6201
|
+
});
|
|
6202
|
+
}
|
|
6203
|
+
async function transcodeToAmr(params) {
|
|
6204
|
+
const args = ["-y", "-i", params.inputPath, "-ar", "8000", "-ac", "1", "-c:a", "amr_nb", params.outputPath];
|
|
6205
|
+
await new Promise((resolve, reject) => {
|
|
6206
|
+
const p = spawn("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
6207
|
+
let err = "";
|
|
6208
|
+
p.stderr?.on("data", (d) => err += String(d));
|
|
6209
|
+
p.on("error", (e) => reject(e));
|
|
6210
|
+
p.on("exit", (code) => {
|
|
6211
|
+
if (code === 0) return resolve();
|
|
6212
|
+
reject(new Error(`ffmpeg transcode failed (code=${code}): ${err.slice(0, 2e3)}`));
|
|
6213
|
+
});
|
|
6214
|
+
});
|
|
6215
|
+
}
|
|
6072
6216
|
|
|
6073
6217
|
// src/channel.ts
|
|
6074
6218
|
function detectMediaType2(filePath, mimeType) {
|
|
6075
6219
|
if (mimeType) {
|
|
6076
6220
|
const mime = mimeType.split(";")[0].trim().toLowerCase();
|
|
6221
|
+
if (mime.includes("svg")) {
|
|
6222
|
+
return "file";
|
|
6223
|
+
}
|
|
6077
6224
|
if (mime.startsWith("image/")) {
|
|
6078
6225
|
return "image";
|
|
6079
6226
|
}
|
|
6227
|
+
if (mime === "audio/wav" || mime === "audio/x-wav") {
|
|
6228
|
+
return "file";
|
|
6229
|
+
}
|
|
6080
6230
|
if (mime.startsWith("audio/") || mime === "audio/amr") {
|
|
6081
6231
|
return "voice";
|
|
6082
6232
|
}
|
|
@@ -6089,10 +6239,16 @@ function detectMediaType2(filePath, mimeType) {
|
|
|
6089
6239
|
if (imageExts.includes(ext)) {
|
|
6090
6240
|
return "image";
|
|
6091
6241
|
}
|
|
6092
|
-
|
|
6242
|
+
if (ext === "svg") {
|
|
6243
|
+
return "file";
|
|
6244
|
+
}
|
|
6245
|
+
const voiceExts = ["amr", "speex", "mp3"];
|
|
6093
6246
|
if (voiceExts.includes(ext)) {
|
|
6094
6247
|
return "voice";
|
|
6095
6248
|
}
|
|
6249
|
+
if (ext === "wav") {
|
|
6250
|
+
return "file";
|
|
6251
|
+
}
|
|
6096
6252
|
return "file";
|
|
6097
6253
|
}
|
|
6098
6254
|
var meta = {
|
|
@@ -6383,8 +6539,53 @@ var wecomAppPlugin = {
|
|
|
6383
6539
|
result = await downloadAndSendImage(account, target, params.mediaUrl);
|
|
6384
6540
|
} else if (mediaType === "voice") {
|
|
6385
6541
|
console.log(`[wecom-app] Routing to downloadAndSendVoice`);
|
|
6386
|
-
|
|
6542
|
+
const voiceUrl = params.mediaUrl;
|
|
6543
|
+
const ext = (voiceUrl.split("?")[0].match(/\.([^.]+)$/)?.[1] || "").toLowerCase();
|
|
6544
|
+
const likelyUnsupported = ext === "wav" || ext === "mp3";
|
|
6545
|
+
const transcodeEnabled = Boolean(account.config.voiceTranscode?.enabled);
|
|
6546
|
+
if (likelyUnsupported && transcodeEnabled) {
|
|
6547
|
+
const can = await hasFfmpeg();
|
|
6548
|
+
if (can) {
|
|
6549
|
+
try {
|
|
6550
|
+
if (!voiceUrl.startsWith("http://") && !voiceUrl.startsWith("https://")) {
|
|
6551
|
+
const os = await import('os');
|
|
6552
|
+
const path = await import('path');
|
|
6553
|
+
const fs = await import('fs');
|
|
6554
|
+
const out = path.join(os.tmpdir(), `wecom-app-voice-${Date.now()}.amr`);
|
|
6555
|
+
console.log(`[wecom-app] voiceTranscode: ffmpeg available, transcoding ${voiceUrl} -> ${out}`);
|
|
6556
|
+
await transcodeToAmr({ inputPath: voiceUrl, outputPath: out });
|
|
6557
|
+
result = await downloadAndSendVoice(account, target, out);
|
|
6558
|
+
try {
|
|
6559
|
+
await fs.promises.unlink(out);
|
|
6560
|
+
} catch {
|
|
6561
|
+
}
|
|
6562
|
+
} else {
|
|
6563
|
+
console.warn(`[wecom-app] voiceTranscode enabled but mediaUrl is remote; fallback to file send (download once is not implemented yet)`);
|
|
6564
|
+
result = await downloadAndSendFile(account, target, voiceUrl);
|
|
6565
|
+
}
|
|
6566
|
+
} catch (e) {
|
|
6567
|
+
console.warn(`[wecom-app] voiceTranscode failed; fallback to file send:`, e);
|
|
6568
|
+
result = await downloadAndSendFile(account, target, voiceUrl);
|
|
6569
|
+
}
|
|
6570
|
+
} else {
|
|
6571
|
+
console.warn(`[wecom-app] voiceTranscode enabled but ffmpeg not found; fallback to file send`);
|
|
6572
|
+
result = await downloadAndSendFile(account, target, voiceUrl);
|
|
6573
|
+
}
|
|
6574
|
+
} else if (likelyUnsupported) {
|
|
6575
|
+
console.log(`[wecom-app] Voice format .${ext} likely unsupported; fallback to file send`);
|
|
6576
|
+
result = await downloadAndSendFile(account, target, voiceUrl);
|
|
6577
|
+
} else {
|
|
6578
|
+
result = await downloadAndSendVoice(account, target, voiceUrl);
|
|
6579
|
+
}
|
|
6387
6580
|
} else {
|
|
6581
|
+
if (params.text?.trim()) {
|
|
6582
|
+
try {
|
|
6583
|
+
console.log(`[wecom-app] Sending caption text before file: ${params.text}`);
|
|
6584
|
+
await sendWecomAppMessage(account, target, params.text);
|
|
6585
|
+
} catch (err) {
|
|
6586
|
+
console.warn(`[wecom-app] Failed to send caption before file:`, err);
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6388
6589
|
console.log(`[wecom-app] Routing to downloadAndSendFile`);
|
|
6389
6590
|
result = await downloadAndSendFile(account, target, params.mediaUrl);
|
|
6390
6591
|
}
|