@agent-wechat/agent-wechat 0.14.0 → 0.15.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/dist/index.js +206 -111
- 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");
|
|
@@ -5543,6 +5563,9 @@ function attachmentKindForMessageType(baseType) {
|
|
|
5543
5563
|
if (baseType === 49) return "file";
|
|
5544
5564
|
return void 0;
|
|
5545
5565
|
}
|
|
5566
|
+
function mediaRequestQuality(baseType) {
|
|
5567
|
+
return baseType === 3 ? "best" : "full";
|
|
5568
|
+
}
|
|
5546
5569
|
function attachmentKindForResult(result, fallback) {
|
|
5547
5570
|
if (result.type === "image" || result.type === "emoji") return "image";
|
|
5548
5571
|
if (result.type === "voice") return "audio";
|
|
@@ -5595,8 +5618,11 @@ function renderAttachmentBody(originalBody, attachment, exposeNonVisualPath = tr
|
|
|
5595
5618
|
return appendReference(originalBody || "<media:video>", `[Video file: ${attachment.path}]`);
|
|
5596
5619
|
}
|
|
5597
5620
|
}
|
|
5598
|
-
|
|
5599
|
-
|
|
5621
|
+
const body = originalBody || (attachment.kind === "image" ? "<media:image>" : originalBody);
|
|
5622
|
+
if (attachment.kind === "image" && attachment.quality === "thumbnail") {
|
|
5623
|
+
return appendReference(body, "[Image preview only: thumbnail quality]");
|
|
5624
|
+
}
|
|
5625
|
+
return body;
|
|
5600
5626
|
}
|
|
5601
5627
|
function applyLiveAttachment(message, originalBody, attachment) {
|
|
5602
5628
|
const attachable = attachment?.status === "ready" && attachment.path && attachment.kind !== "file";
|
|
@@ -5779,6 +5805,131 @@ async function listMessageWindow(client, chatId, previousLastSeen, firstPoll, un
|
|
|
5779
5805
|
return messages;
|
|
5780
5806
|
}
|
|
5781
5807
|
|
|
5808
|
+
// src/outbound-media.ts
|
|
5809
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5810
|
+
import { readFile } from "node:fs/promises";
|
|
5811
|
+
import { basename, extname } from "node:path";
|
|
5812
|
+
var EXT_MIME = {
|
|
5813
|
+
png: "image/png",
|
|
5814
|
+
jpg: "image/jpeg",
|
|
5815
|
+
jpeg: "image/jpeg",
|
|
5816
|
+
gif: "image/gif",
|
|
5817
|
+
webp: "image/webp",
|
|
5818
|
+
mp3: "audio/mpeg",
|
|
5819
|
+
wav: "audio/wav",
|
|
5820
|
+
ogg: "audio/ogg",
|
|
5821
|
+
opus: "audio/ogg",
|
|
5822
|
+
m4a: "audio/mp4",
|
|
5823
|
+
flac: "audio/flac",
|
|
5824
|
+
aac: "audio/aac"
|
|
5825
|
+
};
|
|
5826
|
+
function isTerminal(job) {
|
|
5827
|
+
return ["completed", "failed", "cancelled", "needs_review"].includes(job.status);
|
|
5828
|
+
}
|
|
5829
|
+
async function waitForVoiceJob(client, jobId) {
|
|
5830
|
+
const deadline = Date.now() + 25 * 6e4;
|
|
5831
|
+
let lastStatusAt = Date.now();
|
|
5832
|
+
while (Date.now() < deadline) {
|
|
5833
|
+
let job;
|
|
5834
|
+
try {
|
|
5835
|
+
job = await client.getVoiceJob(jobId);
|
|
5836
|
+
lastStatusAt = Date.now();
|
|
5837
|
+
} catch (error) {
|
|
5838
|
+
if (Date.now() - lastStatusAt > 3e4) {
|
|
5839
|
+
throw new Error(`Voice job ${jobId} status is unavailable: ${String(error)}. Inspect the job before retrying.`);
|
|
5840
|
+
}
|
|
5841
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
5842
|
+
continue;
|
|
5843
|
+
}
|
|
5844
|
+
if (isTerminal(job)) {
|
|
5845
|
+
if (job.status === "completed") return job;
|
|
5846
|
+
const sent = job.chunks.filter((chunk) => chunk.status === "verified").length;
|
|
5847
|
+
throw new Error(`Voice job ${jobId} ${job.status}: ${sent}/${job.chunks.length} notes verified. ${job.error ?? ""} Do not resend the whole recording.`);
|
|
5848
|
+
}
|
|
5849
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
5850
|
+
}
|
|
5851
|
+
throw new Error(`Voice job ${jobId} exceeded the 25-minute wait. Inspect the job before retrying.`);
|
|
5852
|
+
}
|
|
5853
|
+
async function sendWeChatMedia(cfg, to, text, mediaUrl, audioAsVoice = false, idempotencyKey) {
|
|
5854
|
+
const account = resolveWeChatAccount(cfg);
|
|
5855
|
+
if (!account?.serverUrl) throw new Error("No serverUrl configured");
|
|
5856
|
+
const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
|
|
5857
|
+
if (!mediaUrl) {
|
|
5858
|
+
if (audioAsVoice) throw new Error("Voice sending requires an audio attachment");
|
|
5859
|
+
const result2 = await client.sendMessage({ chatId: to, text: text || void 0 });
|
|
5860
|
+
if (!result2.success) throw new Error(result2.error ?? "Send failed");
|
|
5861
|
+
return `agent-wechat:${to}:${Date.now()}`;
|
|
5862
|
+
}
|
|
5863
|
+
let bytes;
|
|
5864
|
+
let mimeType;
|
|
5865
|
+
let filename;
|
|
5866
|
+
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
5867
|
+
const res = await fetch(mediaUrl);
|
|
5868
|
+
if (!res.ok) throw new Error(`Media fetch failed: ${res.status}`);
|
|
5869
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
5870
|
+
mimeType = res.headers.get("content-type")?.split(";")[0] ?? "application/octet-stream";
|
|
5871
|
+
filename = basename(new URL(mediaUrl).pathname) || "file";
|
|
5872
|
+
if (mimeType === "application/octet-stream") {
|
|
5873
|
+
mimeType = EXT_MIME[extname(filename).toLowerCase().slice(1)] ?? mimeType;
|
|
5874
|
+
}
|
|
5875
|
+
} else {
|
|
5876
|
+
const localPath = mediaUrl.startsWith("file://") ? new URL(mediaUrl) : mediaUrl;
|
|
5877
|
+
const pathText = localPath instanceof URL ? localPath.pathname : localPath;
|
|
5878
|
+
bytes = new Uint8Array(await readFile(localPath));
|
|
5879
|
+
filename = basename(pathText);
|
|
5880
|
+
mimeType = EXT_MIME[extname(filename).toLowerCase().slice(1)] ?? "application/octet-stream";
|
|
5881
|
+
}
|
|
5882
|
+
if (audioAsVoice) {
|
|
5883
|
+
if (!mimeType.startsWith("audio/")) throw new Error(`Voice media must be audio, got ${mimeType}`);
|
|
5884
|
+
const job = await client.createVoiceJob(to, bytes, idempotencyKey ?? randomUUID2());
|
|
5885
|
+
await waitForVoiceJob(client, job.jobId);
|
|
5886
|
+
if (text) {
|
|
5887
|
+
const caption = await client.sendMessage({ chatId: to, text });
|
|
5888
|
+
if (!caption.success) throw new Error(`Voice job ${job.jobId} completed, but caption failed: ${caption.error ?? "unknown error"}`);
|
|
5889
|
+
}
|
|
5890
|
+
return `agent-wechat:voice:${job.jobId}`;
|
|
5891
|
+
}
|
|
5892
|
+
const base64 = Buffer.from(bytes).toString("base64");
|
|
5893
|
+
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 } });
|
|
5894
|
+
if (!result.success) throw new Error(result.error ?? "Send media failed");
|
|
5895
|
+
return `agent-wechat:${to}:${Date.now()}`;
|
|
5896
|
+
}
|
|
5897
|
+
|
|
5898
|
+
// src/media-poll.ts
|
|
5899
|
+
function rejectsBestQuality(error) {
|
|
5900
|
+
return error instanceof Error && /^(400|422)\b/.test(error.message) && /\bbest\b/i.test(error.message);
|
|
5901
|
+
}
|
|
5902
|
+
async function pollMedia(client, chatId, localId, quality, log, maxAttempts = 15, intervalMs = 1e3) {
|
|
5903
|
+
let lastResult = null;
|
|
5904
|
+
let legacyImageServer = false;
|
|
5905
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5906
|
+
let result;
|
|
5907
|
+
if (legacyImageServer) {
|
|
5908
|
+
const full = await client.getMedia(chatId, localId, "full");
|
|
5909
|
+
if (full.data || full.type === "unsupported") return full;
|
|
5910
|
+
const preview = await client.getMedia(chatId, localId, "thumbnail");
|
|
5911
|
+
result = preview.data ? { ...preview, quality: "thumbnail" } : full;
|
|
5912
|
+
} else {
|
|
5913
|
+
try {
|
|
5914
|
+
result = await client.getMedia(chatId, localId, quality);
|
|
5915
|
+
} catch (error) {
|
|
5916
|
+
if (quality !== "best" || !rejectsBestQuality(error)) throw error;
|
|
5917
|
+
legacyImageServer = true;
|
|
5918
|
+
log?.info?.("[media] Server does not support best image quality; trying full then thumbnail");
|
|
5919
|
+
attempt--;
|
|
5920
|
+
continue;
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
lastResult = result;
|
|
5924
|
+
if (result.type === "unsupported" || result.data) return result;
|
|
5925
|
+
if (attempt < maxAttempts) {
|
|
5926
|
+
log?.info?.(`[media] Attempt ${attempt}/${maxAttempts} for ${chatId}:${localId} returned no data, retrying...`);
|
|
5927
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
return lastResult;
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5782
5933
|
// src/access-control.ts
|
|
5783
5934
|
import {
|
|
5784
5935
|
buildChannelKeyCandidates,
|
|
@@ -5989,6 +6140,7 @@ function resolveWeChatMentionGate(params) {
|
|
|
5989
6140
|
// src/monitor.ts
|
|
5990
6141
|
var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
|
|
5991
6142
|
var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
|
|
6143
|
+
var voiceTranscriptCache = /* @__PURE__ */ new Map();
|
|
5992
6144
|
function isProcessedMessage(value) {
|
|
5993
6145
|
if (!value || typeof value !== "object") return false;
|
|
5994
6146
|
const message = value;
|
|
@@ -6006,24 +6158,6 @@ function sleep(ms, signal) {
|
|
|
6006
6158
|
});
|
|
6007
6159
|
});
|
|
6008
6160
|
}
|
|
6009
|
-
async function pollMedia(client, chatId, localId, log, maxAttempts = 15, intervalMs = 1e3) {
|
|
6010
|
-
let lastResult = null;
|
|
6011
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
6012
|
-
const result = await client.getMedia(chatId, localId, "full");
|
|
6013
|
-
lastResult = result;
|
|
6014
|
-
if (result.type === "unsupported") {
|
|
6015
|
-
return result;
|
|
6016
|
-
}
|
|
6017
|
-
if (result.data) {
|
|
6018
|
-
return result;
|
|
6019
|
-
}
|
|
6020
|
-
if (attempt < maxAttempts) {
|
|
6021
|
-
log?.info?.(`[media] Attempt ${attempt}/${maxAttempts} for ${chatId}:${localId} returned no data, retrying...`);
|
|
6022
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
6023
|
-
}
|
|
6024
|
-
}
|
|
6025
|
-
return lastResult;
|
|
6026
|
-
}
|
|
6027
6161
|
function enqueueWeChatSystemEvent(text, contextKey) {
|
|
6028
6162
|
try {
|
|
6029
6163
|
const core = getWeChatRuntime();
|
|
@@ -6040,7 +6174,7 @@ async function retrieveAttachment(client, chatId, msg, liveAccount, log, maxAtte
|
|
|
6040
6174
|
if (!expectedKind) return void 0;
|
|
6041
6175
|
const fallbackFilename = attachmentFallbackFilename(expectedKind, msg.localId, msg.content);
|
|
6042
6176
|
try {
|
|
6043
|
-
const result = await pollMedia(client, chatId, msg.localId, log, maxAttempts);
|
|
6177
|
+
const result = await pollMedia(client, chatId, msg.localId, mediaRequestQuality(baseType), log, maxAttempts);
|
|
6044
6178
|
if (!result) {
|
|
6045
6179
|
return { kind: expectedKind, status: "unavailable", filename: fallbackFilename };
|
|
6046
6180
|
}
|
|
@@ -6062,9 +6196,9 @@ async function retrieveAttachment(client, chatId, msg, liveAccount, log, maxAtte
|
|
|
6062
6196
|
saveMediaBuffer: getWeChatRuntime().channel.media.saveMediaBuffer
|
|
6063
6197
|
});
|
|
6064
6198
|
log?.info?.(
|
|
6065
|
-
`[wechat:${liveAccount.accountId}] Saved ${kind} attachment for msg ${msg.localId} (${saved.size} bytes)`
|
|
6199
|
+
`[wechat:${liveAccount.accountId}] Saved ${kind} attachment for msg ${msg.localId} (${saved.size} bytes${result.quality ? `, ${result.quality} quality` : ""})`
|
|
6066
6200
|
);
|
|
6067
|
-
return { kind, status: "ready", filename: saved.filename, mime, path: saved.path };
|
|
6201
|
+
return { kind, status: "ready", filename: saved.filename, mime, path: saved.path, quality: result.quality };
|
|
6068
6202
|
} catch (error) {
|
|
6069
6203
|
const status = error instanceof AttachmentTooLargeError ? "too_large" : "error";
|
|
6070
6204
|
log?.error?.(
|
|
@@ -6360,6 +6494,36 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6360
6494
|
return "ignored";
|
|
6361
6495
|
}
|
|
6362
6496
|
try {
|
|
6497
|
+
for (const item of segment) {
|
|
6498
|
+
const attachment = item.attachment;
|
|
6499
|
+
if (item.mediaPath || attachment?.kind !== "audio" || attachment.status !== "ready" || !attachment.path) continue;
|
|
6500
|
+
const cacheKey = `${liveAccount.accountId}:${chatId}:${item.msg.localId}:${attachment.path}`;
|
|
6501
|
+
let transcript = voiceTranscriptCache.get(cacheKey);
|
|
6502
|
+
if (!transcript) {
|
|
6503
|
+
try {
|
|
6504
|
+
const result = await core.mediaUnderstanding.transcribeAudioFile({
|
|
6505
|
+
filePath: attachment.path,
|
|
6506
|
+
cfg,
|
|
6507
|
+
mime: attachment.mime
|
|
6508
|
+
});
|
|
6509
|
+
transcript = result.text?.trim();
|
|
6510
|
+
if (transcript) {
|
|
6511
|
+
if (voiceTranscriptCache.size >= 500) voiceTranscriptCache.clear();
|
|
6512
|
+
voiceTranscriptCache.set(cacheKey, transcript);
|
|
6513
|
+
}
|
|
6514
|
+
} catch (error) {
|
|
6515
|
+
log?.error?.(`[wechat:${liveAccount.accountId}] Voice transcription failed for ${item.msg.localId}: ${String(error)}`);
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
if (transcript) {
|
|
6519
|
+
item.rawBody += `
|
|
6520
|
+
[Voice transcript: ${transcript.slice(0, 1e4)}]`;
|
|
6521
|
+
item.commandBody = normalizeWeChatCommandBody(item.rawBody, {
|
|
6522
|
+
isGroup: item.isGroup,
|
|
6523
|
+
wasMentioned: item.isMentioned
|
|
6524
|
+
});
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6363
6527
|
const route = core.channel.routing.resolveAgentRoute({
|
|
6364
6528
|
cfg,
|
|
6365
6529
|
channel: "agent-wechat",
|
|
@@ -6388,7 +6552,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6388
6552
|
timestamp,
|
|
6389
6553
|
previousTimestamp,
|
|
6390
6554
|
envelope: envelopeOptions,
|
|
6391
|
-
body: isGroup ? `${senderName}: ${rawBody}` : rawBody
|
|
6555
|
+
body: isGroup ? `${senderName}: ${lastMsg.rawBody}` : lastMsg.rawBody
|
|
6392
6556
|
});
|
|
6393
6557
|
} else {
|
|
6394
6558
|
const historyMessages = segment.slice(0, -1);
|
|
@@ -6408,7 +6572,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6408
6572
|
timestamp,
|
|
6409
6573
|
previousTimestamp,
|
|
6410
6574
|
envelope: envelopeOptions,
|
|
6411
|
-
body: isGroup ? `${senderName}: ${rawBody}` : rawBody
|
|
6575
|
+
body: isGroup ? `${senderName}: ${lastMsg.rawBody}` : lastMsg.rawBody
|
|
6412
6576
|
});
|
|
6413
6577
|
body = [
|
|
6414
6578
|
HISTORY_CONTEXT_MARKER,
|
|
@@ -6430,8 +6594,8 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6430
6594
|
}
|
|
6431
6595
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
6432
6596
|
Body: body,
|
|
6433
|
-
BodyForAgent: rawBody,
|
|
6434
|
-
RawBody: rawBody,
|
|
6597
|
+
BodyForAgent: lastMsg.rawBody,
|
|
6598
|
+
RawBody: lastMsg.rawBody,
|
|
6435
6599
|
CommandBody: commandBody,
|
|
6436
6600
|
InboundHistory: inboundHistory,
|
|
6437
6601
|
From: isGroup ? `wechat:group:${chatId}` : `wechat:${senderId}`,
|
|
@@ -6473,12 +6637,14 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6473
6637
|
accountId: liveAccount.accountId
|
|
6474
6638
|
});
|
|
6475
6639
|
let replyFailed = false;
|
|
6640
|
+
let deliverySequence = 0;
|
|
6476
6641
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
6477
6642
|
ctx: ctxPayload,
|
|
6478
6643
|
cfg,
|
|
6479
6644
|
dispatcherOptions: {
|
|
6480
6645
|
...prefixOptions,
|
|
6481
6646
|
deliver: async (payload) => {
|
|
6647
|
+
const deliveryIndex = deliverySequence++;
|
|
6482
6648
|
const mediaList = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
6483
6649
|
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
6484
6650
|
cfg,
|
|
@@ -6490,49 +6656,17 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6490
6656
|
tableMode
|
|
6491
6657
|
);
|
|
6492
6658
|
if (mediaList.length > 0) {
|
|
6493
|
-
for (
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
const pathmod = await import("path");
|
|
6497
|
-
let base64;
|
|
6498
|
-
let mimeType;
|
|
6499
|
-
let filename;
|
|
6500
|
-
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
6501
|
-
const res = await fetch(mediaUrl);
|
|
6502
|
-
const buffer = await res.arrayBuffer();
|
|
6503
|
-
base64 = Buffer.from(buffer).toString("base64");
|
|
6504
|
-
mimeType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
6505
|
-
const urlPath = new URL(mediaUrl).pathname;
|
|
6506
|
-
filename = pathmod.basename(urlPath) || "file";
|
|
6507
|
-
} else {
|
|
6508
|
-
const buf = await fsmod.readFile(mediaUrl);
|
|
6509
|
-
base64 = buf.toString("base64");
|
|
6510
|
-
filename = pathmod.basename(mediaUrl);
|
|
6511
|
-
const ext = pathmod.extname(mediaUrl).toLowerCase().replace(".", "");
|
|
6512
|
-
const extMime = {
|
|
6513
|
-
png: "image/png",
|
|
6514
|
-
jpg: "image/jpeg",
|
|
6515
|
-
jpeg: "image/jpeg",
|
|
6516
|
-
gif: "image/gif",
|
|
6517
|
-
webp: "image/webp"
|
|
6518
|
-
};
|
|
6519
|
-
mimeType = extMime[ext] ?? "application/octet-stream";
|
|
6520
|
-
}
|
|
6521
|
-
const isImage = mimeType.startsWith("image/");
|
|
6522
|
-
if (isImage) {
|
|
6523
|
-
await client.sendMessage({ chatId, image: { data: base64, mimeType } });
|
|
6524
|
-
} else {
|
|
6525
|
-
await client.sendMessage({ chatId, file: { data: base64, filename } });
|
|
6526
|
-
}
|
|
6527
|
-
} catch (err) {
|
|
6528
|
-
log?.error?.(`[wechat:${liveAccount.accountId}] Failed to send media: ${err}`);
|
|
6529
|
-
}
|
|
6659
|
+
for (let index = 0; index < mediaList.length; index++) {
|
|
6660
|
+
const retryKey = createHash3("sha256").update(`${liveAccount.accountId}\0${chatId}\0${msg.localId}\0${deliveryIndex}\0${index}`).digest("hex");
|
|
6661
|
+
await sendWeChatMedia(cfg, chatId, "", mediaList[index], payload.audioAsVoice === true, retryKey);
|
|
6530
6662
|
}
|
|
6531
6663
|
if (text) {
|
|
6532
|
-
await client.sendMessage({ chatId, text });
|
|
6664
|
+
const result = await client.sendMessage({ chatId, text });
|
|
6665
|
+
if (!result.success) throw new Error(result.error ?? "Text send failed");
|
|
6533
6666
|
}
|
|
6534
6667
|
} else if (text) {
|
|
6535
|
-
await client.sendMessage({ chatId, text });
|
|
6668
|
+
const result = await client.sendMessage({ chatId, text });
|
|
6669
|
+
if (!result.success) throw new Error(result.error ?? "Text send failed");
|
|
6536
6670
|
}
|
|
6537
6671
|
},
|
|
6538
6672
|
onError: (err, info) => {
|
|
@@ -7109,46 +7243,6 @@ async function sendWeChatText(cfg, to, text) {
|
|
|
7109
7243
|
if (!result.success) throw new Error(result.error ?? "Send failed");
|
|
7110
7244
|
return `agent-wechat:${to}:${Date.now()}`;
|
|
7111
7245
|
}
|
|
7112
|
-
async function sendWeChatMedia(cfg, to, text, mediaUrl) {
|
|
7113
|
-
const account = resolveWeChatAccount(cfg);
|
|
7114
|
-
if (!account?.serverUrl) throw new Error("No serverUrl configured");
|
|
7115
|
-
const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
|
|
7116
|
-
if (!mediaUrl) {
|
|
7117
|
-
const result2 = await client.sendMessage({ chatId: to, text: text || void 0 });
|
|
7118
|
-
if (!result2.success) throw new Error(result2.error ?? "Send failed");
|
|
7119
|
-
return `agent-wechat:${to}:${Date.now()}`;
|
|
7120
|
-
}
|
|
7121
|
-
const fsmod = await import("fs/promises");
|
|
7122
|
-
const pathmod = await import("path");
|
|
7123
|
-
let base64;
|
|
7124
|
-
let mimeType;
|
|
7125
|
-
let filename;
|
|
7126
|
-
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
7127
|
-
const res = await fetch(mediaUrl);
|
|
7128
|
-
const buffer = await res.arrayBuffer();
|
|
7129
|
-
base64 = Buffer.from(buffer).toString("base64");
|
|
7130
|
-
mimeType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
7131
|
-
const urlPath = new URL(mediaUrl).pathname;
|
|
7132
|
-
filename = pathmod.basename(urlPath) || "file";
|
|
7133
|
-
} else {
|
|
7134
|
-
const buf = await fsmod.readFile(mediaUrl);
|
|
7135
|
-
base64 = buf.toString("base64");
|
|
7136
|
-
filename = pathmod.basename(mediaUrl);
|
|
7137
|
-
const ext = pathmod.extname(mediaUrl).toLowerCase().replace(".", "");
|
|
7138
|
-
const extMime = {
|
|
7139
|
-
png: "image/png",
|
|
7140
|
-
jpg: "image/jpeg",
|
|
7141
|
-
jpeg: "image/jpeg",
|
|
7142
|
-
gif: "image/gif",
|
|
7143
|
-
webp: "image/webp"
|
|
7144
|
-
};
|
|
7145
|
-
mimeType = extMime[ext] ?? "application/octet-stream";
|
|
7146
|
-
}
|
|
7147
|
-
const isImage = mimeType.startsWith("image/");
|
|
7148
|
-
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 } });
|
|
7149
|
-
if (!result.success) throw new Error(result.error ?? "Send media failed");
|
|
7150
|
-
return `agent-wechat:${to}:${Date.now()}`;
|
|
7151
|
-
}
|
|
7152
7246
|
var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
7153
7247
|
id: "agent-wechat",
|
|
7154
7248
|
capabilities: {
|
|
@@ -7161,9 +7255,9 @@ var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
|
7161
7255
|
channel: "agent-wechat",
|
|
7162
7256
|
messageId: await sendWeChatText(cfg, to, text)
|
|
7163
7257
|
}),
|
|
7164
|
-
sendMedia: async ({ cfg, to, text, mediaUrl }) => ({
|
|
7258
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, audioAsVoice }) => ({
|
|
7165
7259
|
channel: "agent-wechat",
|
|
7166
|
-
messageId: await sendWeChatMedia(cfg, to, text, mediaUrl)
|
|
7260
|
+
messageId: await sendWeChatMedia(cfg, to, text, mediaUrl, audioAsVoice)
|
|
7167
7261
|
})
|
|
7168
7262
|
}
|
|
7169
7263
|
});
|
|
@@ -7184,7 +7278,8 @@ var wechatPlugin = {
|
|
|
7184
7278
|
reactions: false,
|
|
7185
7279
|
threads: false,
|
|
7186
7280
|
media: true,
|
|
7187
|
-
reply: true
|
|
7281
|
+
reply: true,
|
|
7282
|
+
tts: { voice: { synthesisTarget: "voice-note", transcodesAudio: true } }
|
|
7188
7283
|
},
|
|
7189
7284
|
reload: { configPrefixes: ["channels.agent-wechat"] },
|
|
7190
7285
|
configSchema: {
|
|
@@ -7311,9 +7406,9 @@ var wechatPlugin = {
|
|
|
7311
7406
|
channel: "agent-wechat",
|
|
7312
7407
|
messageId: await sendWeChatText(cfg, to, text)
|
|
7313
7408
|
}),
|
|
7314
|
-
sendMedia: async ({ cfg, to, text, mediaUrl }) => ({
|
|
7409
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, audioAsVoice }) => ({
|
|
7315
7410
|
channel: "agent-wechat",
|
|
7316
|
-
messageId: await sendWeChatMedia(cfg, to, text, mediaUrl)
|
|
7411
|
+
messageId: await sendWeChatMedia(cfg, to, text, mediaUrl, audioAsVoice)
|
|
7317
7412
|
})
|
|
7318
7413
|
},
|
|
7319
7414
|
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.1",
|
|
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.1"
|
|
15
15
|
},
|
|
16
16
|
"openclaw": {
|
|
17
17
|
"extensions": [
|