@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 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(path) {
1158
- const res = await fetch(`${this.base}${path}`, {
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(path, body) {
1166
- const res = await fetch(`${this.base}${path}`, {
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(path) {
1176
- const res = await fetch(`${this.base}${path}`, {
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 = [...path, ...issueData.path || []];
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, path, key) {
1897
+ constructor(parent, value, path2, key) {
1896
1898
  this._cachedPath = [];
1897
1899
  this.parent = parent;
1898
1900
  this.data = value;
1899
- this._path = 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 null;
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 null;
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 lastSeenId = /* @__PURE__ */ new Map();
5769
- const groupHistory = /* @__PURE__ */ new Map();
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.listChats(50);
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 mayHaveMedia = MEDIA_TYPES.has(baseType) || baseType === 49;
5918
- if (mayHaveMedia) {
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
- try {
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 rawBody = msg.content || "";
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
- rawBody = rawBody ? `${rawBody}
6260
+ sourceBody = sourceBody ? `${sourceBody}
5982
6261
 
5983
6262
  ${replyBlock}` : replyBlock;
5984
6263
  }
5985
- return {
6264
+ const prepared = applyLiveAttachment({
5986
6265
  msg,
5987
- rawBody,
5988
- commandBody: normalizeWeChatCommandBody(rawBody, {
5989
- isGroup,
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 buildSegments(processed) {
6003
- const segments = [];
6004
- let currentBatch = [];
6005
- let mediaCount = 0;
6006
- for (const pm of processed) {
6007
- if (pm.hasMedia && mediaCount >= 1) {
6008
- segments.push(currentBatch);
6009
- currentBatch = [pm];
6010
- mediaCount = 1;
6011
- } else {
6012
- if (pm.hasMedia) mediaCount++;
6013
- currentBatch.push(pm);
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
- if (currentBatch.length > 0) {
6017
- segments.push(currentBatch);
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 mediaMime = mediaMsg?.mediaMime;
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 ? ` media=${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 false;
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 false;
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
- ...mediaPath ? { MediaPath: mediaPath, MediaUrl: mediaPath, MediaType: mediaMime } : {},
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 true;
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 false;
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
- log?.info?.(`[wechat:${liveAccount.accountId}] Opened chat ${chatId}`);
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.listMessages(chatId, fetchLimit);
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}, content=${(msg.content || "").slice(0, 50)}`
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
- for (const pm of buffered) {
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
- let latestMediaIdx = -1;
6383
- for (let i = processed.length - 1; i >= 0; i--) {
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]) : buildSegments(processed);
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 dispatched = await dispatchSegment(
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 (!dispatched) {
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;
@@ -91,6 +91,11 @@
91
91
  "authPollIntervalMs": {
92
92
  "type": "integer",
93
93
  "minimum": 1000
94
+ },
95
+ "mediaMaxMb": {
96
+ "type": "integer",
97
+ "minimum": 1,
98
+ "maximum": 1024
94
99
  }
95
100
  }
96
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-wechat/agent-wechat",
3
- "version": "0.12.2",
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.1.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
  }