@agent-wechat/agent-wechat 0.12.2 → 0.14.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 +447 -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,269 @@ 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 attachmentFallbackFilename(kind, localId, originalBody = "") {
|
|
5565
|
+
if (kind === "file" && originalBody && !originalBody.trimStart().startsWith("<")) {
|
|
5566
|
+
return safeDisplayName(originalBody);
|
|
5567
|
+
}
|
|
5568
|
+
const extension = kind === "image" ? "jpg" : kind === "audio" ? "mp3" : kind === "video" ? "mp4" : "bin";
|
|
5569
|
+
return `message-${localId}.${extension}`;
|
|
5570
|
+
}
|
|
5571
|
+
function attachmentSourceBody(originalBody, kind) {
|
|
5572
|
+
if (!kind || kind === "file") return originalBody;
|
|
5573
|
+
return originalBody.trimStart().startsWith("<") ? "" : originalBody;
|
|
5574
|
+
}
|
|
5575
|
+
function renderAttachmentBody(originalBody, attachment, exposeNonVisualPath = true) {
|
|
5576
|
+
if (!attachment) return originalBody;
|
|
5577
|
+
const name = safeDisplayName(attachment.filename);
|
|
5578
|
+
if (attachment.status !== "ready" || !attachment.path) {
|
|
5579
|
+
const label = attachment.kind === "file" ? "File" : attachment.kind === "image" ? "Image" : attachment.kind === "audio" ? "Voice message" : "Video";
|
|
5580
|
+
if (attachment.status === "pending") {
|
|
5581
|
+
return appendReference(originalBody, `[${label} pending: ${name}]`);
|
|
5582
|
+
}
|
|
5583
|
+
return appendReference(originalBody, `[${label} unavailable: ${name} (${attachment.status})]`);
|
|
5584
|
+
}
|
|
5585
|
+
if (attachment.kind === "file") {
|
|
5586
|
+
const prefix = originalBody.trim() === name ? "" : originalBody;
|
|
5587
|
+
return appendReference(prefix, `[File: ${name}]
|
|
5588
|
+
[Local file: ${attachment.path}]`);
|
|
5589
|
+
}
|
|
5590
|
+
if (exposeNonVisualPath) {
|
|
5591
|
+
if (attachment.kind === "audio") {
|
|
5592
|
+
return appendReference(originalBody || "<media:audio>", `[Voice message file: ${attachment.path}]`);
|
|
5593
|
+
}
|
|
5594
|
+
if (attachment.kind === "video") {
|
|
5595
|
+
return appendReference(originalBody || "<media:video>", `[Video file: ${attachment.path}]`);
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
if (!originalBody) return attachment.kind === "image" ? "<media:image>" : originalBody;
|
|
5599
|
+
return originalBody;
|
|
5600
|
+
}
|
|
5601
|
+
function applyLiveAttachment(message, originalBody, attachment) {
|
|
5602
|
+
const attachable = attachment?.status === "ready" && attachment.path && attachment.kind !== "file";
|
|
5603
|
+
return {
|
|
5604
|
+
...message,
|
|
5605
|
+
rawBody: renderAttachmentBody(originalBody, attachment),
|
|
5606
|
+
attachment,
|
|
5607
|
+
mediaPath: attachable ? attachment.path : void 0,
|
|
5608
|
+
mediaMime: attachable ? attachment.mime : void 0,
|
|
5609
|
+
hasMedia: attachment !== void 0
|
|
5610
|
+
};
|
|
5611
|
+
}
|
|
5612
|
+
function applyCatchupAttachmentPolicy(messages) {
|
|
5613
|
+
let latestImage = -1;
|
|
5614
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
5615
|
+
const attachment = messages[index].attachment;
|
|
5616
|
+
if (attachment?.kind === "image" && attachment.status === "ready" && attachment.path) {
|
|
5617
|
+
latestImage = index;
|
|
5618
|
+
break;
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
return messages.map((message, index) => {
|
|
5622
|
+
const attachment = message.attachment;
|
|
5623
|
+
if (!attachment) return { ...message, mediaPath: void 0, mediaMime: void 0 };
|
|
5624
|
+
const selectImage = index === latestImage;
|
|
5625
|
+
let rawBody = renderAttachmentBody(message.rawBody, void 0);
|
|
5626
|
+
if (attachment.kind === "file") {
|
|
5627
|
+
rawBody = message.rawBody;
|
|
5628
|
+
} else if (!selectImage && attachment.status === "ready" && attachment.path) {
|
|
5629
|
+
const label = attachment.kind === "image" ? "Image file" : attachment.kind === "audio" ? "Voice message file" : "Video file";
|
|
5630
|
+
const reference = `[${label}: ${attachment.path}]`;
|
|
5631
|
+
rawBody = message.rawBody.includes(reference) ? message.rawBody : appendReference(message.rawBody, reference);
|
|
5632
|
+
}
|
|
5633
|
+
return {
|
|
5634
|
+
...message,
|
|
5635
|
+
rawBody,
|
|
5636
|
+
mediaPath: selectImage ? attachment.path : void 0,
|
|
5637
|
+
mediaMime: selectImage ? attachment.mime : void 0,
|
|
5638
|
+
hasMedia: attachment !== void 0
|
|
5639
|
+
};
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
function buildMediaSegments(messages) {
|
|
5643
|
+
const segments = [];
|
|
5644
|
+
let batch = [];
|
|
5645
|
+
let hasAttachedMedia = false;
|
|
5646
|
+
for (const message of messages) {
|
|
5647
|
+
if (message.mediaPath && hasAttachedMedia) {
|
|
5648
|
+
segments.push(batch);
|
|
5649
|
+
batch = [message];
|
|
5650
|
+
hasAttachedMedia = true;
|
|
5651
|
+
} else {
|
|
5652
|
+
batch.push(message);
|
|
5653
|
+
if (message.mediaPath) hasAttachedMedia = true;
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
if (batch.length > 0) segments.push(batch);
|
|
5657
|
+
return segments;
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
// src/attachment-store.ts
|
|
5661
|
+
import { createHash } from "node:crypto";
|
|
5662
|
+
import fs from "node:fs/promises";
|
|
5663
|
+
var AttachmentTooLargeError = class extends Error {
|
|
5664
|
+
};
|
|
5665
|
+
function decodedSize(base64) {
|
|
5666
|
+
if (!base64 || base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(base64)) {
|
|
5667
|
+
throw new Error("Invalid attachment encoding");
|
|
5668
|
+
}
|
|
5669
|
+
const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
|
|
5670
|
+
return base64.length / 4 * 3 - padding;
|
|
5671
|
+
}
|
|
5672
|
+
async function saveManagedAttachment(params) {
|
|
5673
|
+
const size = decodedSize(params.data);
|
|
5674
|
+
if (size > params.maxBytes) {
|
|
5675
|
+
throw new AttachmentTooLargeError(`Attachment exceeds configured ${params.maxBytes} byte limit`);
|
|
5676
|
+
}
|
|
5677
|
+
const buffer = Buffer.from(params.data, "base64");
|
|
5678
|
+
if (buffer.byteLength !== size) throw new Error("Attachment decoding length mismatch");
|
|
5679
|
+
const sha256 = createHash("sha256").update(buffer).digest("hex");
|
|
5680
|
+
const saved = await params.saveMediaBuffer(
|
|
5681
|
+
buffer,
|
|
5682
|
+
params.mime,
|
|
5683
|
+
"inbound",
|
|
5684
|
+
params.maxBytes,
|
|
5685
|
+
params.filename
|
|
5686
|
+
);
|
|
5687
|
+
return { path: saved.path, size, sha256, filename: params.filename };
|
|
5688
|
+
}
|
|
5689
|
+
async function attachmentPathExists(filePath) {
|
|
5690
|
+
if (!filePath) return false;
|
|
5691
|
+
try {
|
|
5692
|
+
return (await fs.stat(filePath)).isFile();
|
|
5693
|
+
} catch {
|
|
5694
|
+
return false;
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5698
|
+
// src/monitor-state.ts
|
|
5699
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
5700
|
+
import fs2 from "node:fs/promises";
|
|
5701
|
+
import path from "node:path";
|
|
5702
|
+
var emptyState = () => ({
|
|
5703
|
+
version: 1,
|
|
5704
|
+
lastSeen: /* @__PURE__ */ Object.create(null),
|
|
5705
|
+
groupHistory: /* @__PURE__ */ Object.create(null)
|
|
5706
|
+
});
|
|
5707
|
+
var MonitorStateStore = class {
|
|
5708
|
+
filePath;
|
|
5709
|
+
constructor(stateDir, accountId) {
|
|
5710
|
+
const account = createHash2("sha256").update(accountId).digest("hex").slice(0, 16);
|
|
5711
|
+
this.filePath = path.join(stateDir, "agent-wechat", "monitor", `${account}.json`);
|
|
5712
|
+
}
|
|
5713
|
+
async load(validateItem) {
|
|
5714
|
+
let parsed;
|
|
5715
|
+
try {
|
|
5716
|
+
parsed = JSON.parse(await fs2.readFile(this.filePath, "utf8"));
|
|
5717
|
+
} catch (error) {
|
|
5718
|
+
if (error.code === "ENOENT") return emptyState();
|
|
5719
|
+
throw error;
|
|
5720
|
+
}
|
|
5721
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) {
|
|
5722
|
+
throw new Error("Unsupported WeChat monitor state");
|
|
5723
|
+
}
|
|
5724
|
+
const input = parsed;
|
|
5725
|
+
const state = emptyState();
|
|
5726
|
+
if (input.lastSeen && typeof input.lastSeen === "object") {
|
|
5727
|
+
for (const [chatId, localId] of Object.entries(input.lastSeen)) {
|
|
5728
|
+
if (typeof localId === "number" && Number.isSafeInteger(localId) && localId >= 0) {
|
|
5729
|
+
state.lastSeen[chatId] = localId;
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
if (input.groupHistory && typeof input.groupHistory === "object") {
|
|
5734
|
+
for (const [chatId, items] of Object.entries(input.groupHistory)) {
|
|
5735
|
+
if (Array.isArray(items) && items.every(validateItem)) state.groupHistory[chatId] = items;
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
5738
|
+
return state;
|
|
5739
|
+
}
|
|
5740
|
+
async save(lastSeen, groupHistory) {
|
|
5741
|
+
const directory = path.dirname(this.filePath);
|
|
5742
|
+
await fs2.mkdir(directory, { recursive: true, mode: 448 });
|
|
5743
|
+
const state = {
|
|
5744
|
+
version: 1,
|
|
5745
|
+
lastSeen: Object.fromEntries(lastSeen),
|
|
5746
|
+
groupHistory: Object.fromEntries(groupHistory)
|
|
5747
|
+
};
|
|
5748
|
+
const temporary = path.join(directory, `.${path.basename(this.filePath)}.${randomUUID()}.tmp`);
|
|
5749
|
+
try {
|
|
5750
|
+
await fs2.writeFile(temporary, JSON.stringify(state), { flag: "wx", mode: 384 });
|
|
5751
|
+
await fs2.rename(temporary, this.filePath);
|
|
5752
|
+
} finally {
|
|
5753
|
+
await fs2.rm(temporary, { force: true }).catch(() => void 0);
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
};
|
|
5757
|
+
|
|
5758
|
+
// src/polling.ts
|
|
5759
|
+
async function listAllChats(client) {
|
|
5760
|
+
const pageSize = 100;
|
|
5761
|
+
const chats = [];
|
|
5762
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
5763
|
+
const page = await client.listChats(pageSize, offset);
|
|
5764
|
+
chats.push(...page);
|
|
5765
|
+
if (page.length < pageSize) return chats;
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
async function listMessageWindow(client, chatId, previousLastSeen, firstPoll, unreadCount) {
|
|
5769
|
+
const pageSize = 200;
|
|
5770
|
+
const messages = [];
|
|
5771
|
+
const firstPollTarget = Math.max(unreadCount, 20);
|
|
5772
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
5773
|
+
const page = await client.listMessages(chatId, pageSize, offset);
|
|
5774
|
+
messages.push(...page);
|
|
5775
|
+
if (page.length < pageSize) break;
|
|
5776
|
+
if (firstPoll && messages.length >= firstPollTarget) break;
|
|
5777
|
+
if (!firstPoll && page.some((message) => message.localId <= previousLastSeen)) break;
|
|
5778
|
+
}
|
|
5779
|
+
return messages;
|
|
5780
|
+
}
|
|
5781
|
+
|
|
5516
5782
|
// src/access-control.ts
|
|
5517
5783
|
import {
|
|
5518
5784
|
buildChannelKeyCandidates,
|
|
@@ -5721,9 +5987,13 @@ function resolveWeChatMentionGate(params) {
|
|
|
5721
5987
|
}
|
|
5722
5988
|
|
|
5723
5989
|
// src/monitor.ts
|
|
5724
|
-
var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34, 43]);
|
|
5725
5990
|
var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
|
|
5726
5991
|
var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
|
|
5992
|
+
function isProcessedMessage(value) {
|
|
5993
|
+
if (!value || typeof value !== "object") return false;
|
|
5994
|
+
const message = value;
|
|
5995
|
+
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";
|
|
5996
|
+
}
|
|
5727
5997
|
function isOfficialAccount(chatId) {
|
|
5728
5998
|
return chatId.startsWith("gh_");
|
|
5729
5999
|
}
|
|
@@ -5737,10 +6007,12 @@ function sleep(ms, signal) {
|
|
|
5737
6007
|
});
|
|
5738
6008
|
}
|
|
5739
6009
|
async function pollMedia(client, chatId, localId, log, maxAttempts = 15, intervalMs = 1e3) {
|
|
6010
|
+
let lastResult = null;
|
|
5740
6011
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5741
|
-
const result = await client.getMedia(chatId, localId);
|
|
6012
|
+
const result = await client.getMedia(chatId, localId, "full");
|
|
6013
|
+
lastResult = result;
|
|
5742
6014
|
if (result.type === "unsupported") {
|
|
5743
|
-
return
|
|
6015
|
+
return result;
|
|
5744
6016
|
}
|
|
5745
6017
|
if (result.data) {
|
|
5746
6018
|
return result;
|
|
@@ -5750,7 +6022,7 @@ async function pollMedia(client, chatId, localId, log, maxAttempts = 15, interva
|
|
|
5750
6022
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
5751
6023
|
}
|
|
5752
6024
|
}
|
|
5753
|
-
return
|
|
6025
|
+
return lastResult;
|
|
5754
6026
|
}
|
|
5755
6027
|
function enqueueWeChatSystemEvent(text, contextKey) {
|
|
5756
6028
|
try {
|
|
@@ -5762,12 +6034,72 @@ function enqueueWeChatSystemEvent(text, contextKey) {
|
|
|
5762
6034
|
} catch {
|
|
5763
6035
|
}
|
|
5764
6036
|
}
|
|
6037
|
+
async function retrieveAttachment(client, chatId, msg, liveAccount, log, maxAttempts = 15) {
|
|
6038
|
+
const baseType = msg.type & 2147483647;
|
|
6039
|
+
const expectedKind = attachmentKindForMessageType(baseType);
|
|
6040
|
+
if (!expectedKind) return void 0;
|
|
6041
|
+
const fallbackFilename = attachmentFallbackFilename(expectedKind, msg.localId, msg.content);
|
|
6042
|
+
try {
|
|
6043
|
+
const result = await pollMedia(client, chatId, msg.localId, log, maxAttempts);
|
|
6044
|
+
if (!result) {
|
|
6045
|
+
return { kind: expectedKind, status: "unavailable", filename: fallbackFilename };
|
|
6046
|
+
}
|
|
6047
|
+
if (result.type === "unsupported") {
|
|
6048
|
+
if (baseType === 49) return void 0;
|
|
6049
|
+
return { kind: expectedKind, status: "unsupported", filename: fallbackFilename };
|
|
6050
|
+
}
|
|
6051
|
+
const kind = attachmentKindForResult(result, expectedKind);
|
|
6052
|
+
const filename = result.filename || fallbackFilename;
|
|
6053
|
+
if (!result.data) {
|
|
6054
|
+
return { kind, status: result.type === "pending" ? "pending" : "unavailable", filename };
|
|
6055
|
+
}
|
|
6056
|
+
const mime = mediaMime(result.format, kind);
|
|
6057
|
+
const saved = await saveManagedAttachment({
|
|
6058
|
+
data: result.data,
|
|
6059
|
+
mime,
|
|
6060
|
+
filename,
|
|
6061
|
+
maxBytes: liveAccount.mediaMaxMb * 1024 * 1024,
|
|
6062
|
+
saveMediaBuffer: getWeChatRuntime().channel.media.saveMediaBuffer
|
|
6063
|
+
});
|
|
6064
|
+
log?.info?.(
|
|
6065
|
+
`[wechat:${liveAccount.accountId}] Saved ${kind} attachment for msg ${msg.localId} (${saved.size} bytes)`
|
|
6066
|
+
);
|
|
6067
|
+
return { kind, status: "ready", filename: saved.filename, mime, path: saved.path };
|
|
6068
|
+
} catch (error) {
|
|
6069
|
+
const status = error instanceof AttachmentTooLargeError ? "too_large" : "error";
|
|
6070
|
+
log?.error?.(
|
|
6071
|
+
`[wechat:${liveAccount.accountId}] Attachment retrieval failed for msg ${msg.localId} (${status})`
|
|
6072
|
+
);
|
|
6073
|
+
return {
|
|
6074
|
+
kind: expectedKind,
|
|
6075
|
+
status,
|
|
6076
|
+
filename: fallbackFilename,
|
|
6077
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6078
|
+
};
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
5765
6081
|
async function startWeChatMonitor(opts) {
|
|
5766
6082
|
const { account, abortSignal, setStatus, log } = opts;
|
|
5767
6083
|
const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
|
|
5768
|
-
const
|
|
5769
|
-
const
|
|
6084
|
+
const runtimeStateDir = getWeChatRuntime().state.resolveStateDir(process.env);
|
|
6085
|
+
const stateStore = new MonitorStateStore(runtimeStateDir, account.accountId);
|
|
6086
|
+
let savedState;
|
|
6087
|
+
try {
|
|
6088
|
+
savedState = await stateStore.load(isProcessedMessage);
|
|
6089
|
+
} catch (error) {
|
|
6090
|
+
log?.error?.(`[wechat:${account.accountId}] Ignoring unreadable monitor state: ${String(error)}`);
|
|
6091
|
+
savedState = { version: 1, lastSeen: {}, groupHistory: {} };
|
|
6092
|
+
}
|
|
6093
|
+
const lastSeenId = new Map(Object.entries(savedState.lastSeen));
|
|
6094
|
+
const groupHistory = new Map(Object.entries(savedState.groupHistory));
|
|
5770
6095
|
const GROUP_HISTORY_LIMIT = 50;
|
|
6096
|
+
const persistState = async () => {
|
|
6097
|
+
try {
|
|
6098
|
+
await stateStore.save(lastSeenId, groupHistory);
|
|
6099
|
+
} catch (error) {
|
|
6100
|
+
log?.error?.(`[wechat:${account.accountId}] Failed to persist monitor state: ${String(error)}`);
|
|
6101
|
+
}
|
|
6102
|
+
};
|
|
5771
6103
|
let lastAuthCheck = 0;
|
|
5772
6104
|
let prevStatus = void 0;
|
|
5773
6105
|
setStatus({
|
|
@@ -5831,7 +6163,7 @@ async function startWeChatMonitor(opts) {
|
|
|
5831
6163
|
}
|
|
5832
6164
|
let chats;
|
|
5833
6165
|
try {
|
|
5834
|
-
chats = await client
|
|
6166
|
+
chats = await listAllChats(client);
|
|
5835
6167
|
} catch (err) {
|
|
5836
6168
|
log?.error?.(
|
|
5837
6169
|
`[wechat:${account.accountId}] Failed to list chats: ${err}`
|
|
@@ -5859,7 +6191,8 @@ async function startWeChatMonitor(opts) {
|
|
|
5859
6191
|
log,
|
|
5860
6192
|
void 0,
|
|
5861
6193
|
groupHistory,
|
|
5862
|
-
GROUP_HISTORY_LIMIT
|
|
6194
|
+
GROUP_HISTORY_LIMIT,
|
|
6195
|
+
persistState
|
|
5863
6196
|
);
|
|
5864
6197
|
}
|
|
5865
6198
|
}
|
|
@@ -5874,7 +6207,7 @@ async function startWeChatMonitor(opts) {
|
|
|
5874
6207
|
log?.info?.(
|
|
5875
6208
|
`[wechat:${account.accountId}] Catch-up: ${chatId} lastMsgLocalId=${chat.lastMsgLocalId} > lastSeenId=${prevSeen}`
|
|
5876
6209
|
);
|
|
5877
|
-
await processUnreadChat(client, chat, lastSeenId, account, cfg, log, true, groupHistory, GROUP_HISTORY_LIMIT);
|
|
6210
|
+
await processUnreadChat(client, chat, lastSeenId, account, cfg, log, true, groupHistory, GROUP_HISTORY_LIMIT, persistState);
|
|
5878
6211
|
}
|
|
5879
6212
|
} catch (err) {
|
|
5880
6213
|
log?.error?.(
|
|
@@ -5890,7 +6223,6 @@ async function startWeChatMonitor(opts) {
|
|
|
5890
6223
|
});
|
|
5891
6224
|
}
|
|
5892
6225
|
async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, log) {
|
|
5893
|
-
const core = getWeChatRuntime();
|
|
5894
6226
|
if (msg.isSelf) {
|
|
5895
6227
|
log?.info?.(`[wechat:${liveAccount.accountId}] Skipping self-sent msg ${msg.localId}`);
|
|
5896
6228
|
return null;
|
|
@@ -5910,113 +6242,68 @@ async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, lo
|
|
|
5910
6242
|
);
|
|
5911
6243
|
return null;
|
|
5912
6244
|
}
|
|
5913
|
-
let mediaPath;
|
|
5914
|
-
let mediaMime;
|
|
5915
|
-
let hasMedia = false;
|
|
5916
6245
|
const baseType = msg.type & 2147483647;
|
|
5917
|
-
const
|
|
5918
|
-
|
|
6246
|
+
const messageAttachmentKind = attachmentKindForMessageType(baseType);
|
|
6247
|
+
let attachment;
|
|
6248
|
+
if (messageAttachmentKind) {
|
|
5919
6249
|
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
|
-
}
|
|
6250
|
+
attachment = await retrieveAttachment(client, chatId, msg, liveAccount, log);
|
|
5959
6251
|
}
|
|
5960
6252
|
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
|
-
}
|
|
6253
|
+
let sourceBody = attachmentSourceBody(msg.content || "", messageAttachmentKind);
|
|
5975
6254
|
if (msg.reply) {
|
|
5976
6255
|
const replySender = msg.reply.sender ?? "unknown sender";
|
|
5977
6256
|
const quotedBody = msg.reply.content.length > 50 ? msg.reply.content.slice(0, 50) + "..." : msg.reply.content;
|
|
5978
6257
|
const replyBlock = `[Replying to ${replySender}]
|
|
5979
6258
|
${quotedBody}
|
|
5980
6259
|
[/Replying]`;
|
|
5981
|
-
|
|
6260
|
+
sourceBody = sourceBody ? `${sourceBody}
|
|
5982
6261
|
|
|
5983
6262
|
${replyBlock}` : replyBlock;
|
|
5984
6263
|
}
|
|
5985
|
-
|
|
6264
|
+
const prepared = applyLiveAttachment({
|
|
5986
6265
|
msg,
|
|
5987
|
-
rawBody,
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
wasMentioned
|
|
5991
|
-
}),
|
|
5992
|
-
mediaPath,
|
|
5993
|
-
mediaMime,
|
|
6266
|
+
rawBody: sourceBody,
|
|
6267
|
+
sourceBody,
|
|
6268
|
+
commandBody: "",
|
|
5994
6269
|
senderName,
|
|
5995
6270
|
senderId,
|
|
5996
6271
|
isGroup,
|
|
5997
6272
|
timestamp,
|
|
5998
|
-
hasMedia,
|
|
6273
|
+
hasMedia: false,
|
|
5999
6274
|
isMentioned: wasMentioned
|
|
6000
|
-
};
|
|
6275
|
+
}, sourceBody, attachment);
|
|
6276
|
+
prepared.commandBody = normalizeWeChatCommandBody(prepared.rawBody, {
|
|
6277
|
+
isGroup,
|
|
6278
|
+
wasMentioned
|
|
6279
|
+
});
|
|
6280
|
+
return prepared;
|
|
6001
6281
|
}
|
|
6002
|
-
function
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6282
|
+
async function refreshAttachment(message, client, chatId, liveAccount, log) {
|
|
6283
|
+
if (!message.attachment) return message;
|
|
6284
|
+
if (message.attachment.status === "ready" && await attachmentPathExists(message.attachment.path)) {
|
|
6285
|
+
return message;
|
|
6286
|
+
}
|
|
6287
|
+
log?.info?.(`[wechat:${liveAccount.accountId}] Retrying attachment for msg ${message.msg.localId}`);
|
|
6288
|
+
const attachment = await retrieveAttachment(client, chatId, message.msg, liveAccount, log, 3);
|
|
6289
|
+
const refreshed = applyLiveAttachment(message, message.sourceBody, attachment);
|
|
6290
|
+
refreshed.commandBody = normalizeWeChatCommandBody(refreshed.rawBody, {
|
|
6291
|
+
isGroup: refreshed.isGroup,
|
|
6292
|
+
wasMentioned: refreshed.isMentioned
|
|
6293
|
+
});
|
|
6294
|
+
return refreshed;
|
|
6295
|
+
}
|
|
6296
|
+
async function refreshAttachments(messages, client, chatId, liveAccount, log) {
|
|
6297
|
+
const output = [...messages];
|
|
6298
|
+
let cursor = 0;
|
|
6299
|
+
const worker = async () => {
|
|
6300
|
+
while (cursor < messages.length) {
|
|
6301
|
+
const index = cursor++;
|
|
6302
|
+
output[index] = await refreshAttachment(messages[index], client, chatId, liveAccount, log);
|
|
6014
6303
|
}
|
|
6015
|
-
}
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
}
|
|
6019
|
-
return segments;
|
|
6304
|
+
};
|
|
6305
|
+
await Promise.all(Array.from({ length: Math.min(3, messages.length) }, worker));
|
|
6306
|
+
return output;
|
|
6020
6307
|
}
|
|
6021
6308
|
async function dispatchSegment(segment, client, chatId, chat, liveAccount, policy, storeAllowFrom, allowTextCommands, cfg, log, remainingSegments) {
|
|
6022
6309
|
const core = getWeChatRuntime();
|
|
@@ -6024,9 +6311,18 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6024
6311
|
const { isGroup, senderId, senderName, timestamp, rawBody, commandBody, msg } = lastMsg;
|
|
6025
6312
|
const mediaMsg = segment.find((pm) => pm.mediaPath);
|
|
6026
6313
|
const mediaPath = mediaMsg?.mediaPath;
|
|
6027
|
-
const
|
|
6314
|
+
const mediaMime2 = mediaMsg?.mediaMime;
|
|
6315
|
+
const attachmentPaths = segment.flatMap(
|
|
6316
|
+
(pm) => pm.attachment?.status === "ready" && pm.attachment.path ? [{
|
|
6317
|
+
path: pm.attachment.path,
|
|
6318
|
+
filename: pm.attachment.filename,
|
|
6319
|
+
kind: pm.attachment.kind,
|
|
6320
|
+
messageId: pm.msg.localId,
|
|
6321
|
+
sender: pm.senderName
|
|
6322
|
+
}] : []
|
|
6323
|
+
);
|
|
6028
6324
|
log?.info?.(
|
|
6029
|
-
`[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ?
|
|
6325
|
+
`[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ? " with model media" : ""} attachments=${attachmentPaths.length}`
|
|
6030
6326
|
);
|
|
6031
6327
|
const hasControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(commandBody, cfg);
|
|
6032
6328
|
const commandAuthorized = await resolveWeChatCommandAuthorization({
|
|
@@ -6046,7 +6342,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6046
6342
|
log?.info?.(
|
|
6047
6343
|
`[wechat:${liveAccount.accountId}] Dropping unauthorized group control command from ${senderId} in ${chatId}`
|
|
6048
6344
|
);
|
|
6049
|
-
return
|
|
6345
|
+
return "ignored";
|
|
6050
6346
|
}
|
|
6051
6347
|
const mentionGate = resolveWeChatMentionGate({
|
|
6052
6348
|
isGroup,
|
|
@@ -6061,7 +6357,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6061
6357
|
log?.info?.(
|
|
6062
6358
|
`[wechat:${liveAccount.accountId}] Skipping group segment (mention required) in ${chatId}`
|
|
6063
6359
|
);
|
|
6064
|
-
return
|
|
6360
|
+
return "ignored";
|
|
6065
6361
|
}
|
|
6066
6362
|
try {
|
|
6067
6363
|
const route = core.channel.routing.resolveAgentRoute({
|
|
@@ -6153,7 +6449,8 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6153
6449
|
CommandAuthorized: commandAuthorized,
|
|
6154
6450
|
OriginatingChannel: "agent-wechat",
|
|
6155
6451
|
OriginatingTo: `wechat:${chatId}`,
|
|
6156
|
-
|
|
6452
|
+
Attachments: attachmentPaths,
|
|
6453
|
+
...mediaPath ? { MediaPath: mediaPath, MediaUrl: mediaPath, MediaType: mediaMime2 } : {},
|
|
6157
6454
|
...msg.reply ? {
|
|
6158
6455
|
ReplyToBody: msg.reply.content.length > 50 ? msg.reply.content.slice(0, 50) + "..." : msg.reply.content,
|
|
6159
6456
|
ReplyToSender: msg.reply.sender
|
|
@@ -6175,6 +6472,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6175
6472
|
channel: "agent-wechat",
|
|
6176
6473
|
accountId: liveAccount.accountId
|
|
6177
6474
|
});
|
|
6475
|
+
let replyFailed = false;
|
|
6178
6476
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
6179
6477
|
ctx: ctxPayload,
|
|
6180
6478
|
cfg,
|
|
@@ -6238,6 +6536,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6238
6536
|
}
|
|
6239
6537
|
},
|
|
6240
6538
|
onError: (err, info) => {
|
|
6539
|
+
replyFailed = true;
|
|
6241
6540
|
log?.error?.(
|
|
6242
6541
|
`[wechat:${liveAccount.accountId}] ${info.kind} reply failed: ${String(err)}`
|
|
6243
6542
|
);
|
|
@@ -6247,18 +6546,19 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
|
|
|
6247
6546
|
onModelSelected
|
|
6248
6547
|
}
|
|
6249
6548
|
});
|
|
6549
|
+
if (replyFailed) return "failed";
|
|
6250
6550
|
core.channel.activity?.record?.({
|
|
6251
6551
|
channel: "agent-wechat",
|
|
6252
6552
|
accountId: liveAccount.accountId,
|
|
6253
6553
|
direction: "inbound",
|
|
6254
6554
|
at: timestamp
|
|
6255
6555
|
});
|
|
6256
|
-
return
|
|
6556
|
+
return "delivered";
|
|
6257
6557
|
} catch (err) {
|
|
6258
6558
|
log?.error?.(
|
|
6259
6559
|
`[wechat:${liveAccount.accountId}] Failed to dispatch segment (last msg ${msg.localId}): ${err}`
|
|
6260
6560
|
);
|
|
6261
|
-
return
|
|
6561
|
+
return "failed";
|
|
6262
6562
|
}
|
|
6263
6563
|
}
|
|
6264
6564
|
function bufferGroupHistory(groupHistory, chatId, pm, limit) {
|
|
@@ -6274,7 +6574,7 @@ function bufferGroupHistory(groupHistory, chatId, pm, limit) {
|
|
|
6274
6574
|
if (first) groupHistory.delete(first);
|
|
6275
6575
|
}
|
|
6276
6576
|
}
|
|
6277
|
-
async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit) {
|
|
6577
|
+
async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, persistState) {
|
|
6278
6578
|
const core = getWeChatRuntime();
|
|
6279
6579
|
const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
|
|
6280
6580
|
const chatId = chat.username ?? chat.id;
|
|
@@ -6292,8 +6592,12 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6292
6592
|
if (!skipOpen) {
|
|
6293
6593
|
log?.info?.(`[wechat:${liveAccount.accountId}] Opening chat ${chatId}...`);
|
|
6294
6594
|
try {
|
|
6295
|
-
await client.openChat(chatId, true);
|
|
6296
|
-
|
|
6595
|
+
const opened = await client.openChat(chatId, true);
|
|
6596
|
+
if (opened.ok) {
|
|
6597
|
+
log?.info?.(`[wechat:${liveAccount.accountId}] Opened chat ${chatId}`);
|
|
6598
|
+
} else {
|
|
6599
|
+
log?.error?.(`[wechat:${liveAccount.accountId}] Chat selection did not complete for ${chatId}: ${opened.error ?? "unknown error"}`);
|
|
6600
|
+
}
|
|
6297
6601
|
} catch (err) {
|
|
6298
6602
|
log?.error?.(
|
|
6299
6603
|
`[wechat:${liveAccount.accountId}] Failed to open chat ${chatId}: ${err}`
|
|
@@ -6302,10 +6606,9 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6302
6606
|
}
|
|
6303
6607
|
const firstPoll = !lastSeenId.has(chatId);
|
|
6304
6608
|
const prevLastSeen = lastSeenId.get(chatId) ?? 0;
|
|
6305
|
-
const fetchLimit = Math.max(chat.unreadCount, 20);
|
|
6306
6609
|
let messages;
|
|
6307
6610
|
try {
|
|
6308
|
-
messages = await client
|
|
6611
|
+
messages = await listMessageWindow(client, chatId, prevLastSeen, firstPoll, chat.unreadCount ?? 0);
|
|
6309
6612
|
} catch (err) {
|
|
6310
6613
|
log?.error?.(
|
|
6311
6614
|
`[wechat:${liveAccount.accountId}] Failed to list messages for ${chatId}: ${err}`
|
|
@@ -6324,11 +6627,13 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6324
6627
|
newMessages = messages.slice(-unread);
|
|
6325
6628
|
const seenMax = messages[messages.length - unread - 1].localId;
|
|
6326
6629
|
lastSeenId.set(chatId, seenMax);
|
|
6630
|
+
await persistState?.();
|
|
6327
6631
|
} else if (unread >= messages.length) {
|
|
6328
6632
|
newMessages = messages;
|
|
6329
6633
|
} else {
|
|
6330
6634
|
const maxId2 = messages[messages.length - 1].localId;
|
|
6331
6635
|
lastSeenId.set(chatId, maxId2);
|
|
6636
|
+
await persistState?.();
|
|
6332
6637
|
return;
|
|
6333
6638
|
}
|
|
6334
6639
|
} else {
|
|
@@ -6344,7 +6649,7 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6344
6649
|
const processed = [];
|
|
6345
6650
|
for (const msg of newMessages) {
|
|
6346
6651
|
log?.info?.(
|
|
6347
|
-
`[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}
|
|
6652
|
+
`[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}`
|
|
6348
6653
|
);
|
|
6349
6654
|
const pm = await prepareMessage(client, msg, chatId, chat, liveAccount, policy, log);
|
|
6350
6655
|
if (pm) {
|
|
@@ -6365,51 +6670,35 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6365
6670
|
log?.info?.(`[wechat:${liveAccount.accountId}] Buffered ${processed.length} msg(s) for group history in ${chatId}`);
|
|
6366
6671
|
const maxId2 = Math.max(...newMessages.map((m) => m.localId));
|
|
6367
6672
|
lastSeenId.set(chatId, maxId2);
|
|
6673
|
+
await persistState?.();
|
|
6368
6674
|
return;
|
|
6369
6675
|
}
|
|
6370
6676
|
if (hasMention) {
|
|
6371
6677
|
clearBufferedHistory = true;
|
|
6372
6678
|
const buffered = groupHistory.get(chatId) ?? [];
|
|
6373
6679
|
if (buffered.length > 0) {
|
|
6374
|
-
|
|
6375
|
-
pm.isMentioned = true;
|
|
6376
|
-
}
|
|
6377
|
-
processed.unshift(...buffered);
|
|
6680
|
+
processed.unshift(...buffered.map((pm) => ({ ...pm, isMentioned: true })));
|
|
6378
6681
|
log?.info?.(
|
|
6379
6682
|
`[wechat:${liveAccount.accountId}] Injected ${buffered.length} buffered msg(s) as history in ${chatId}`
|
|
6380
6683
|
);
|
|
6381
6684
|
}
|
|
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
|
-
}
|
|
6685
|
+
processed.splice(0, processed.length, ...await refreshAttachments(processed, client, chatId, liveAccount, log));
|
|
6686
|
+
processed.splice(0, processed.length, ...applyCatchupAttachmentPolicy(processed));
|
|
6399
6687
|
}
|
|
6400
6688
|
} else {
|
|
6401
6689
|
clearBufferedHistory = true;
|
|
6402
6690
|
}
|
|
6403
6691
|
}
|
|
6404
6692
|
if (processed.length > 0) {
|
|
6405
|
-
const segments = hasControlCommandInWindow ? processed.map((pm) => [pm]) :
|
|
6693
|
+
const segments = hasControlCommandInWindow ? processed.map((pm) => [pm]) : buildMediaSegments(processed);
|
|
6406
6694
|
log?.info?.(
|
|
6407
6695
|
`[wechat:${liveAccount.accountId}] ${chatId}: ${processed.length} dispatchable msg(s) in ${segments.length} segment(s)`
|
|
6408
6696
|
);
|
|
6409
6697
|
let allDispatched = true;
|
|
6698
|
+
let allHandled = true;
|
|
6410
6699
|
for (let i = 0; i < segments.length; i++) {
|
|
6411
6700
|
const remaining = segments.length - i - 1;
|
|
6412
|
-
const
|
|
6701
|
+
const outcome = await dispatchSegment(
|
|
6413
6702
|
segments[i],
|
|
6414
6703
|
client,
|
|
6415
6704
|
chatId,
|
|
@@ -6422,16 +6711,22 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
|
|
|
6422
6711
|
log,
|
|
6423
6712
|
hasControlCommandInWindow ? void 0 : remaining
|
|
6424
6713
|
);
|
|
6425
|
-
if (
|
|
6714
|
+
if (outcome !== "delivered") {
|
|
6426
6715
|
allDispatched = false;
|
|
6427
6716
|
}
|
|
6717
|
+
if (outcome === "failed") allHandled = false;
|
|
6428
6718
|
}
|
|
6429
6719
|
if (clearBufferedHistory && allDispatched && groupHistory) {
|
|
6430
6720
|
groupHistory.set(chatId, []);
|
|
6431
6721
|
}
|
|
6722
|
+
if (!allHandled) {
|
|
6723
|
+
await persistState?.();
|
|
6724
|
+
return;
|
|
6725
|
+
}
|
|
6432
6726
|
}
|
|
6433
6727
|
const maxId = Math.max(...newMessages.map((m) => m.localId));
|
|
6434
6728
|
lastSeenId.set(chatId, maxId);
|
|
6729
|
+
await persistState?.();
|
|
6435
6730
|
}
|
|
6436
6731
|
|
|
6437
6732
|
// src/status.ts
|
|
@@ -6926,7 +7221,8 @@ var wechatPlugin = {
|
|
|
6926
7221
|
}
|
|
6927
7222
|
},
|
|
6928
7223
|
pollIntervalMs: { type: "integer", minimum: 100 },
|
|
6929
|
-
authPollIntervalMs: { type: "integer", minimum: 1e3 }
|
|
7224
|
+
authPollIntervalMs: { type: "integer", minimum: 1e3 },
|
|
7225
|
+
mediaMaxMb: { type: "integer", minimum: 1, maximum: 1024 }
|
|
6930
7226
|
}
|
|
6931
7227
|
}
|
|
6932
7228
|
},
|
|
@@ -6950,7 +7246,8 @@ var wechatPlugin = {
|
|
|
6950
7246
|
groupAllowFrom: [],
|
|
6951
7247
|
groups: {},
|
|
6952
7248
|
pollIntervalMs: 1e3,
|
|
6953
|
-
authPollIntervalMs: 3e4
|
|
7249
|
+
authPollIntervalMs: 3e4,
|
|
7250
|
+
mediaMaxMb: 50
|
|
6954
7251
|
};
|
|
6955
7252
|
}
|
|
6956
7253
|
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.14.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
|
}
|