@agent-wechat/agent-wechat 0.13.0 → 0.15.0
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.js +182 -94
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1127,6 +1127,9 @@ function getWeChatRuntime() {
|
|
|
1127
1127
|
return runtime;
|
|
1128
1128
|
}
|
|
1129
1129
|
|
|
1130
|
+
// src/monitor.ts
|
|
1131
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1132
|
+
|
|
1130
1133
|
// ../shared/dist/client.js
|
|
1131
1134
|
function normalizeUrl(base) {
|
|
1132
1135
|
const url = base.startsWith("http") ? base : `http://${base}`;
|
|
@@ -1230,6 +1233,23 @@ var WeChatClient = class {
|
|
|
1230
1233
|
async sendMessage(params) {
|
|
1231
1234
|
return this.post("/api/messages/send", params);
|
|
1232
1235
|
}
|
|
1236
|
+
async createVoiceJob(chatId, audio, idempotencyKey) {
|
|
1237
|
+
const form = new FormData();
|
|
1238
|
+
form.set("chatId", chatId);
|
|
1239
|
+
form.set("audio", new Blob([new Uint8Array(audio)]), "voice-audio");
|
|
1240
|
+
const headers = { ...this.headers, "Idempotency-Key": idempotencyKey };
|
|
1241
|
+
delete headers["Content-Type"];
|
|
1242
|
+
const res = await fetch(`${this.base}/api/messages/voice`, { method: "POST", headers, body: form });
|
|
1243
|
+
if (!res.ok)
|
|
1244
|
+
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
|
|
1245
|
+
return res.json();
|
|
1246
|
+
}
|
|
1247
|
+
async getVoiceJob(jobId) {
|
|
1248
|
+
return this.get(`/api/messages/voice/${encodeURIComponent(jobId)}`);
|
|
1249
|
+
}
|
|
1250
|
+
async cancelVoiceJob(jobId) {
|
|
1251
|
+
return this.post(`/api/messages/voice/${encodeURIComponent(jobId)}/cancel`);
|
|
1252
|
+
}
|
|
1233
1253
|
// ---- Debug ----
|
|
1234
1254
|
async screenshot() {
|
|
1235
1255
|
return this.get("/api/debug/screenshot");
|
|
@@ -5561,11 +5581,25 @@ ${reference}` : reference;
|
|
|
5561
5581
|
function safeDisplayName(name) {
|
|
5562
5582
|
return name.replace(/[\r\n\0]/g, " ").trim() || "attachment";
|
|
5563
5583
|
}
|
|
5584
|
+
function attachmentFallbackFilename(kind, localId, originalBody = "") {
|
|
5585
|
+
if (kind === "file" && originalBody && !originalBody.trimStart().startsWith("<")) {
|
|
5586
|
+
return safeDisplayName(originalBody);
|
|
5587
|
+
}
|
|
5588
|
+
const extension = kind === "image" ? "jpg" : kind === "audio" ? "mp3" : kind === "video" ? "mp4" : "bin";
|
|
5589
|
+
return `message-${localId}.${extension}`;
|
|
5590
|
+
}
|
|
5591
|
+
function attachmentSourceBody(originalBody, kind) {
|
|
5592
|
+
if (!kind || kind === "file") return originalBody;
|
|
5593
|
+
return originalBody.trimStart().startsWith("<") ? "" : originalBody;
|
|
5594
|
+
}
|
|
5564
5595
|
function renderAttachmentBody(originalBody, attachment, exposeNonVisualPath = true) {
|
|
5565
5596
|
if (!attachment) return originalBody;
|
|
5566
5597
|
const name = safeDisplayName(attachment.filename);
|
|
5567
5598
|
if (attachment.status !== "ready" || !attachment.path) {
|
|
5568
5599
|
const label = attachment.kind === "file" ? "File" : attachment.kind === "image" ? "Image" : attachment.kind === "audio" ? "Voice message" : "Video";
|
|
5600
|
+
if (attachment.status === "pending") {
|
|
5601
|
+
return appendReference(originalBody, `[${label} pending: ${name}]`);
|
|
5602
|
+
}
|
|
5569
5603
|
return appendReference(originalBody, `[${label} unavailable: ${name} (${attachment.status})]`);
|
|
5570
5604
|
}
|
|
5571
5605
|
if (attachment.kind === "file") {
|
|
@@ -5765,6 +5799,96 @@ async function listMessageWindow(client, chatId, previousLastSeen, firstPoll, un
|
|
|
5765
5799
|
return messages;
|
|
5766
5800
|
}
|
|
5767
5801
|
|
|
5802
|
+
// src/outbound-media.ts
|
|
5803
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5804
|
+
import { readFile } from "node:fs/promises";
|
|
5805
|
+
import { basename, extname } from "node:path";
|
|
5806
|
+
var EXT_MIME = {
|
|
5807
|
+
png: "image/png",
|
|
5808
|
+
jpg: "image/jpeg",
|
|
5809
|
+
jpeg: "image/jpeg",
|
|
5810
|
+
gif: "image/gif",
|
|
5811
|
+
webp: "image/webp",
|
|
5812
|
+
mp3: "audio/mpeg",
|
|
5813
|
+
wav: "audio/wav",
|
|
5814
|
+
ogg: "audio/ogg",
|
|
5815
|
+
opus: "audio/ogg",
|
|
5816
|
+
m4a: "audio/mp4",
|
|
5817
|
+
flac: "audio/flac",
|
|
5818
|
+
aac: "audio/aac"
|
|
5819
|
+
};
|
|
5820
|
+
function isTerminal(job) {
|
|
5821
|
+
return ["completed", "failed", "cancelled", "needs_review"].includes(job.status);
|
|
5822
|
+
}
|
|
5823
|
+
async function waitForVoiceJob(client, jobId) {
|
|
5824
|
+
const deadline = Date.now() + 25 * 6e4;
|
|
5825
|
+
let lastStatusAt = Date.now();
|
|
5826
|
+
while (Date.now() < deadline) {
|
|
5827
|
+
let job;
|
|
5828
|
+
try {
|
|
5829
|
+
job = await client.getVoiceJob(jobId);
|
|
5830
|
+
lastStatusAt = Date.now();
|
|
5831
|
+
} catch (error) {
|
|
5832
|
+
if (Date.now() - lastStatusAt > 3e4) {
|
|
5833
|
+
throw new Error(`Voice job ${jobId} status is unavailable: ${String(error)}. Inspect the job before retrying.`);
|
|
5834
|
+
}
|
|
5835
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
5836
|
+
continue;
|
|
5837
|
+
}
|
|
5838
|
+
if (isTerminal(job)) {
|
|
5839
|
+
if (job.status === "completed") return job;
|
|
5840
|
+
const sent = job.chunks.filter((chunk) => chunk.status === "verified").length;
|
|
5841
|
+
throw new Error(`Voice job ${jobId} ${job.status}: ${sent}/${job.chunks.length} notes verified. ${job.error ?? ""} Do not resend the whole recording.`);
|
|
5842
|
+
}
|
|
5843
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
5844
|
+
}
|
|
5845
|
+
throw new Error(`Voice job ${jobId} exceeded the 25-minute wait. Inspect the job before retrying.`);
|
|
5846
|
+
}
|
|
5847
|
+
async function sendWeChatMedia(cfg, to, text, mediaUrl, audioAsVoice = false, idempotencyKey) {
|
|
5848
|
+
const account = resolveWeChatAccount(cfg);
|
|
5849
|
+
if (!account?.serverUrl) throw new Error("No serverUrl configured");
|
|
5850
|
+
const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
|
|
5851
|
+
if (!mediaUrl) {
|
|
5852
|
+
if (audioAsVoice) throw new Error("Voice sending requires an audio attachment");
|
|
5853
|
+
const result2 = await client.sendMessage({ chatId: to, text: text || void 0 });
|
|
5854
|
+
if (!result2.success) throw new Error(result2.error ?? "Send failed");
|
|
5855
|
+
return `agent-wechat:${to}:${Date.now()}`;
|
|
5856
|
+
}
|
|
5857
|
+
let bytes;
|
|
5858
|
+
let mimeType;
|
|
5859
|
+
let filename;
|
|
5860
|
+
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
5861
|
+
const res = await fetch(mediaUrl);
|
|
5862
|
+
if (!res.ok) throw new Error(`Media fetch failed: ${res.status}`);
|
|
5863
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
5864
|
+
mimeType = res.headers.get("content-type")?.split(";")[0] ?? "application/octet-stream";
|
|
5865
|
+
filename = basename(new URL(mediaUrl).pathname) || "file";
|
|
5866
|
+
if (mimeType === "application/octet-stream") {
|
|
5867
|
+
mimeType = EXT_MIME[extname(filename).toLowerCase().slice(1)] ?? mimeType;
|
|
5868
|
+
}
|
|
5869
|
+
} else {
|
|
5870
|
+
const localPath = mediaUrl.startsWith("file://") ? new URL(mediaUrl) : mediaUrl;
|
|
5871
|
+
const pathText = localPath instanceof URL ? localPath.pathname : localPath;
|
|
5872
|
+
bytes = new Uint8Array(await readFile(localPath));
|
|
5873
|
+
filename = basename(pathText);
|
|
5874
|
+
mimeType = EXT_MIME[extname(filename).toLowerCase().slice(1)] ?? "application/octet-stream";
|
|
5875
|
+
}
|
|
5876
|
+
if (audioAsVoice) {
|
|
5877
|
+
if (!mimeType.startsWith("audio/")) throw new Error(`Voice media must be audio, got ${mimeType}`);
|
|
5878
|
+
const job = await client.createVoiceJob(to, bytes, idempotencyKey ?? randomUUID2());
|
|
5879
|
+
await waitForVoiceJob(client, job.jobId);
|
|
5880
|
+
if (text) {
|
|
5881
|
+
const caption = await client.sendMessage({ chatId: to, text });
|
|
5882
|
+
if (!caption.success) throw new Error(`Voice job ${job.jobId} completed, but caption failed: ${caption.error ?? "unknown error"}`);
|
|
5883
|
+
}
|
|
5884
|
+
return `agent-wechat:voice:${job.jobId}`;
|
|
5885
|
+
}
|
|
5886
|
+
const base64 = Buffer.from(bytes).toString("base64");
|
|
5887
|
+
const result = mimeType.startsWith("image/") ? await client.sendMessage({ chatId: to, text: text || void 0, image: { data: base64, mimeType } }) : await client.sendMessage({ chatId: to, text: text || void 0, file: { data: base64, filename } });
|
|
5888
|
+
if (!result.success) throw new Error(result.error ?? "Send media failed");
|
|
5889
|
+
return `agent-wechat:${to}:${Date.now()}`;
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5768
5892
|
// src/access-control.ts
|
|
5769
5893
|
import {
|
|
5770
5894
|
buildChannelKeyCandidates,
|
|
@@ -5975,6 +6099,7 @@ function resolveWeChatMentionGate(params) {
|
|
|
5975
6099
|
// src/monitor.ts
|
|
5976
6100
|
var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
|
|
5977
6101
|
var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
|
|
6102
|
+
var voiceTranscriptCache = /* @__PURE__ */ new Map();
|
|
5978
6103
|
function isProcessedMessage(value) {
|
|
5979
6104
|
if (!value || typeof value !== "object") return false;
|
|
5980
6105
|
const message = value;
|
|
@@ -6024,17 +6149,18 @@ async function retrieveAttachment(client, chatId, msg, liveAccount, log, maxAtte
|
|
|
6024
6149
|
const baseType = msg.type & 2147483647;
|
|
6025
6150
|
const expectedKind = attachmentKindForMessageType(baseType);
|
|
6026
6151
|
if (!expectedKind) return void 0;
|
|
6152
|
+
const fallbackFilename = attachmentFallbackFilename(expectedKind, msg.localId, msg.content);
|
|
6027
6153
|
try {
|
|
6028
6154
|
const result = await pollMedia(client, chatId, msg.localId, log, maxAttempts);
|
|
6029
6155
|
if (!result) {
|
|
6030
|
-
return { kind: expectedKind, status: "unavailable", filename:
|
|
6156
|
+
return { kind: expectedKind, status: "unavailable", filename: fallbackFilename };
|
|
6031
6157
|
}
|
|
6032
6158
|
if (result.type === "unsupported") {
|
|
6033
6159
|
if (baseType === 49) return void 0;
|
|
6034
|
-
return { kind: expectedKind, status: "unsupported", filename:
|
|
6160
|
+
return { kind: expectedKind, status: "unsupported", filename: fallbackFilename };
|
|
6035
6161
|
}
|
|
6036
6162
|
const kind = attachmentKindForResult(result, expectedKind);
|
|
6037
|
-
const filename = result.filename ||
|
|
6163
|
+
const filename = result.filename || fallbackFilename;
|
|
6038
6164
|
if (!result.data) {
|
|
6039
6165
|
return { kind, status: result.type === "pending" ? "pending" : "unavailable", filename };
|
|
6040
6166
|
}
|
|
@@ -6058,7 +6184,7 @@ async function retrieveAttachment(client, chatId, msg, liveAccount, log, maxAtte
|
|
|
6058
6184
|
return {
|
|
6059
6185
|
kind: expectedKind,
|
|
6060
6186
|
status,
|
|
6061
|
-
filename:
|
|
6187
|
+
filename: fallbackFilename,
|
|
6062
6188
|
error: error instanceof Error ? error.message : String(error)
|
|
6063
6189
|
};
|
|
6064
6190
|
}
|
|
@@ -6228,13 +6354,14 @@ async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, lo
|
|
|
6228
6354
|
return null;
|
|
6229
6355
|
}
|
|
6230
6356
|
const baseType = msg.type & 2147483647;
|
|
6357
|
+
const messageAttachmentKind = attachmentKindForMessageType(baseType);
|
|
6231
6358
|
let attachment;
|
|
6232
|
-
if (
|
|
6359
|
+
if (messageAttachmentKind) {
|
|
6233
6360
|
log?.info?.(`[wechat:${liveAccount.accountId}] Checking media for msg ${msg.localId} (type ${baseType})`);
|
|
6234
6361
|
attachment = await retrieveAttachment(client, chatId, msg, liveAccount, log);
|
|
6235
6362
|
}
|
|
6236
6363
|
const timestamp = new Date(msg.timestamp).getTime();
|
|
6237
|
-
let sourceBody = msg.content || "";
|
|
6364
|
+
let sourceBody = attachmentSourceBody(msg.content || "", messageAttachmentKind);
|
|
6238
6365
|
if (msg.reply) {
|
|
6239
6366
|
const replySender = msg.reply.sender ?? "unknown sender";
|
|
6240
6367
|
const quotedBody = msg.reply.content.length > 50 ? msg.reply.content.slice(0, 50) + "..." : msg.reply.content;
|
|
@@ -6344,6 +6471,36 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6344
6471
|
return "ignored";
|
|
6345
6472
|
}
|
|
6346
6473
|
try {
|
|
6474
|
+
for (const item of segment) {
|
|
6475
|
+
const attachment = item.attachment;
|
|
6476
|
+
if (item.mediaPath || attachment?.kind !== "audio" || attachment.status !== "ready" || !attachment.path) continue;
|
|
6477
|
+
const cacheKey = `${liveAccount.accountId}:${chatId}:${item.msg.localId}:${attachment.path}`;
|
|
6478
|
+
let transcript = voiceTranscriptCache.get(cacheKey);
|
|
6479
|
+
if (!transcript) {
|
|
6480
|
+
try {
|
|
6481
|
+
const result = await core.mediaUnderstanding.transcribeAudioFile({
|
|
6482
|
+
filePath: attachment.path,
|
|
6483
|
+
cfg,
|
|
6484
|
+
mime: attachment.mime
|
|
6485
|
+
});
|
|
6486
|
+
transcript = result.text?.trim();
|
|
6487
|
+
if (transcript) {
|
|
6488
|
+
if (voiceTranscriptCache.size >= 500) voiceTranscriptCache.clear();
|
|
6489
|
+
voiceTranscriptCache.set(cacheKey, transcript);
|
|
6490
|
+
}
|
|
6491
|
+
} catch (error) {
|
|
6492
|
+
log?.error?.(`[wechat:${liveAccount.accountId}] Voice transcription failed for ${item.msg.localId}: ${String(error)}`);
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
if (transcript) {
|
|
6496
|
+
item.rawBody += `
|
|
6497
|
+
[Voice transcript: ${transcript.slice(0, 1e4)}]`;
|
|
6498
|
+
item.commandBody = normalizeWeChatCommandBody(item.rawBody, {
|
|
6499
|
+
isGroup: item.isGroup,
|
|
6500
|
+
wasMentioned: item.isMentioned
|
|
6501
|
+
});
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6347
6504
|
const route = core.channel.routing.resolveAgentRoute({
|
|
6348
6505
|
cfg,
|
|
6349
6506
|
channel: "agent-wechat",
|
|
@@ -6372,7 +6529,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6372
6529
|
timestamp,
|
|
6373
6530
|
previousTimestamp,
|
|
6374
6531
|
envelope: envelopeOptions,
|
|
6375
|
-
body: isGroup ? `${senderName}: ${rawBody}` : rawBody
|
|
6532
|
+
body: isGroup ? `${senderName}: ${lastMsg.rawBody}` : lastMsg.rawBody
|
|
6376
6533
|
});
|
|
6377
6534
|
} else {
|
|
6378
6535
|
const historyMessages = segment.slice(0, -1);
|
|
@@ -6392,7 +6549,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6392
6549
|
timestamp,
|
|
6393
6550
|
previousTimestamp,
|
|
6394
6551
|
envelope: envelopeOptions,
|
|
6395
|
-
body: isGroup ? `${senderName}: ${rawBody}` : rawBody
|
|
6552
|
+
body: isGroup ? `${senderName}: ${lastMsg.rawBody}` : lastMsg.rawBody
|
|
6396
6553
|
});
|
|
6397
6554
|
body = [
|
|
6398
6555
|
HISTORY_CONTEXT_MARKER,
|
|
@@ -6414,8 +6571,8 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6414
6571
|
}
|
|
6415
6572
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
6416
6573
|
Body: body,
|
|
6417
|
-
BodyForAgent: rawBody,
|
|
6418
|
-
RawBody: rawBody,
|
|
6574
|
+
BodyForAgent: lastMsg.rawBody,
|
|
6575
|
+
RawBody: lastMsg.rawBody,
|
|
6419
6576
|
CommandBody: commandBody,
|
|
6420
6577
|
InboundHistory: inboundHistory,
|
|
6421
6578
|
From: isGroup ? `wechat:group:${chatId}` : `wechat:${senderId}`,
|
|
@@ -6457,12 +6614,14 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6457
6614
|
accountId: liveAccount.accountId
|
|
6458
6615
|
});
|
|
6459
6616
|
let replyFailed = false;
|
|
6617
|
+
let deliverySequence = 0;
|
|
6460
6618
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
6461
6619
|
ctx: ctxPayload,
|
|
6462
6620
|
cfg,
|
|
6463
6621
|
dispatcherOptions: {
|
|
6464
6622
|
...prefixOptions,
|
|
6465
6623
|
deliver: async (payload) => {
|
|
6624
|
+
const deliveryIndex = deliverySequence++;
|
|
6466
6625
|
const mediaList = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
6467
6626
|
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
6468
6627
|
cfg,
|
|
@@ -6474,49 +6633,17 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6474
6633
|
tableMode
|
|
6475
6634
|
);
|
|
6476
6635
|
if (mediaList.length > 0) {
|
|
6477
|
-
for (
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
const pathmod = await import("path");
|
|
6481
|
-
let base64;
|
|
6482
|
-
let mimeType;
|
|
6483
|
-
let filename;
|
|
6484
|
-
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
6485
|
-
const res = await fetch(mediaUrl);
|
|
6486
|
-
const buffer = await res.arrayBuffer();
|
|
6487
|
-
base64 = Buffer.from(buffer).toString("base64");
|
|
6488
|
-
mimeType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
6489
|
-
const urlPath = new URL(mediaUrl).pathname;
|
|
6490
|
-
filename = pathmod.basename(urlPath) || "file";
|
|
6491
|
-
} else {
|
|
6492
|
-
const buf = await fsmod.readFile(mediaUrl);
|
|
6493
|
-
base64 = buf.toString("base64");
|
|
6494
|
-
filename = pathmod.basename(mediaUrl);
|
|
6495
|
-
const ext = pathmod.extname(mediaUrl).toLowerCase().replace(".", "");
|
|
6496
|
-
const extMime = {
|
|
6497
|
-
png: "image/png",
|
|
6498
|
-
jpg: "image/jpeg",
|
|
6499
|
-
jpeg: "image/jpeg",
|
|
6500
|
-
gif: "image/gif",
|
|
6501
|
-
webp: "image/webp"
|
|
6502
|
-
};
|
|
6503
|
-
mimeType = extMime[ext] ?? "application/octet-stream";
|
|
6504
|
-
}
|
|
6505
|
-
const isImage = mimeType.startsWith("image/");
|
|
6506
|
-
if (isImage) {
|
|
6507
|
-
await client.sendMessage({ chatId, image: { data: base64, mimeType } });
|
|
6508
|
-
} else {
|
|
6509
|
-
await client.sendMessage({ chatId, file: { data: base64, filename } });
|
|
6510
|
-
}
|
|
6511
|
-
} catch (err) {
|
|
6512
|
-
log?.error?.(`[wechat:${liveAccount.accountId}] Failed to send media: ${err}`);
|
|
6513
|
-
}
|
|
6636
|
+
for (let index = 0; index < mediaList.length; index++) {
|
|
6637
|
+
const retryKey = createHash3("sha256").update(`${liveAccount.accountId}\0${chatId}\0${msg.localId}\0${deliveryIndex}\0${index}`).digest("hex");
|
|
6638
|
+
await sendWeChatMedia(cfg, chatId, "", mediaList[index], payload.audioAsVoice === true, retryKey);
|
|
6514
6639
|
}
|
|
6515
6640
|
if (text) {
|
|
6516
|
-
await client.sendMessage({ chatId, text });
|
|
6641
|
+
const result = await client.sendMessage({ chatId, text });
|
|
6642
|
+
if (!result.success) throw new Error(result.error ?? "Text send failed");
|
|
6517
6643
|
}
|
|
6518
6644
|
} else if (text) {
|
|
6519
|
-
await client.sendMessage({ chatId, text });
|
|
6645
|
+
const result = await client.sendMessage({ chatId, text });
|
|
6646
|
+
if (!result.success) throw new Error(result.error ?? "Text send failed");
|
|
6520
6647
|
}
|
|
6521
6648
|
},
|
|
6522
6649
|
onError: (err, info) => {
|
|
@@ -7093,46 +7220,6 @@ async function sendWeChatText(cfg, to, text) {
|
|
|
7093
7220
|
if (!result.success) throw new Error(result.error ?? "Send failed");
|
|
7094
7221
|
return `agent-wechat:${to}:${Date.now()}`;
|
|
7095
7222
|
}
|
|
7096
|
-
async function sendWeChatMedia(cfg, to, text, mediaUrl) {
|
|
7097
|
-
const account = resolveWeChatAccount(cfg);
|
|
7098
|
-
if (!account?.serverUrl) throw new Error("No serverUrl configured");
|
|
7099
|
-
const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
|
|
7100
|
-
if (!mediaUrl) {
|
|
7101
|
-
const result2 = await client.sendMessage({ chatId: to, text: text || void 0 });
|
|
7102
|
-
if (!result2.success) throw new Error(result2.error ?? "Send failed");
|
|
7103
|
-
return `agent-wechat:${to}:${Date.now()}`;
|
|
7104
|
-
}
|
|
7105
|
-
const fsmod = await import("fs/promises");
|
|
7106
|
-
const pathmod = await import("path");
|
|
7107
|
-
let base64;
|
|
7108
|
-
let mimeType;
|
|
7109
|
-
let filename;
|
|
7110
|
-
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
7111
|
-
const res = await fetch(mediaUrl);
|
|
7112
|
-
const buffer = await res.arrayBuffer();
|
|
7113
|
-
base64 = Buffer.from(buffer).toString("base64");
|
|
7114
|
-
mimeType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
7115
|
-
const urlPath = new URL(mediaUrl).pathname;
|
|
7116
|
-
filename = pathmod.basename(urlPath) || "file";
|
|
7117
|
-
} else {
|
|
7118
|
-
const buf = await fsmod.readFile(mediaUrl);
|
|
7119
|
-
base64 = buf.toString("base64");
|
|
7120
|
-
filename = pathmod.basename(mediaUrl);
|
|
7121
|
-
const ext = pathmod.extname(mediaUrl).toLowerCase().replace(".", "");
|
|
7122
|
-
const extMime = {
|
|
7123
|
-
png: "image/png",
|
|
7124
|
-
jpg: "image/jpeg",
|
|
7125
|
-
jpeg: "image/jpeg",
|
|
7126
|
-
gif: "image/gif",
|
|
7127
|
-
webp: "image/webp"
|
|
7128
|
-
};
|
|
7129
|
-
mimeType = extMime[ext] ?? "application/octet-stream";
|
|
7130
|
-
}
|
|
7131
|
-
const isImage = mimeType.startsWith("image/");
|
|
7132
|
-
const result = isImage ? await client.sendMessage({ chatId: to, text: text || void 0, image: { data: base64, mimeType } }) : await client.sendMessage({ chatId: to, text: text || void 0, file: { data: base64, filename } });
|
|
7133
|
-
if (!result.success) throw new Error(result.error ?? "Send media failed");
|
|
7134
|
-
return `agent-wechat:${to}:${Date.now()}`;
|
|
7135
|
-
}
|
|
7136
7223
|
var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
7137
7224
|
id: "agent-wechat",
|
|
7138
7225
|
capabilities: {
|
|
@@ -7145,9 +7232,9 @@ var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
|
7145
7232
|
channel: "agent-wechat",
|
|
7146
7233
|
messageId: await sendWeChatText(cfg, to, text)
|
|
7147
7234
|
}),
|
|
7148
|
-
sendMedia: async ({ cfg, to, text, mediaUrl }) => ({
|
|
7235
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, audioAsVoice }) => ({
|
|
7149
7236
|
channel: "agent-wechat",
|
|
7150
|
-
messageId: await sendWeChatMedia(cfg, to, text, mediaUrl)
|
|
7237
|
+
messageId: await sendWeChatMedia(cfg, to, text, mediaUrl, audioAsVoice)
|
|
7151
7238
|
})
|
|
7152
7239
|
}
|
|
7153
7240
|
});
|
|
@@ -7168,7 +7255,8 @@ var wechatPlugin = {
|
|
|
7168
7255
|
reactions: false,
|
|
7169
7256
|
threads: false,
|
|
7170
7257
|
media: true,
|
|
7171
|
-
reply: true
|
|
7258
|
+
reply: true,
|
|
7259
|
+
tts: { voice: { synthesisTarget: "voice-note", transcodesAudio: true } }
|
|
7172
7260
|
},
|
|
7173
7261
|
reload: { configPrefixes: ["channels.agent-wechat"] },
|
|
7174
7262
|
configSchema: {
|
|
@@ -7295,9 +7383,9 @@ var wechatPlugin = {
|
|
|
7295
7383
|
channel: "agent-wechat",
|
|
7296
7384
|
messageId: await sendWeChatText(cfg, to, text)
|
|
7297
7385
|
}),
|
|
7298
|
-
sendMedia: async ({ cfg, to, text, mediaUrl }) => ({
|
|
7386
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, audioAsVoice }) => ({
|
|
7299
7387
|
channel: "agent-wechat",
|
|
7300
|
-
messageId: await sendWeChatMedia(cfg, to, text, mediaUrl)
|
|
7388
|
+
messageId: await sendWeChatMedia(cfg, to, text, mediaUrl, audioAsVoice)
|
|
7301
7389
|
})
|
|
7302
7390
|
},
|
|
7303
7391
|
message: wechatMessageAdapter,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-wechat/agent-wechat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"esbuild": "^0.25.0",
|
|
12
12
|
"openclaw": "^2026.8.1",
|
|
13
13
|
"typescript": "^5.4.5",
|
|
14
|
-
"@agent-wechat/shared": "0.
|
|
14
|
+
"@agent-wechat/shared": "0.3.0"
|
|
15
15
|
},
|
|
16
16
|
"openclaw": {
|
|
17
17
|
"extensions": [
|