@agent-wechat/agent-wechat 0.12.1 → 0.13.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 +431 -150
- package/openclaw.plugin.json +5 -0
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -1094,6 +1094,7 @@ function normalizeGroupPolicy(policy) {
|
|
|
1094
1094
|
}
|
|
1095
1095
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
1096
1096
|
var DEFAULT_AUTH_POLL_INTERVAL_MS = 3e4;
|
|
1097
|
+
var DEFAULT_MEDIA_MAX_MB = 50;
|
|
1097
1098
|
var DEFAULT_ACCOUNT_ID = "default";
|
|
1098
1099
|
function resolveWeChatAccount(cfg, accountId) {
|
|
1099
1100
|
const wechat = cfg.channels?.["agent-wechat"];
|
|
@@ -1109,7 +1110,8 @@ function resolveWeChatAccount(cfg, accountId) {
|
|
|
1109
1110
|
groupAllowFrom: wechat.groupAllowFrom ?? [],
|
|
1110
1111
|
groups: wechat.groups ?? {},
|
|
1111
1112
|
pollIntervalMs: wechat.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
1112
|
-
authPollIntervalMs: wechat.authPollIntervalMs ?? DEFAULT_AUTH_POLL_INTERVAL_MS
|
|
1113
|
+
authPollIntervalMs: wechat.authPollIntervalMs ?? DEFAULT_AUTH_POLL_INTERVAL_MS,
|
|
1114
|
+
mediaMaxMb: Number.isInteger(wechat.mediaMaxMb) && (wechat.mediaMaxMb ?? 0) > 0 && (wechat.mediaMaxMb ?? 0) <= 1024 ? wechat.mediaMaxMb : DEFAULT_MEDIA_MAX_MB
|
|
1113
1115
|
};
|
|
1114
1116
|
}
|
|
1115
1117
|
|
|
@@ -1154,16 +1156,16 @@ var WeChatClient = class {
|
|
|
1154
1156
|
return this.base;
|
|
1155
1157
|
}
|
|
1156
1158
|
// ---- internal helpers ----
|
|
1157
|
-
async get(
|
|
1158
|
-
const res = await fetch(`${this.base}${
|
|
1159
|
+
async get(path2) {
|
|
1160
|
+
const res = await fetch(`${this.base}${path2}`, {
|
|
1159
1161
|
headers: this.headers
|
|
1160
1162
|
});
|
|
1161
1163
|
if (!res.ok)
|
|
1162
1164
|
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
|
|
1163
1165
|
return res.json();
|
|
1164
1166
|
}
|
|
1165
|
-
async post(
|
|
1166
|
-
const res = await fetch(`${this.base}${
|
|
1167
|
+
async post(path2, body) {
|
|
1168
|
+
const res = await fetch(`${this.base}${path2}`, {
|
|
1167
1169
|
method: "POST",
|
|
1168
1170
|
headers: this.headers,
|
|
1169
1171
|
body: body != null ? JSON.stringify(body) : void 0
|
|
@@ -1172,8 +1174,8 @@ var WeChatClient = class {
|
|
|
1172
1174
|
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
|
|
1173
1175
|
return res.json();
|
|
1174
1176
|
}
|
|
1175
|
-
async del(
|
|
1176
|
-
const res = await fetch(`${this.base}${
|
|
1177
|
+
async del(path2) {
|
|
1178
|
+
const res = await fetch(`${this.base}${path2}`, {
|
|
1177
1179
|
method: "DELETE",
|
|
1178
1180
|
headers: this.headers
|
|
1179
1181
|
});
|
|
@@ -1222,8 +1224,8 @@ var WeChatClient = class {
|
|
|
1222
1224
|
async listMessages(chatId, limit, offset) {
|
|
1223
1225
|
return this.get(`/api/messages/${encodeURIComponent(chatId)}${qs({ limit, offset })}`);
|
|
1224
1226
|
}
|
|
1225
|
-
async getMedia(chatId, localId) {
|
|
1226
|
-
return this.get(`/api/messages/${encodeURIComponent(chatId)}/media/${localId}`);
|
|
1227
|
+
async getMedia(chatId, localId, quality) {
|
|
1228
|
+
return this.get(`/api/messages/${encodeURIComponent(chatId)}/media/${localId}${qs({ quality })}`);
|
|
1227
1229
|
}
|
|
1228
1230
|
async sendMessage(params) {
|
|
1229
1231
|
return this.post("/api/messages/send", params);
|
|
@@ -1775,8 +1777,8 @@ function getErrorMap() {
|
|
|
1775
1777
|
|
|
1776
1778
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
|
|
1777
1779
|
var makeIssue = (params) => {
|
|
1778
|
-
const { data, path, errorMaps, issueData } = params;
|
|
1779
|
-
const fullPath = [...
|
|
1780
|
+
const { data, path: path2, errorMaps, issueData } = params;
|
|
1781
|
+
const fullPath = [...path2, ...issueData.path || []];
|
|
1780
1782
|
const fullIssue = {
|
|
1781
1783
|
...issueData,
|
|
1782
1784
|
path: fullPath
|
|
@@ -1892,11 +1894,11 @@ var errorUtil;
|
|
|
1892
1894
|
|
|
1893
1895
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
|
|
1894
1896
|
var ParseInputLazyPath = class {
|
|
1895
|
-
constructor(parent, value,
|
|
1897
|
+
constructor(parent, value, path2, key) {
|
|
1896
1898
|
this._cachedPath = [];
|
|
1897
1899
|
this.parent = parent;
|
|
1898
1900
|
this.data = value;
|
|
1899
|
-
this._path =
|
|
1901
|
+
this._path = path2;
|
|
1900
1902
|
this._key = key;
|
|
1901
1903
|
}
|
|
1902
1904
|
get path() {
|
|
@@ -5495,10 +5497,11 @@ var sendResultSchema = external_exports.object({
|
|
|
5495
5497
|
});
|
|
5496
5498
|
var getMediaParamsSchema = external_exports.object({
|
|
5497
5499
|
chatId: external_exports.string().min(1),
|
|
5498
|
-
localId: external_exports.number().int()
|
|
5500
|
+
localId: external_exports.number().int(),
|
|
5501
|
+
quality: external_exports.enum(["full", "thumbnail"]).optional()
|
|
5499
5502
|
});
|
|
5500
5503
|
var mediaResultSchema = external_exports.object({
|
|
5501
|
-
type: external_exports.enum(["image", "emoji", "voice", "video", "unsupported"]),
|
|
5504
|
+
type: external_exports.enum(["image", "emoji", "voice", "video", "file", "pending", "unsupported"]),
|
|
5502
5505
|
data: external_exports.string().optional(),
|
|
5503
5506
|
url: external_exports.string().optional(),
|
|
5504
5507
|
format: external_exports.string(),
|
|
@@ -5513,6 +5516,255 @@ var agentConfigSchema = external_exports.object({
|
|
|
5513
5516
|
// src/monitor.ts
|
|
5514
5517
|
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
|
5515
5518
|
|
|
5519
|
+
// src/media-delivery.ts
|
|
5520
|
+
var MIME_BY_FORMAT = {
|
|
5521
|
+
jpeg: "image/jpeg",
|
|
5522
|
+
jpg: "image/jpeg",
|
|
5523
|
+
png: "image/png",
|
|
5524
|
+
gif: "image/gif",
|
|
5525
|
+
webp: "image/webp",
|
|
5526
|
+
mp3: "audio/mpeg",
|
|
5527
|
+
silk: "audio/silk",
|
|
5528
|
+
mp4: "video/mp4",
|
|
5529
|
+
pdf: "application/pdf",
|
|
5530
|
+
doc: "application/msword",
|
|
5531
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
5532
|
+
xls: "application/vnd.ms-excel",
|
|
5533
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
5534
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
5535
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
5536
|
+
zip: "application/zip",
|
|
5537
|
+
txt: "text/plain"
|
|
5538
|
+
};
|
|
5539
|
+
function attachmentKindForMessageType(baseType) {
|
|
5540
|
+
if (baseType === 3) return "image";
|
|
5541
|
+
if (baseType === 34) return "audio";
|
|
5542
|
+
if (baseType === 43) return "video";
|
|
5543
|
+
if (baseType === 49) return "file";
|
|
5544
|
+
return void 0;
|
|
5545
|
+
}
|
|
5546
|
+
function attachmentKindForResult(result, fallback) {
|
|
5547
|
+
if (result.type === "image" || result.type === "emoji") return "image";
|
|
5548
|
+
if (result.type === "voice") return "audio";
|
|
5549
|
+
if (result.type === "video") return "video";
|
|
5550
|
+
if (result.type === "file") return "file";
|
|
5551
|
+
return fallback;
|
|
5552
|
+
}
|
|
5553
|
+
function mediaMime(format, kind) {
|
|
5554
|
+
const normalized = format.toLowerCase().replace(/^\./, "");
|
|
5555
|
+
return MIME_BY_FORMAT[normalized] ?? (kind === "image" ? "image/*" : kind === "audio" ? "audio/*" : kind === "video" ? "video/*" : "application/octet-stream");
|
|
5556
|
+
}
|
|
5557
|
+
function appendReference(body, reference) {
|
|
5558
|
+
return body ? `${body}
|
|
5559
|
+
${reference}` : reference;
|
|
5560
|
+
}
|
|
5561
|
+
function safeDisplayName(name) {
|
|
5562
|
+
return name.replace(/[\r\n\0]/g, " ").trim() || "attachment";
|
|
5563
|
+
}
|
|
5564
|
+
function renderAttachmentBody(originalBody, attachment, exposeNonVisualPath = true) {
|
|
5565
|
+
if (!attachment) return originalBody;
|
|
5566
|
+
const name = safeDisplayName(attachment.filename);
|
|
5567
|
+
if (attachment.status !== "ready" || !attachment.path) {
|
|
5568
|
+
const label = attachment.kind === "file" ? "File" : attachment.kind === "image" ? "Image" : attachment.kind === "audio" ? "Voice message" : "Video";
|
|
5569
|
+
return appendReference(originalBody, `[${label} unavailable: ${name} (${attachment.status})]`);
|
|
5570
|
+
}
|
|
5571
|
+
if (attachment.kind === "file") {
|
|
5572
|
+
const prefix = originalBody.trim() === name ? "" : originalBody;
|
|
5573
|
+
return appendReference(prefix, `[File: ${name}]
|
|
5574
|
+
[Local file: ${attachment.path}]`);
|
|
5575
|
+
}
|
|
5576
|
+
if (exposeNonVisualPath) {
|
|
5577
|
+
if (attachment.kind === "audio") {
|
|
5578
|
+
return appendReference(originalBody || "<media:audio>", `[Voice message file: ${attachment.path}]`);
|
|
5579
|
+
}
|
|
5580
|
+
if (attachment.kind === "video") {
|
|
5581
|
+
return appendReference(originalBody || "<media:video>", `[Video file: ${attachment.path}]`);
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
if (!originalBody) return attachment.kind === "image" ? "<media:image>" : originalBody;
|
|
5585
|
+
return originalBody;
|
|
5586
|
+
}
|
|
5587
|
+
function applyLiveAttachment(message, originalBody, attachment) {
|
|
5588
|
+
const attachable = attachment?.status === "ready" && attachment.path && attachment.kind !== "file";
|
|
5589
|
+
return {
|
|
5590
|
+
...message,
|
|
5591
|
+
rawBody: renderAttachmentBody(originalBody, attachment),
|
|
5592
|
+
attachment,
|
|
5593
|
+
mediaPath: attachable ? attachment.path : void 0,
|
|
5594
|
+
mediaMime: attachable ? attachment.mime : void 0,
|
|
5595
|
+
hasMedia: attachment !== void 0
|
|
5596
|
+
};
|
|
5597
|
+
}
|
|
5598
|
+
function applyCatchupAttachmentPolicy(messages) {
|
|
5599
|
+
let latestImage = -1;
|
|
5600
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
5601
|
+
const attachment = messages[index].attachment;
|
|
5602
|
+
if (attachment?.kind === "image" && attachment.status === "ready" && attachment.path) {
|
|
5603
|
+
latestImage = index;
|
|
5604
|
+
break;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
return messages.map((message, index) => {
|
|
5608
|
+
const attachment = message.attachment;
|
|
5609
|
+
if (!attachment) return { ...message, mediaPath: void 0, mediaMime: void 0 };
|
|
5610
|
+
const selectImage = index === latestImage;
|
|
5611
|
+
let rawBody = renderAttachmentBody(message.rawBody, void 0);
|
|
5612
|
+
if (attachment.kind === "file") {
|
|
5613
|
+
rawBody = message.rawBody;
|
|
5614
|
+
} else if (!selectImage && attachment.status === "ready" && attachment.path) {
|
|
5615
|
+
const label = attachment.kind === "image" ? "Image file" : attachment.kind === "audio" ? "Voice message file" : "Video file";
|
|
5616
|
+
const reference = `[${label}: ${attachment.path}]`;
|
|
5617
|
+
rawBody = message.rawBody.includes(reference) ? message.rawBody : appendReference(message.rawBody, reference);
|
|
5618
|
+
}
|
|
5619
|
+
return {
|
|
5620
|
+
...message,
|
|
5621
|
+
rawBody,
|
|
5622
|
+
mediaPath: selectImage ? attachment.path : void 0,
|
|
5623
|
+
mediaMime: selectImage ? attachment.mime : void 0,
|
|
5624
|
+
hasMedia: attachment !== void 0
|
|
5625
|
+
};
|
|
5626
|
+
});
|
|
5627
|
+
}
|
|
5628
|
+
function buildMediaSegments(messages) {
|
|
5629
|
+
const segments = [];
|
|
5630
|
+
let batch = [];
|
|
5631
|
+
let hasAttachedMedia = false;
|
|
5632
|
+
for (const message of messages) {
|
|
5633
|
+
if (message.mediaPath && hasAttachedMedia) {
|
|
5634
|
+
segments.push(batch);
|
|
5635
|
+
batch = [message];
|
|
5636
|
+
hasAttachedMedia = true;
|
|
5637
|
+
} else {
|
|
5638
|
+
batch.push(message);
|
|
5639
|
+
if (message.mediaPath) hasAttachedMedia = true;
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
if (batch.length > 0) segments.push(batch);
|
|
5643
|
+
return segments;
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
// src/attachment-store.ts
|
|
5647
|
+
import { createHash } from "node:crypto";
|
|
5648
|
+
import fs from "node:fs/promises";
|
|
5649
|
+
var AttachmentTooLargeError = class extends Error {
|
|
5650
|
+
};
|
|
5651
|
+
function decodedSize(base64) {
|
|
5652
|
+
if (!base64 || base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(base64)) {
|
|
5653
|
+
throw new Error("Invalid attachment encoding");
|
|
5654
|
+
}
|
|
5655
|
+
const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
|
|
5656
|
+
return base64.length / 4 * 3 - padding;
|
|
5657
|
+
}
|
|
5658
|
+
async function saveManagedAttachment(params) {
|
|
5659
|
+
const size = decodedSize(params.data);
|
|
5660
|
+
if (size > params.maxBytes) {
|
|
5661
|
+
throw new AttachmentTooLargeError(`Attachment exceeds configured ${params.maxBytes} byte limit`);
|
|
5662
|
+
}
|
|
5663
|
+
const buffer = Buffer.from(params.data, "base64");
|
|
5664
|
+
if (buffer.byteLength !== size) throw new Error("Attachment decoding length mismatch");
|
|
5665
|
+
const sha256 = createHash("sha256").update(buffer).digest("hex");
|
|
5666
|
+
const saved = await params.saveMediaBuffer(
|
|
5667
|
+
buffer,
|
|
5668
|
+
params.mime,
|
|
5669
|
+
"inbound",
|
|
5670
|
+
params.maxBytes,
|
|
5671
|
+
params.filename
|
|
5672
|
+
);
|
|
5673
|
+
return { path: saved.path, size, sha256, filename: params.filename };
|
|
5674
|
+
}
|
|
5675
|
+
async function attachmentPathExists(filePath) {
|
|
5676
|
+
if (!filePath) return false;
|
|
5677
|
+
try {
|
|
5678
|
+
return (await fs.stat(filePath)).isFile();
|
|
5679
|
+
} catch {
|
|
5680
|
+
return false;
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5684
|
+
// src/monitor-state.ts
|
|
5685
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
5686
|
+
import fs2 from "node:fs/promises";
|
|
5687
|
+
import path from "node:path";
|
|
5688
|
+
var emptyState = () => ({
|
|
5689
|
+
version: 1,
|
|
5690
|
+
lastSeen: /* @__PURE__ */ Object.create(null),
|
|
5691
|
+
groupHistory: /* @__PURE__ */ Object.create(null)
|
|
5692
|
+
});
|
|
5693
|
+
var MonitorStateStore = class {
|
|
5694
|
+
filePath;
|
|
5695
|
+
constructor(stateDir, accountId) {
|
|
5696
|
+
const account = createHash2("sha256").update(accountId).digest("hex").slice(0, 16);
|
|
5697
|
+
this.filePath = path.join(stateDir, "agent-wechat", "monitor", `${account}.json`);
|
|
5698
|
+
}
|
|
5699
|
+
async load(validateItem) {
|
|
5700
|
+
let parsed;
|
|
5701
|
+
try {
|
|
5702
|
+
parsed = JSON.parse(await fs2.readFile(this.filePath, "utf8"));
|
|
5703
|
+
} catch (error) {
|
|
5704
|
+
if (error.code === "ENOENT") return emptyState();
|
|
5705
|
+
throw error;
|
|
5706
|
+
}
|
|
5707
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) {
|
|
5708
|
+
throw new Error("Unsupported WeChat monitor state");
|
|
5709
|
+
}
|
|
5710
|
+
const input = parsed;
|
|
5711
|
+
const state = emptyState();
|
|
5712
|
+
if (input.lastSeen && typeof input.lastSeen === "object") {
|
|
5713
|
+
for (const [chatId, localId] of Object.entries(input.lastSeen)) {
|
|
5714
|
+
if (typeof localId === "number" && Number.isSafeInteger(localId) && localId >= 0) {
|
|
5715
|
+
state.lastSeen[chatId] = localId;
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
if (input.groupHistory && typeof input.groupHistory === "object") {
|
|
5720
|
+
for (const [chatId, items] of Object.entries(input.groupHistory)) {
|
|
5721
|
+
if (Array.isArray(items) && items.every(validateItem)) state.groupHistory[chatId] = items;
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
return state;
|
|
5725
|
+
}
|
|
5726
|
+
async save(lastSeen, groupHistory) {
|
|
5727
|
+
const directory = path.dirname(this.filePath);
|
|
5728
|
+
await fs2.mkdir(directory, { recursive: true, mode: 448 });
|
|
5729
|
+
const state = {
|
|
5730
|
+
version: 1,
|
|
5731
|
+
lastSeen: Object.fromEntries(lastSeen),
|
|
5732
|
+
groupHistory: Object.fromEntries(groupHistory)
|
|
5733
|
+
};
|
|
5734
|
+
const temporary = path.join(directory, `.${path.basename(this.filePath)}.${randomUUID()}.tmp`);
|
|
5735
|
+
try {
|
|
5736
|
+
await fs2.writeFile(temporary, JSON.stringify(state), { flag: "wx", mode: 384 });
|
|
5737
|
+
await fs2.rename(temporary, this.filePath);
|
|
5738
|
+
} finally {
|
|
5739
|
+
await fs2.rm(temporary, { force: true }).catch(() => void 0);
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
};
|
|
5743
|
+
|
|
5744
|
+
// src/polling.ts
|
|
5745
|
+
async function listAllChats(client) {
|
|
5746
|
+
const pageSize = 100;
|
|
5747
|
+
const chats = [];
|
|
5748
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
5749
|
+
const page = await client.listChats(pageSize, offset);
|
|
5750
|
+
chats.push(...page);
|
|
5751
|
+
if (page.length < pageSize) return chats;
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
async function listMessageWindow(client, chatId, previousLastSeen, firstPoll, unreadCount) {
|
|
5755
|
+
const pageSize = 200;
|
|
5756
|
+
const messages = [];
|
|
5757
|
+
const firstPollTarget = Math.max(unreadCount, 20);
|
|
5758
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
5759
|
+
const page = await client.listMessages(chatId, pageSize, offset);
|
|
5760
|
+
messages.push(...page);
|
|
5761
|
+
if (page.length < pageSize) break;
|
|
5762
|
+
if (firstPoll && messages.length >= firstPollTarget) break;
|
|
5763
|
+
if (!firstPoll && page.some((message) => message.localId <= previousLastSeen)) break;
|
|
5764
|
+
}
|
|
5765
|
+
return messages;
|
|
5766
|
+
}
|
|
5767
|
+
|
|
5516
5768
|
// src/access-control.ts
|
|
5517
5769
|
import {
|
|
5518
5770
|
buildChannelKeyCandidates,
|
|
@@ -5721,9 +5973,13 @@ function resolveWeChatMentionGate(params) {
|
|
|
5721
5973
|
}
|
|
5722
5974
|
|
|
5723
5975
|
// src/monitor.ts
|
|
5724
|
-
var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34, 43]);
|
|
5725
5976
|
var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
|
|
5726
5977
|
var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
|
|
5978
|
+
function isProcessedMessage(value) {
|
|
5979
|
+
if (!value || typeof value !== "object") return false;
|
|
5980
|
+
const message = value;
|
|
5981
|
+
return !!message.msg && typeof message.rawBody === "string" && typeof message.commandBody === "string" && typeof message.senderName === "string" && typeof message.senderId === "string" && typeof message.isGroup === "boolean" && typeof message.timestamp === "number" && typeof message.hasMedia === "boolean" && typeof message.isMentioned === "boolean" && typeof message.sourceBody === "string";
|
|
5982
|
+
}
|
|
5727
5983
|
function isOfficialAccount(chatId) {
|
|
5728
5984
|
return chatId.startsWith("gh_");
|
|
5729
5985
|
}
|
|
@@ -5737,10 +5993,12 @@ function sleep(ms, signal) {
|
|
|
5737
5993
|
});
|
|
5738
5994
|
}
|
|
5739
5995
|
async function pollMedia(client, chatId, localId, log, maxAttempts = 15, intervalMs = 1e3) {
|
|
5996
|
+
let lastResult = null;
|
|
5740
5997
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5741
|
-
const result = await client.getMedia(chatId, localId);
|
|
5998
|
+
const result = await client.getMedia(chatId, localId, "full");
|
|
5999
|
+
lastResult = result;
|
|
5742
6000
|
if (result.type === "unsupported") {
|
|
5743
|
-
return
|
|
6001
|
+
return result;
|
|
5744
6002
|
}
|
|
5745
6003
|
if (result.data) {
|
|
5746
6004
|
return result;
|
|
@@ -5750,7 +6008,7 @@ async function pollMedia(client, chatId, localId, log, maxAttempts = 15, interva
|
|
|
5750
6008
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
5751
6009
|
}
|
|
5752
6010
|
}
|
|
5753
|
-
return
|
|
6011
|
+
return lastResult;
|
|
5754
6012
|
}
|
|
5755
6013
|
function enqueueWeChatSystemEvent(text, contextKey) {
|
|
5756
6014
|
try {
|
|
@@ -5762,12 +6020,71 @@ function enqueueWeChatSystemEvent(text, contextKey) {
|
|
|
5762
6020
|
} catch {
|
|
5763
6021
|
}
|
|
5764
6022
|
}
|
|
6023
|
+
async function retrieveAttachment(client, chatId, msg, liveAccount, log, maxAttempts = 15) {
|
|
6024
|
+
const baseType = msg.type & 2147483647;
|
|
6025
|
+
const expectedKind = attachmentKindForMessageType(baseType);
|
|
6026
|
+
if (!expectedKind) return void 0;
|
|
6027
|
+
try {
|
|
6028
|
+
const result = await pollMedia(client, chatId, msg.localId, log, maxAttempts);
|
|
6029
|
+
if (!result) {
|
|
6030
|
+
return { kind: expectedKind, status: "unavailable", filename: msg.content || `message-${msg.localId}` };
|
|
6031
|
+
}
|
|
6032
|
+
if (result.type === "unsupported") {
|
|
6033
|
+
if (baseType === 49) return void 0;
|
|
6034
|
+
return { kind: expectedKind, status: "unsupported", filename: msg.content || `message-${msg.localId}` };
|
|
6035
|
+
}
|
|
6036
|
+
const kind = attachmentKindForResult(result, expectedKind);
|
|
6037
|
+
const filename = result.filename || msg.content || `message-${msg.localId}`;
|
|
6038
|
+
if (!result.data) {
|
|
6039
|
+
return { kind, status: result.type === "pending" ? "pending" : "unavailable", filename };
|
|
6040
|
+
}
|
|
6041
|
+
const mime = mediaMime(result.format, kind);
|
|
6042
|
+
const saved = await saveManagedAttachment({
|
|
6043
|
+
data: result.data,
|
|
6044
|
+
mime,
|
|
6045
|
+
filename,
|
|
6046
|
+
maxBytes: liveAccount.mediaMaxMb * 1024 * 1024,
|
|
6047
|
+
saveMediaBuffer: getWeChatRuntime().channel.media.saveMediaBuffer
|
|
6048
|
+
});
|
|
6049
|
+
log?.info?.(
|
|
6050
|
+
`[wechat:${liveAccount.accountId}] Saved ${kind} attachment for msg ${msg.localId} (${saved.size} bytes)`
|
|
6051
|
+
);
|
|
6052
|
+
return { kind, status: "ready", filename: saved.filename, mime, path: saved.path };
|
|
6053
|
+
} catch (error) {
|
|
6054
|
+
const status = error instanceof AttachmentTooLargeError ? "too_large" : "error";
|
|
6055
|
+
log?.error?.(
|
|
6056
|
+
`[wechat:${liveAccount.accountId}] Attachment retrieval failed for msg ${msg.localId} (${status})`
|
|
6057
|
+
);
|
|
6058
|
+
return {
|
|
6059
|
+
kind: expectedKind,
|
|
6060
|
+
status,
|
|
6061
|
+
filename: msg.content || `message-${msg.localId}`,
|
|
6062
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6063
|
+
};
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
5765
6066
|
async function startWeChatMonitor(opts) {
|
|
5766
6067
|
const { account, abortSignal, setStatus, log } = opts;
|
|
5767
6068
|
const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
|
|
5768
|
-
const
|
|
5769
|
-
const
|
|
6069
|
+
const runtimeStateDir = getWeChatRuntime().state.resolveStateDir(process.env);
|
|
6070
|
+
const stateStore = new MonitorStateStore(runtimeStateDir, account.accountId);
|
|
6071
|
+
let savedState;
|
|
6072
|
+
try {
|
|
6073
|
+
savedState = await stateStore.load(isProcessedMessage);
|
|
6074
|
+
} catch (error) {
|
|
6075
|
+
log?.error?.(`[wechat:${account.accountId}] Ignoring unreadable monitor state: ${String(error)}`);
|
|
6076
|
+
savedState = { version: 1, lastSeen: {}, groupHistory: {} };
|
|
6077
|
+
}
|
|
6078
|
+
const lastSeenId = new Map(Object.entries(savedState.lastSeen));
|
|
6079
|
+
const groupHistory = new Map(Object.entries(savedState.groupHistory));
|
|
5770
6080
|
const GROUP_HISTORY_LIMIT = 50;
|
|
6081
|
+
const persistState = async () => {
|
|
6082
|
+
try {
|
|
6083
|
+
await stateStore.save(lastSeenId, groupHistory);
|
|
6084
|
+
} catch (error) {
|
|
6085
|
+
log?.error?.(`[wechat:${account.accountId}] Failed to persist monitor state: ${String(error)}`);
|
|
6086
|
+
}
|
|
6087
|
+
};
|
|
5771
6088
|
let lastAuthCheck = 0;
|
|
5772
6089
|
let prevStatus = void 0;
|
|
5773
6090
|
setStatus({
|
|
@@ -5831,7 +6148,7 @@ async function startWeChatMonitor(opts) {
|
|
|
5831
6148
|
}
|
|
5832
6149
|
let chats;
|
|
5833
6150
|
try {
|
|
5834
|
-
chats = await client
|
|
6151
|
+
chats = await listAllChats(client);
|
|
5835
6152
|
} catch (err) {
|
|
5836
6153
|
log?.error?.(
|
|
5837
6154
|
`[wechat:${account.accountId}] Failed to list chats: ${err}`
|
|
@@ -5859,7 +6176,8 @@ async function startWeChatMonitor(opts) {
|
|
|
5859
6176
|
log,
|
|
5860
6177
|
void 0,
|
|
5861
6178
|
groupHistory,
|
|
5862
|
-
GROUP_HISTORY_LIMIT
|
|
6179
|
+
GROUP_HISTORY_LIMIT,
|
|
6180
|
+
persistState
|
|
5863
6181
|
);
|
|
5864
6182
|
}
|
|
5865
6183
|
}
|
|
@@ -5874,7 +6192,7 @@ async function startWeChatMonitor(opts) {
|
|
|
5874
6192
|
log?.info?.(
|
|
5875
6193
|
`[wechat:${account.accountId}] Catch-up: ${chatId} lastMsgLocalId=${chat.lastMsgLocalId} > lastSeenId=${prevSeen}`
|
|
5876
6194
|
);
|
|
5877
|
-
await processUnreadChat(client, chat, lastSeenId, account, cfg, log, true, groupHistory, GROUP_HISTORY_LIMIT);
|
|
6195
|
+
await processUnreadChat(client, chat, lastSeenId, account, cfg, log, true, groupHistory, GROUP_HISTORY_LIMIT, persistState);
|
|
5878
6196
|
}
|
|
5879
6197
|
} catch (err) {
|
|
5880
6198
|
log?.error?.(
|
|
@@ -5890,7 +6208,6 @@ async function startWeChatMonitor(opts) {
|
|
|
5890
6208
|
});
|
|
5891
6209
|
}
|
|
5892
6210
|
async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, log) {
|
|
5893
|
-
const core = getWeChatRuntime();
|
|
5894
6211
|
if (msg.isSelf) {
|
|
5895
6212
|
log?.info?.(`[wechat:${liveAccount.accountId}] Skipping self-sent msg ${msg.localId}`);
|
|
5896
6213
|
return null;
|
|
@@ -5910,113 +6227,67 @@ async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, lo
|
|
|
5910
6227
|
);
|
|
5911
6228
|
return null;
|
|
5912
6229
|
}
|
|
5913
|
-
let mediaPath;
|
|
5914
|
-
let mediaMime;
|
|
5915
|
-
let hasMedia = false;
|
|
5916
6230
|
const baseType = msg.type & 2147483647;
|
|
5917
|
-
|
|
5918
|
-
if (
|
|
6231
|
+
let attachment;
|
|
6232
|
+
if (attachmentKindForMessageType(baseType)) {
|
|
5919
6233
|
log?.info?.(`[wechat:${liveAccount.accountId}] Checking media for msg ${msg.localId} (type ${baseType})`);
|
|
5920
|
-
|
|
5921
|
-
const result = await pollMedia(client, chatId, msg.localId, log);
|
|
5922
|
-
if (result && result.data && result.type !== "unsupported") {
|
|
5923
|
-
hasMedia = true;
|
|
5924
|
-
log?.info?.(`[wechat:${liveAccount.accountId}] Media result: type=${result.type}, format=${result.format}, hasData=${!!result.data}, filename=${result.filename}`);
|
|
5925
|
-
const mimeMap = {
|
|
5926
|
-
jpeg: "image/jpeg",
|
|
5927
|
-
jpg: "image/jpeg",
|
|
5928
|
-
png: "image/png",
|
|
5929
|
-
gif: "image/gif",
|
|
5930
|
-
mp3: "audio/mpeg",
|
|
5931
|
-
pdf: "application/pdf",
|
|
5932
|
-
doc: "application/msword",
|
|
5933
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
5934
|
-
xls: "application/vnd.ms-excel",
|
|
5935
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
5936
|
-
ppt: "application/vnd.ms-powerpoint",
|
|
5937
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
5938
|
-
zip: "application/zip",
|
|
5939
|
-
txt: "text/plain"
|
|
5940
|
-
};
|
|
5941
|
-
mediaMime = mimeMap[result.format] ?? `application/${result.format || "octet-stream"}`;
|
|
5942
|
-
const buf = Buffer.from(result.data, "base64");
|
|
5943
|
-
const saved = await core.channel.media.saveMediaBuffer(
|
|
5944
|
-
buf,
|
|
5945
|
-
mediaMime,
|
|
5946
|
-
"inbound",
|
|
5947
|
-
void 0,
|
|
5948
|
-
result.filename
|
|
5949
|
-
);
|
|
5950
|
-
mediaPath = saved?.path;
|
|
5951
|
-
log?.info?.(`[wechat:${liveAccount.accountId}] Saved media to ${mediaPath}`);
|
|
5952
|
-
} else if (MEDIA_TYPES.has(baseType)) {
|
|
5953
|
-
hasMedia = true;
|
|
5954
|
-
log?.info?.(`[wechat:${liveAccount.accountId}] Media not available after retries for msg ${msg.localId}`);
|
|
5955
|
-
}
|
|
5956
|
-
} catch (err) {
|
|
5957
|
-
log?.error?.(`[wechat:${liveAccount.accountId}] Media download failed: ${err}`);
|
|
5958
|
-
}
|
|
6234
|
+
attachment = await retrieveAttachment(client, chatId, msg, liveAccount, log);
|
|
5959
6235
|
}
|
|
5960
6236
|
const timestamp = new Date(msg.timestamp).getTime();
|
|
5961
|
-
let
|
|
5962
|
-
if (mediaPath && mediaMime) {
|
|
5963
|
-
if (!rawBody) {
|
|
5964
|
-
if (mediaMime.startsWith("audio/")) {
|
|
5965
|
-
rawBody = "<media:audio>";
|
|
5966
|
-
} else if (mediaMime.startsWith("image/")) {
|
|
5967
|
-
rawBody = "<media:image>";
|
|
5968
|
-
} else {
|
|
5969
|
-
rawBody = "<media:file>";
|
|
5970
|
-
}
|
|
5971
|
-
} else if (!mediaMime.startsWith("image/") && !mediaMime.startsWith("audio/")) {
|
|
5972
|
-
rawBody = `[File: ${rawBody}]`;
|
|
5973
|
-
}
|
|
5974
|
-
}
|
|
6237
|
+
let sourceBody = msg.content || "";
|
|
5975
6238
|
if (msg.reply) {
|
|
5976
6239
|
const replySender = msg.reply.sender ?? "unknown sender";
|
|
5977
6240
|
const quotedBody = msg.reply.content.length > 50 ? msg.reply.content.slice(0, 50) + "..." : msg.reply.content;
|
|
5978
6241
|
const replyBlock = `[Replying to ${replySender}]
|
|
5979
6242
|
${quotedBody}
|
|
5980
6243
|
[/Replying]`;
|
|
5981
|
-
|
|
6244
|
+
sourceBody = sourceBody ? `${sourceBody}
|
|
5982
6245
|
|
|
5983
6246
|
${replyBlock}` : replyBlock;
|
|
5984
6247
|
}
|
|
5985
|
-
|
|
6248
|
+
const prepared = applyLiveAttachment({
|
|
5986
6249
|
msg,
|
|
5987
|
-
rawBody,
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
wasMentioned
|
|
5991
|
-
}),
|
|
5992
|
-
mediaPath,
|
|
5993
|
-
mediaMime,
|
|
6250
|
+
rawBody: sourceBody,
|
|
6251
|
+
sourceBody,
|
|
6252
|
+
commandBody: "",
|
|
5994
6253
|
senderName,
|
|
5995
6254
|
senderId,
|
|
5996
6255
|
isGroup,
|
|
5997
6256
|
timestamp,
|
|
5998
|
-
hasMedia,
|
|
6257
|
+
hasMedia: false,
|
|
5999
6258
|
isMentioned: wasMentioned
|
|
6000
|
-
};
|
|
6259
|
+
}, sourceBody, attachment);
|
|
6260
|
+
prepared.commandBody = normalizeWeChatCommandBody(prepared.rawBody, {
|
|
6261
|
+
isGroup,
|
|
6262
|
+
wasMentioned
|
|
6263
|
+
});
|
|
6264
|
+
return prepared;
|
|
6001
6265
|
}
|
|
6002
|
-
function
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6266
|
+
async function refreshAttachment(message, client, chatId, liveAccount, log) {
|
|
6267
|
+
if (!message.attachment) return message;
|
|
6268
|
+
if (message.attachment.status === "ready" && await attachmentPathExists(message.attachment.path)) {
|
|
6269
|
+
return message;
|
|
6270
|
+
}
|
|
6271
|
+
log?.info?.(`[wechat:${liveAccount.accountId}] Retrying attachment for msg ${message.msg.localId}`);
|
|
6272
|
+
const attachment = await retrieveAttachment(client, chatId, message.msg, liveAccount, log, 3);
|
|
6273
|
+
const refreshed = applyLiveAttachment(message, message.sourceBody, attachment);
|
|
6274
|
+
refreshed.commandBody = normalizeWeChatCommandBody(refreshed.rawBody, {
|
|
6275
|
+
isGroup: refreshed.isGroup,
|
|
6276
|
+
wasMentioned: refreshed.isMentioned
|
|
6277
|
+
});
|
|
6278
|
+
return refreshed;
|
|
6279
|
+
}
|
|
6280
|
+
async function refreshAttachments(messages, client, chatId, liveAccount, log) {
|
|
6281
|
+
const output = [...messages];
|
|
6282
|
+
let cursor = 0;
|
|
6283
|
+
const worker = async () => {
|
|
6284
|
+
while (cursor < messages.length) {
|
|
6285
|
+
const index = cursor++;
|
|
6286
|
+
output[index] = await refreshAttachment(messages[index], client, chatId, liveAccount, log);
|
|
6014
6287
|
}
|
|
6015
|
-
}
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
}
|
|
6019
|
-
return segments;
|
|
6288
|
+
};
|
|
6289
|
+
await Promise.all(Array.from({ length: Math.min(3, messages.length) }, worker));
|
|
6290
|
+
return output;
|
|
6020
6291
|
}
|
|
6021
6292
|
async function dispatchSegment(segment, client, chatId, chat, liveAccount, policy, storeAllowFrom, allowTextCommands, cfg, log, remainingSegments) {
|
|
6022
6293
|
const core = getWeChatRuntime();
|
|
@@ -6024,9 +6295,18 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6024
6295
|
const { isGroup, senderId, senderName, timestamp, rawBody, commandBody, msg } = lastMsg;
|
|
6025
6296
|
const mediaMsg = segment.find((pm) => pm.mediaPath);
|
|
6026
6297
|
const mediaPath = mediaMsg?.mediaPath;
|
|
6027
|
-
const
|
|
6298
|
+
const mediaMime2 = mediaMsg?.mediaMime;
|
|
6299
|
+
const attachmentPaths = segment.flatMap(
|
|
6300
|
+
(pm) => pm.attachment?.status === "ready" && pm.attachment.path ? [{
|
|
6301
|
+
path: pm.attachment.path,
|
|
6302
|
+
filename: pm.attachment.filename,
|
|
6303
|
+
kind: pm.attachment.kind,
|
|
6304
|
+
messageId: pm.msg.localId,
|
|
6305
|
+
sender: pm.senderName
|
|
6306
|
+
}] : []
|
|
6307
|
+
);
|
|
6028
6308
|
log?.info?.(
|
|
6029
|
-
`[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ?
|
|
6309
|
+
`[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ? " with model media" : ""} attachments=${attachmentPaths.length}`
|
|
6030
6310
|
);
|
|
6031
6311
|
const hasControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(commandBody, cfg);
|
|
6032
6312
|
const commandAuthorized = await resolveWeChatCommandAuthorization({
|
|
@@ -6046,7 +6326,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6046
6326
|
log?.info?.(
|
|
6047
6327
|
`[wechat:${liveAccount.accountId}] Dropping unauthorized group control command from ${senderId} in ${chatId}`
|
|
6048
6328
|
);
|
|
6049
|
-
return
|
|
6329
|
+
return "ignored";
|
|
6050
6330
|
}
|
|
6051
6331
|
const mentionGate = resolveWeChatMentionGate({
|
|
6052
6332
|
isGroup,
|
|
@@ -6061,7 +6341,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6061
6341
|
log?.info?.(
|
|
6062
6342
|
`[wechat:${liveAccount.accountId}] Skipping group segment (mention required) in ${chatId}`
|
|
6063
6343
|
);
|
|
6064
|
-
return
|
|
6344
|
+
return "ignored";
|
|
6065
6345
|
}
|
|
6066
6346
|
try {
|
|
6067
6347
|
const route = core.channel.routing.resolveAgentRoute({
|
|
@@ -6153,7 +6433,8 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6153
6433
|
CommandAuthorized: commandAuthorized,
|
|
6154
6434
|
OriginatingChannel: "agent-wechat",
|
|
6155
6435
|
OriginatingTo: `wechat:${chatId}`,
|
|
6156
|
-
|
|
6436
|
+
Attachments: attachmentPaths,
|
|
6437
|
+
...mediaPath ? { MediaPath: mediaPath, MediaUrl: mediaPath, MediaType: mediaMime2 } : {},
|
|
6157
6438
|
...msg.reply ? {
|
|
6158
6439
|
ReplyToBody: msg.reply.content.length > 50 ? msg.reply.content.slice(0, 50) + "..." : msg.reply.content,
|
|
6159
6440
|
ReplyToSender: msg.reply.sender
|
|
@@ -6175,6 +6456,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6175
6456
|
channel: "agent-wechat",
|
|
6176
6457
|
accountId: liveAccount.accountId
|
|
6177
6458
|
});
|
|
6459
|
+
let replyFailed = false;
|
|
6178
6460
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
6179
6461
|
ctx: ctxPayload,
|
|
6180
6462
|
cfg,
|
|
@@ -6238,6 +6520,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6238
6520
|
}
|
|
6239
6521
|
},
|
|
6240
6522
|
onError: (err, info) => {
|
|
6523
|
+
replyFailed = true;
|
|
6241
6524
|
log?.error?.(
|
|
6242
6525
|
`[wechat:${liveAccount.accountId}] ${info.kind} reply failed: ${String(err)}`
|
|
6243
6526
|
);
|
|
@@ -6247,18 +6530,19 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6247
6530
|
onModelSelected
|
|
6248
6531
|
}
|
|
6249
6532
|
});
|
|
6533
|
+
if (replyFailed) return "failed";
|
|
6250
6534
|
core.channel.activity?.record?.({
|
|
6251
6535
|
channel: "agent-wechat",
|
|
6252
6536
|
accountId: liveAccount.accountId,
|
|
6253
6537
|
direction: "inbound",
|
|
6254
6538
|
at: timestamp
|
|
6255
6539
|
});
|
|
6256
|
-
return
|
|
6540
|
+
return "delivered";
|
|
6257
6541
|
} catch (err) {
|
|
6258
6542
|
log?.error?.(
|
|
6259
6543
|
`[wechat:${liveAccount.accountId}] Failed to dispatch segment (last msg ${msg.localId}): ${err}`
|
|
6260
6544
|
);
|
|
6261
|
-
return
|
|
6545
|
+
return "failed";
|
|
6262
6546
|
}
|
|
6263
6547
|
}
|
|
6264
6548
|
function bufferGroupHistory(groupHistory, chatId, pm, limit) {
|
|
@@ -6274,7 +6558,7 @@ function bufferGroupHistory(groupHistory, chatId, pm, limit) {
|
|
|
6274
6558
|
if (first) groupHistory.delete(first);
|
|
6275
6559
|
}
|
|
6276
6560
|
}
|
|
6277
|
-
async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit) {
|
|
6561
|
+
async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, persistState) {
|
|
6278
6562
|
const core = getWeChatRuntime();
|
|
6279
6563
|
const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
|
|
6280
6564
|
const chatId = chat.username ?? chat.id;
|
|
@@ -6292,8 +6576,12 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6292
6576
|
if (!skipOpen) {
|
|
6293
6577
|
log?.info?.(`[wechat:${liveAccount.accountId}] Opening chat ${chatId}...`);
|
|
6294
6578
|
try {
|
|
6295
|
-
await client.openChat(chatId, true);
|
|
6296
|
-
|
|
6579
|
+
const opened = await client.openChat(chatId, true);
|
|
6580
|
+
if (opened.ok) {
|
|
6581
|
+
log?.info?.(`[wechat:${liveAccount.accountId}] Opened chat ${chatId}`);
|
|
6582
|
+
} else {
|
|
6583
|
+
log?.error?.(`[wechat:${liveAccount.accountId}] Chat selection did not complete for ${chatId}: ${opened.error ?? "unknown error"}`);
|
|
6584
|
+
}
|
|
6297
6585
|
} catch (err) {
|
|
6298
6586
|
log?.error?.(
|
|
6299
6587
|
`[wechat:${liveAccount.accountId}] Failed to open chat ${chatId}: ${err}`
|
|
@@ -6302,10 +6590,9 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6302
6590
|
}
|
|
6303
6591
|
const firstPoll = !lastSeenId.has(chatId);
|
|
6304
6592
|
const prevLastSeen = lastSeenId.get(chatId) ?? 0;
|
|
6305
|
-
const fetchLimit = Math.max(chat.unreadCount, 20);
|
|
6306
6593
|
let messages;
|
|
6307
6594
|
try {
|
|
6308
|
-
messages = await client
|
|
6595
|
+
messages = await listMessageWindow(client, chatId, prevLastSeen, firstPoll, chat.unreadCount ?? 0);
|
|
6309
6596
|
} catch (err) {
|
|
6310
6597
|
log?.error?.(
|
|
6311
6598
|
`[wechat:${liveAccount.accountId}] Failed to list messages for ${chatId}: ${err}`
|
|
@@ -6324,11 +6611,13 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6324
6611
|
newMessages = messages.slice(-unread);
|
|
6325
6612
|
const seenMax = messages[messages.length - unread - 1].localId;
|
|
6326
6613
|
lastSeenId.set(chatId, seenMax);
|
|
6614
|
+
await persistState?.();
|
|
6327
6615
|
} else if (unread >= messages.length) {
|
|
6328
6616
|
newMessages = messages;
|
|
6329
6617
|
} else {
|
|
6330
6618
|
const maxId2 = messages[messages.length - 1].localId;
|
|
6331
6619
|
lastSeenId.set(chatId, maxId2);
|
|
6620
|
+
await persistState?.();
|
|
6332
6621
|
return;
|
|
6333
6622
|
}
|
|
6334
6623
|
} else {
|
|
@@ -6344,7 +6633,7 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6344
6633
|
const processed = [];
|
|
6345
6634
|
for (const msg of newMessages) {
|
|
6346
6635
|
log?.info?.(
|
|
6347
|
-
`[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}
|
|
6636
|
+
`[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}`
|
|
6348
6637
|
);
|
|
6349
6638
|
const pm = await prepareMessage(client, msg, chatId, chat, liveAccount, policy, log);
|
|
6350
6639
|
if (pm) {
|
|
@@ -6365,51 +6654,35 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6365
6654
|
log?.info?.(`[wechat:${liveAccount.accountId}] Buffered ${processed.length} msg(s) for group history in ${chatId}`);
|
|
6366
6655
|
const maxId2 = Math.max(...newMessages.map((m) => m.localId));
|
|
6367
6656
|
lastSeenId.set(chatId, maxId2);
|
|
6657
|
+
await persistState?.();
|
|
6368
6658
|
return;
|
|
6369
6659
|
}
|
|
6370
6660
|
if (hasMention) {
|
|
6371
6661
|
clearBufferedHistory = true;
|
|
6372
6662
|
const buffered = groupHistory.get(chatId) ?? [];
|
|
6373
6663
|
if (buffered.length > 0) {
|
|
6374
|
-
|
|
6375
|
-
pm.isMentioned = true;
|
|
6376
|
-
}
|
|
6377
|
-
processed.unshift(...buffered);
|
|
6664
|
+
processed.unshift(...buffered.map((pm) => ({ ...pm, isMentioned: true })));
|
|
6378
6665
|
log?.info?.(
|
|
6379
6666
|
`[wechat:${liveAccount.accountId}] Injected ${buffered.length} buffered msg(s) as history in ${chatId}`
|
|
6380
6667
|
);
|
|
6381
6668
|
}
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
if (processed[i].mediaPath) {
|
|
6385
|
-
latestMediaIdx = i;
|
|
6386
|
-
break;
|
|
6387
|
-
}
|
|
6388
|
-
}
|
|
6389
|
-
for (let i = 0; i < processed.length; i++) {
|
|
6390
|
-
if (processed[i].mediaPath && i !== latestMediaIdx) {
|
|
6391
|
-
processed[i] = {
|
|
6392
|
-
...processed[i],
|
|
6393
|
-
mediaPath: void 0,
|
|
6394
|
-
mediaMime: void 0,
|
|
6395
|
-
hasMedia: false
|
|
6396
|
-
};
|
|
6397
|
-
}
|
|
6398
|
-
}
|
|
6669
|
+
processed.splice(0, processed.length, ...await refreshAttachments(processed, client, chatId, liveAccount, log));
|
|
6670
|
+
processed.splice(0, processed.length, ...applyCatchupAttachmentPolicy(processed));
|
|
6399
6671
|
}
|
|
6400
6672
|
} else {
|
|
6401
6673
|
clearBufferedHistory = true;
|
|
6402
6674
|
}
|
|
6403
6675
|
}
|
|
6404
6676
|
if (processed.length > 0) {
|
|
6405
|
-
const segments = hasControlCommandInWindow ? processed.map((pm) => [pm]) :
|
|
6677
|
+
const segments = hasControlCommandInWindow ? processed.map((pm) => [pm]) : buildMediaSegments(processed);
|
|
6406
6678
|
log?.info?.(
|
|
6407
6679
|
`[wechat:${liveAccount.accountId}] ${chatId}: ${processed.length} dispatchable msg(s) in ${segments.length} segment(s)`
|
|
6408
6680
|
);
|
|
6409
6681
|
let allDispatched = true;
|
|
6682
|
+
let allHandled = true;
|
|
6410
6683
|
for (let i = 0; i < segments.length; i++) {
|
|
6411
6684
|
const remaining = segments.length - i - 1;
|
|
6412
|
-
const
|
|
6685
|
+
const outcome = await dispatchSegment(
|
|
6413
6686
|
segments[i],
|
|
6414
6687
|
client,
|
|
6415
6688
|
chatId,
|
|
@@ -6422,16 +6695,22 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6422
6695
|
log,
|
|
6423
6696
|
hasControlCommandInWindow ? void 0 : remaining
|
|
6424
6697
|
);
|
|
6425
|
-
if (
|
|
6698
|
+
if (outcome !== "delivered") {
|
|
6426
6699
|
allDispatched = false;
|
|
6427
6700
|
}
|
|
6701
|
+
if (outcome === "failed") allHandled = false;
|
|
6428
6702
|
}
|
|
6429
6703
|
if (clearBufferedHistory && allDispatched && groupHistory) {
|
|
6430
6704
|
groupHistory.set(chatId, []);
|
|
6431
6705
|
}
|
|
6706
|
+
if (!allHandled) {
|
|
6707
|
+
await persistState?.();
|
|
6708
|
+
return;
|
|
6709
|
+
}
|
|
6432
6710
|
}
|
|
6433
6711
|
const maxId = Math.max(...newMessages.map((m) => m.localId));
|
|
6434
6712
|
lastSeenId.set(chatId, maxId);
|
|
6713
|
+
await persistState?.();
|
|
6435
6714
|
}
|
|
6436
6715
|
|
|
6437
6716
|
// src/status.ts
|
|
@@ -6926,7 +7205,8 @@ var wechatPlugin = {
|
|
|
6926
7205
|
}
|
|
6927
7206
|
},
|
|
6928
7207
|
pollIntervalMs: { type: "integer", minimum: 100 },
|
|
6929
|
-
authPollIntervalMs: { type: "integer", minimum: 1e3 }
|
|
7208
|
+
authPollIntervalMs: { type: "integer", minimum: 1e3 },
|
|
7209
|
+
mediaMaxMb: { type: "integer", minimum: 1, maximum: 1024 }
|
|
6930
7210
|
}
|
|
6931
7211
|
}
|
|
6932
7212
|
},
|
|
@@ -6950,7 +7230,8 @@ var wechatPlugin = {
|
|
|
6950
7230
|
groupAllowFrom: [],
|
|
6951
7231
|
groups: {},
|
|
6952
7232
|
pollIntervalMs: 1e3,
|
|
6953
|
-
authPollIntervalMs: 3e4
|
|
7233
|
+
authPollIntervalMs: 3e4,
|
|
7234
|
+
mediaMaxMb: 50
|
|
6954
7235
|
};
|
|
6955
7236
|
}
|
|
6956
7237
|
return account;
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-wechat/agent-wechat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.2.0"
|
|
15
15
|
},
|
|
16
16
|
"openclaw": {
|
|
17
17
|
"extensions": [
|
|
@@ -106,6 +106,11 @@
|
|
|
106
106
|
"authPollIntervalMs": {
|
|
107
107
|
"type": "integer",
|
|
108
108
|
"minimum": 1000
|
|
109
|
+
},
|
|
110
|
+
"mediaMaxMb": {
|
|
111
|
+
"type": "integer",
|
|
112
|
+
"minimum": 1,
|
|
113
|
+
"maximum": 1024
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
}
|