@kyan-du/agent-wechat-openclaw 0.12.0 → 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/README.md CHANGED
@@ -81,19 +81,19 @@ If running alongside OpenClaw on the same Docker network, set `serverUrl` to `ht
81
81
  # Pending P1-B (not available yet): openclaw plugins install @kyan-du/agent-wechat-openclaw
82
82
  pnpm --filter @kyan-du/agent-wechat-openclaw build
83
83
  node scripts/prepare-openclaw-plugin.mjs
84
- openclaw plugins install -l ./.artifacts/openclaw-extension
84
+ openclaw plugins install ./.artifacts/agent-wechat-openclaw.tgz
85
85
  ```
86
86
 
87
87
  ### 3. Configure the channel
88
88
 
89
- OpenClaw's channel catalog may map `openclaw channels add --channel wechat` to its official Weixin plugin rather than this source plugin. Configure the staged plugin through its schema keys and verify the runtime load explicitly:
89
+ This plugin registers as channel id `agent-wechat`. Do not use `wechat`, `weixin`, or `微信` — OpenClaw's official catalog maps those aliases to `@tencent-weixin/openclaw-weixin`, so outbound and cron delivery never reach this plugin.
90
90
 
91
91
  ```bash
92
- openclaw config set channels.wechat.enabled true
93
- openclaw config set channels.wechat.serverUrl http://localhost:6174
92
+ openclaw config set channels.agent-wechat.enabled true
93
+ openclaw config set channels.agent-wechat.serverUrl http://localhost:6174
94
94
  # Optional when the server requires an explicit token:
95
- openclaw config set channels.wechat.token <token>
96
- openclaw plugins inspect wechat --runtime
95
+ openclaw config set channels.agent-wechat.token <token>
96
+ openclaw plugins inspect agent-wechat --runtime
97
97
  ```
98
98
 
99
99
  Or edit `~/.openclaw/openclaw.json` directly:
@@ -101,7 +101,7 @@ Or edit `~/.openclaw/openclaw.json` directly:
101
101
  ```json
102
102
  {
103
103
  "channels": {
104
- "wechat": {
104
+ "agent-wechat": {
105
105
  "enabled": true,
106
106
  "serverUrl": "http://localhost:6174",
107
107
  "dmPolicy": "open",
@@ -130,7 +130,7 @@ Ask your bot to log in to WeChat:
130
130
  Your bot should generate a QR code image. Alternatively, use the CLI:
131
131
 
132
132
  ```bash
133
- openclaw channels login --channel wechat
133
+ openclaw channels login --channel agent-wechat
134
134
  ```
135
135
 
136
136
  ### 6. Scan the QR code
@@ -155,7 +155,7 @@ Once connected, configure how the bot handles direct messages and group chats. Y
155
155
 
156
156
  ## Configuration Reference
157
157
 
158
- All config lives under `channels.wechat` in OpenClaw's config file:
158
+ All config lives under `channels.agent-wechat` in OpenClaw's config file. `channels.wechat` is still read as a one-release fallback.
159
159
 
160
160
  | Key | Type | Default | Description |
161
161
  |-----|------|---------|-------------|
@@ -185,14 +185,14 @@ cd agent-wechat
185
185
  pnpm install && pnpm build
186
186
  ```
187
187
 
188
- ### Link for local development
188
+ ### Install a private source build
189
189
 
190
190
  ```bash
191
191
  node scripts/prepare-openclaw-plugin.mjs
192
- openclaw plugins install -l ./.artifacts/openclaw-extension
192
+ openclaw plugins install ./.artifacts/agent-wechat-openclaw.tgz
193
193
  ```
194
194
 
195
- This stages only the built plugin files and a production manifest, avoiding pnpm workspace symlinks that OpenClaw correctly rejects as escaping the install root. Rebuild and restage after making changes, then restart the gateway.
195
+ This creates a clean npm archive from the built plugin. OpenClaw installs its runtime dependencies, including the platform-specific `sharp` package, without relying on workspace-hoisted modules. Rebuild and repack after making changes, then restart the gateway.
196
196
 
197
197
  ## Architecture
198
198
 
package/dist/index.js CHANGED
@@ -1102,8 +1102,10 @@ var DEFAULT_CATCH_UP_MAX_AGE_MS = 5 * 6e4;
1102
1102
  var DEFAULT_CATCH_UP_CHAT_BUDGET = 5;
1103
1103
  var DEFAULT_MEDIA_PART_DELAY_MS = 750;
1104
1104
  var DEFAULT_ACCOUNT_ID = "default";
1105
+ var OPENCLAW_CHANNEL_ID = "agent-wechat";
1105
1106
  function resolveWeChatAccount(cfg, accountId) {
1106
- const wechat = cfg.channels?.wechat;
1107
+ const channels = cfg.channels;
1108
+ const wechat = channels?.[OPENCLAW_CHANNEL_ID] ?? channels?.wechat;
1107
1109
  if (!wechat?.serverUrl) return null;
1108
1110
  return {
1109
1111
  accountId: accountId ?? DEFAULT_ACCOUNT_ID,
@@ -6154,7 +6156,7 @@ function normalizeWeChatId(raw) {
6154
6156
  if (!trimmed) {
6155
6157
  return "";
6156
6158
  }
6157
- return trimmed.replace(/^wechat:/i, "").trim();
6159
+ return trimmed.replace(/^(agent-wechat|wechat):/i, "").trim();
6158
6160
  }
6159
6161
  function normalizeWeChatAllowFrom(values) {
6160
6162
  const normalized = (values ?? []).map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry === "*" ? "*" : normalizeWeChatId(entry)).filter(Boolean);
@@ -6240,7 +6242,7 @@ function resolveWeChatPolicyContext(params) {
6240
6242
  const requireMention = firstDefined(groupEntry?.requireMention, wildcardEntry?.requireMention, true) !== false;
6241
6243
  const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
6242
6244
  const { groupPolicy: fallbackGroupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
6243
- providerConfigPresent: params.cfg.channels?.wechat !== void 0,
6245
+ providerConfigPresent: params.cfg.channels?.["agent-wechat"] !== void 0 || params.cfg.channels?.wechat !== void 0,
6244
6246
  groupPolicy: normalizeGroupPolicy2(params.account.groupPolicy),
6245
6247
  defaultGroupPolicy: normalizeGroupPolicy2(defaultGroupPolicy)
6246
6248
  });
@@ -6355,6 +6357,310 @@ function nextReconnectState(input) {
6355
6357
  return { reconnect: true, dispatched: input.dispatched };
6356
6358
  }
6357
6359
 
6360
+ // src/inbound-media.ts
6361
+ import sharp from "sharp";
6362
+ var MIME_BY_FORMAT = {
6363
+ jpeg: "image/jpeg",
6364
+ jpg: "image/jpeg",
6365
+ png: "image/png",
6366
+ webp: "image/webp",
6367
+ gif: "image/gif",
6368
+ mp3: "audio/mpeg",
6369
+ pdf: "application/pdf",
6370
+ doc: "application/msword",
6371
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
6372
+ xls: "application/vnd.ms-excel",
6373
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
6374
+ ppt: "application/vnd.ms-powerpoint",
6375
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
6376
+ zip: "application/zip",
6377
+ txt: "text/plain"
6378
+ };
6379
+ var MAX_IMAGE_BYTES = 20 * 1024 * 1024;
6380
+ var MAX_IMAGE_PIXELS = 4e7;
6381
+ var MAX_IMAGE_DIMENSION = 32768;
6382
+ var MAX_DECODED_IMAGE_BYTES = MAX_IMAGE_PIXELS * 4;
6383
+ var MAX_GIF_FRAMES = 64;
6384
+ var GIF_MAX_DICTIONARY_SIZE = 4096;
6385
+ var MAX_GIF_LZW_OPERATIONS = 1e7;
6386
+ var IMAGE_DECODE_TIMEOUT_SECONDS = 5;
6387
+ var SHARP_INPUT_OPTIONS = {
6388
+ failOn: "warning",
6389
+ limitInputPixels: MAX_IMAGE_PIXELS,
6390
+ limitInputChannels: 5,
6391
+ sequentialRead: true,
6392
+ unlimited: false,
6393
+ page: 0,
6394
+ pages: 1,
6395
+ animated: false
6396
+ };
6397
+ sharp.cache({ files: 0, items: 0, memory: 32 });
6398
+ sharp.concurrency(1);
6399
+ function imageMimeFromMagic(buf) {
6400
+ if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
6401
+ if (buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return "image/png";
6402
+ if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP") return "image/webp";
6403
+ if (buf.length >= 6 && (buf.toString("ascii", 0, 6) === "GIF87a" || buf.toString("ascii", 0, 6) === "GIF89a")) return "image/gif";
6404
+ return void 0;
6405
+ }
6406
+ function validDimensions(width, height) {
6407
+ return Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0 && width <= MAX_IMAGE_DIMENSION && height <= MAX_IMAGE_DIMENSION && width * height <= MAX_IMAGE_PIXELS;
6408
+ }
6409
+ var SHARP_FORMAT_BY_MIME = {
6410
+ "image/jpeg": "jpeg",
6411
+ "image/png": "png",
6412
+ "image/webp": "webp",
6413
+ "image/gif": "gif"
6414
+ };
6415
+ function skipGifSubBlocks(buffer, startOffset) {
6416
+ let offset = startOffset;
6417
+ while (offset < buffer.length) {
6418
+ const size = buffer[offset++];
6419
+ if (size === 0) return offset;
6420
+ if (offset + size > buffer.length) return void 0;
6421
+ offset += size;
6422
+ }
6423
+ return void 0;
6424
+ }
6425
+ var GifCodeReader = class {
6426
+ offset;
6427
+ blockRemaining = 0;
6428
+ datum = 0;
6429
+ bits = 0;
6430
+ ended = false;
6431
+ buffer;
6432
+ constructor(buffer, startOffset) {
6433
+ this.buffer = buffer;
6434
+ this.offset = startOffset;
6435
+ }
6436
+ nextByte() {
6437
+ if (this.ended) return void 0;
6438
+ if (this.blockRemaining === 0) {
6439
+ if (this.offset >= this.buffer.length) return void 0;
6440
+ this.blockRemaining = this.buffer[this.offset++];
6441
+ if (this.blockRemaining === 0) {
6442
+ this.ended = true;
6443
+ return void 0;
6444
+ }
6445
+ if (this.offset + this.blockRemaining > this.buffer.length) return void 0;
6446
+ }
6447
+ this.blockRemaining -= 1;
6448
+ return this.buffer[this.offset++];
6449
+ }
6450
+ read(codeSize) {
6451
+ while (this.bits < codeSize) {
6452
+ const byte = this.nextByte();
6453
+ if (byte === void 0) return void 0;
6454
+ this.datum |= byte << this.bits;
6455
+ this.bits += 8;
6456
+ }
6457
+ const code = this.datum & (1 << codeSize) - 1;
6458
+ this.datum >>>= codeSize;
6459
+ this.bits -= codeSize;
6460
+ return code;
6461
+ }
6462
+ finishSubBlocks() {
6463
+ if (this.ended) return this.offset;
6464
+ this.offset += this.blockRemaining;
6465
+ this.blockRemaining = 0;
6466
+ return skipGifSubBlocks(this.buffer, this.offset);
6467
+ }
6468
+ };
6469
+ function decodeExactGifLzwPixels(buffer, startOffset, minCodeSize, pixelCount, colorCount, operationBudget) {
6470
+ if (!Number.isInteger(minCodeSize) || minCodeSize < 2 || minCodeSize > 8) return void 0;
6471
+ const clearCode = 1 << minCodeSize;
6472
+ const endCode = clearCode + 1;
6473
+ const prefix = new Uint16Array(GIF_MAX_DICTIONARY_SIZE);
6474
+ const suffix = new Uint8Array(GIF_MAX_DICTIONARY_SIZE);
6475
+ const stack = new Uint8Array(GIF_MAX_DICTIONARY_SIZE + 1);
6476
+ for (let code = 0; code < clearCode; code += 1) suffix[code] = code;
6477
+ const reader = new GifCodeReader(buffer, startOffset);
6478
+ let codeSize = minCodeSize + 1;
6479
+ let available = clearCode + 2;
6480
+ let oldCode = -1;
6481
+ let first = 0;
6482
+ let produced = 0;
6483
+ let sawInitialClear = false;
6484
+ while (produced <= pixelCount && operationBudget.remaining > 0) {
6485
+ operationBudget.remaining -= 1;
6486
+ const next = reader.read(codeSize);
6487
+ if (next === void 0) return void 0;
6488
+ if (!sawInitialClear) {
6489
+ if (next !== clearCode) return void 0;
6490
+ sawInitialClear = true;
6491
+ continue;
6492
+ }
6493
+ if (next === clearCode) {
6494
+ codeSize = minCodeSize + 1;
6495
+ available = clearCode + 2;
6496
+ oldCode = -1;
6497
+ continue;
6498
+ }
6499
+ if (next === endCode) {
6500
+ return produced === pixelCount ? reader.finishSubBlocks() : void 0;
6501
+ }
6502
+ if (next > available || next >= GIF_MAX_DICTIONARY_SIZE) return void 0;
6503
+ if (oldCode === -1) {
6504
+ if (next >= clearCode || next >= colorCount) return void 0;
6505
+ produced += 1;
6506
+ oldCode = next;
6507
+ first = next;
6508
+ continue;
6509
+ }
6510
+ const inCode = next;
6511
+ let code = next;
6512
+ let top = 0;
6513
+ if (code === available) {
6514
+ stack[top++] = first;
6515
+ code = oldCode;
6516
+ } else if (code > available) {
6517
+ return void 0;
6518
+ }
6519
+ let links = 0;
6520
+ while (code >= clearCode + 2) {
6521
+ operationBudget.remaining -= 1;
6522
+ if (operationBudget.remaining < 0 || code >= available || top >= stack.length || links++ >= GIF_MAX_DICTIONARY_SIZE) return void 0;
6523
+ const value = suffix[code];
6524
+ if (value >= colorCount) return void 0;
6525
+ stack[top++] = value;
6526
+ code = prefix[code];
6527
+ }
6528
+ if (code >= clearCode || code >= colorCount || top >= stack.length) return void 0;
6529
+ first = suffix[code];
6530
+ stack[top++] = first;
6531
+ produced += top;
6532
+ if (produced > pixelCount) return void 0;
6533
+ if (available < GIF_MAX_DICTIONARY_SIZE) {
6534
+ prefix[available] = oldCode;
6535
+ suffix[available] = first;
6536
+ available += 1;
6537
+ if (available === 1 << codeSize && codeSize < 12) codeSize += 1;
6538
+ }
6539
+ oldCode = inCode;
6540
+ }
6541
+ return void 0;
6542
+ }
6543
+ function validateGifLzwStreams(buffer) {
6544
+ if (buffer.length < 14) return false;
6545
+ const canvasWidth = buffer.readUInt16LE(6);
6546
+ const canvasHeight = buffer.readUInt16LE(8);
6547
+ if (!validDimensions(canvasWidth, canvasHeight)) return false;
6548
+ const logicalPacked = buffer[10];
6549
+ const globalColorCount = (logicalPacked & 128) !== 0 ? 1 << (logicalPacked & 7) + 1 : 0;
6550
+ let offset = 13 + globalColorCount * 3;
6551
+ if (offset > buffer.length) return false;
6552
+ let frameCount = 0;
6553
+ const operationBudget = { remaining: MAX_GIF_LZW_OPERATIONS };
6554
+ while (offset < buffer.length) {
6555
+ const marker = buffer[offset++];
6556
+ if (marker === 59) return frameCount > 0 && offset === buffer.length;
6557
+ if (marker === 33) {
6558
+ if (offset >= buffer.length) return false;
6559
+ offset += 1;
6560
+ const nextOffset2 = skipGifSubBlocks(buffer, offset);
6561
+ if (nextOffset2 === void 0) return false;
6562
+ offset = nextOffset2;
6563
+ continue;
6564
+ }
6565
+ if (marker !== 44 || offset + 9 > buffer.length) return false;
6566
+ const left = buffer.readUInt16LE(offset);
6567
+ const top = buffer.readUInt16LE(offset + 2);
6568
+ const width = buffer.readUInt16LE(offset + 4);
6569
+ const height = buffer.readUInt16LE(offset + 6);
6570
+ const packed = buffer[offset + 8];
6571
+ offset += 9;
6572
+ if (!validDimensions(width, height)) return false;
6573
+ if (left + width > canvasWidth || top + height > canvasHeight) return false;
6574
+ frameCount += 1;
6575
+ if (frameCount > MAX_GIF_FRAMES) return false;
6576
+ const localColorCount = (packed & 128) !== 0 ? 1 << (packed & 7) + 1 : 0;
6577
+ if (localColorCount > 0) {
6578
+ offset += localColorCount * 3;
6579
+ if (offset > buffer.length) return false;
6580
+ }
6581
+ const colorCount = localColorCount || globalColorCount;
6582
+ if (colorCount === 0 || offset >= buffer.length) return false;
6583
+ const minCodeSize = buffer[offset++];
6584
+ const nextOffset = decodeExactGifLzwPixels(
6585
+ buffer,
6586
+ offset,
6587
+ minCodeSize,
6588
+ width * height,
6589
+ colorCount,
6590
+ operationBudget
6591
+ );
6592
+ if (nextOffset === void 0) return false;
6593
+ offset = nextOffset;
6594
+ }
6595
+ return false;
6596
+ }
6597
+ async function validateDecodedImage(buffer, mime) {
6598
+ if (buffer.length > MAX_IMAGE_BYTES) return "MEDIA_IMAGE_TOO_LARGE";
6599
+ if (mime === "image/gif" && !validateGifLzwStreams(buffer)) return "MEDIA_IMAGE_INVALID";
6600
+ try {
6601
+ const metadata = await sharp(buffer, SHARP_INPUT_OPTIONS).timeout({ seconds: IMAGE_DECODE_TIMEOUT_SECONDS }).metadata();
6602
+ const width = metadata.width;
6603
+ const height = metadata.pageHeight ?? metadata.height;
6604
+ if (metadata.format !== SHARP_FORMAT_BY_MIME[mime]) return "MEDIA_IMAGE_INVALID";
6605
+ if (!validDimensions(width, height)) return "MEDIA_IMAGE_TOO_LARGE";
6606
+ const decoded = await sharp(buffer, SHARP_INPUT_OPTIONS).timeout({ seconds: IMAGE_DECODE_TIMEOUT_SECONDS }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
6607
+ const decodedBytes = decoded.data.length;
6608
+ if (!validDimensions(decoded.info.width, decoded.info.height)) return "MEDIA_IMAGE_TOO_LARGE";
6609
+ if (decodedBytes !== decoded.info.width * decoded.info.height * decoded.info.channels) return "MEDIA_IMAGE_INVALID";
6610
+ if (decodedBytes > MAX_DECODED_IMAGE_BYTES) return "MEDIA_IMAGE_TOO_LARGE";
6611
+ return void 0;
6612
+ } catch {
6613
+ return "MEDIA_IMAGE_INVALID";
6614
+ }
6615
+ }
6616
+ async function validateInboundMedia(result) {
6617
+ if (!result.data) return { ok: false, code: "MEDIA_EMPTY_DATA" };
6618
+ const normalized = result.data.replace(/\s/g, "");
6619
+ if (!normalized || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
6620
+ return { ok: false, code: "MEDIA_INVALID_BASE64" };
6621
+ }
6622
+ const buffer = Buffer.from(normalized, "base64");
6623
+ if (buffer.length === 0) return { ok: false, code: "MEDIA_EMPTY_DATA" };
6624
+ const declared = MIME_BY_FORMAT[result.format.toLowerCase()] ?? `application/${result.format || "octet-stream"}`;
6625
+ if (result.type === "image") {
6626
+ const detected = imageMimeFromMagic(buffer);
6627
+ if (!detected || detected !== declared) return { ok: false, code: "MEDIA_MAGIC_MISMATCH" };
6628
+ const invalidCode = await validateDecodedImage(buffer, detected);
6629
+ if (invalidCode) return { ok: false, code: invalidCode };
6630
+ }
6631
+ return { ok: true, value: { buffer, mime: declared } };
6632
+ }
6633
+ function safeBodyAfterKnownMediaFailure(baseType, content) {
6634
+ return baseType === 3 ? "[Image unavailable]" : content;
6635
+ }
6636
+ async function saveValidatedInboundMedia(result, save) {
6637
+ const validated = await validateInboundMedia(result);
6638
+ if (!validated.ok) return validated;
6639
+ try {
6640
+ const saved = await save(validated.value.buffer, validated.value.mime, result.filename);
6641
+ if (!saved?.path || typeof saved.path !== "string" || !saved.path.trim()) {
6642
+ return { ok: false, code: "MEDIA_SAVE_FAILED" };
6643
+ }
6644
+ return { ok: true, path: saved.path, mime: validated.value.mime };
6645
+ } catch {
6646
+ return { ok: false, code: "MEDIA_SAVE_FAILED" };
6647
+ }
6648
+ }
6649
+
6650
+ // src/inbound-media-poll.ts
6651
+ async function pollMedia(client, chatId, localId, log, maxAttempts = 15, intervalMs = 1e3) {
6652
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
6653
+ const result = await client.getMedia(chatId, localId);
6654
+ if (result.type === "unsupported") return null;
6655
+ if (result.data) return result;
6656
+ if (attempt < maxAttempts) {
6657
+ log?.info?.(`[wechat:media] pending attempt=${attempt}/${maxAttempts}`);
6658
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
6659
+ }
6660
+ }
6661
+ return null;
6662
+ }
6663
+
6358
6664
  // src/monitor.ts
6359
6665
  var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34, 43]);
6360
6666
  var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
@@ -6371,22 +6677,6 @@ function sleep(ms, signal) {
6371
6677
  });
6372
6678
  });
6373
6679
  }
6374
- async function pollMedia(client, chatId, localId, log, maxAttempts = 15, intervalMs = 1e3) {
6375
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
6376
- const result = await client.getMedia(chatId, localId);
6377
- if (result.type === "unsupported") {
6378
- return null;
6379
- }
6380
- if (result.data) {
6381
- return result;
6382
- }
6383
- if (attempt < maxAttempts) {
6384
- log?.info?.(`[media] Attempt ${attempt}/${maxAttempts} for ${chatId}:${localId} returned no data, retrying...`);
6385
- await new Promise((r) => setTimeout(r, intervalMs));
6386
- }
6387
- }
6388
- return null;
6389
- }
6390
6680
  function enqueueWeChatSystemEvent(text, contextKey) {
6391
6681
  try {
6392
6682
  const core = getWeChatRuntime();
@@ -6657,44 +6947,28 @@ async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, lo
6657
6947
  const result = await pollMedia(client, chatId, msg.localId, log);
6658
6948
  if (result && result.data && result.type !== "unsupported") {
6659
6949
  hasMedia = true;
6660
- log?.info?.(`[wechat:${liveAccount.accountId}] Media result: type=${result.type}, format=${result.format}, hasData=${!!result.data}, filename=${result.filename}`);
6661
- const mimeMap = {
6662
- jpeg: "image/jpeg",
6663
- jpg: "image/jpeg",
6664
- png: "image/png",
6665
- gif: "image/gif",
6666
- mp3: "audio/mpeg",
6667
- pdf: "application/pdf",
6668
- doc: "application/msword",
6669
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
6670
- xls: "application/vnd.ms-excel",
6671
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
6672
- ppt: "application/vnd.ms-powerpoint",
6673
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
6674
- zip: "application/zip",
6675
- txt: "text/plain"
6676
- };
6677
- mediaMime = mimeMap[result.format] ?? `application/${result.format || "octet-stream"}`;
6678
- const buf = Buffer.from(result.data, "base64");
6679
- const saved = await core.channel.media.saveMediaBuffer(
6680
- buf,
6681
- mediaMime,
6682
- "inbound",
6683
- void 0,
6684
- result.filename
6950
+ log?.info?.(`[wechat:media] inbound media received type=${result.type} format=${result.format}`);
6951
+ const saved = await saveValidatedInboundMedia(
6952
+ result,
6953
+ async (buffer, mime, filename) => core.channel.media.saveMediaBuffer(buffer, mime, "inbound", void 0, filename)
6685
6954
  );
6686
- mediaPath = saved?.path;
6687
- log?.info?.(`[wechat:${liveAccount.accountId}] Saved media to ${mediaPath}`);
6955
+ if (!saved.ok) {
6956
+ log?.error?.(`[wechat:media] inbound media rejected code=${saved.code}`);
6957
+ } else {
6958
+ mediaMime = saved.mime;
6959
+ mediaPath = saved.path;
6960
+ log?.info?.("[wechat:media] inbound media saved");
6961
+ }
6688
6962
  } else if (MEDIA_TYPES.has(baseType)) {
6689
6963
  hasMedia = true;
6690
- log?.info?.(`[wechat:${liveAccount.accountId}] Media not available after retries for msg ${msg.localId}`);
6964
+ log?.info?.("[wechat:media] inbound media unavailable after retries");
6691
6965
  }
6692
6966
  } catch (err) {
6693
- log?.error?.(`[wechat:${liveAccount.accountId}] Media download failed: ${err}`);
6967
+ log?.error?.("[wechat:media] inbound media failed code=MEDIA_DOWNLOAD_FAILED");
6694
6968
  }
6695
6969
  }
6696
6970
  const timestamp = new Date(msg.timestamp).getTime();
6697
- let rawBody = msg.content || "";
6971
+ let rawBody = mediaPath ? msg.content || "" : safeBodyAfterKnownMediaFailure(baseType, msg.content || "");
6698
6972
  if (mediaPath && mediaMime) {
6699
6973
  if (!rawBody) {
6700
6974
  if (mediaMime.startsWith("audio/")) {
@@ -6762,7 +7036,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6762
7036
  const mediaPath = mediaMsg?.mediaPath;
6763
7037
  const mediaMime = mediaMsg?.mediaMime;
6764
7038
  log?.info?.(
6765
- `[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ? ` media=${mediaPath}` : ""}`
7039
+ `[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ? " media=present" : ""}`
6766
7040
  );
6767
7041
  const hasControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(commandBody, cfg);
6768
7042
  const commandAuthorized = await resolveWeChatCommandAuthorization({
@@ -6802,7 +7076,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6802
7076
  try {
6803
7077
  const route = core.channel.routing.resolveAgentRoute({
6804
7078
  cfg,
6805
- channel: "wechat",
7079
+ channel: OPENCLAW_CHANNEL_ID,
6806
7080
  accountId: liveAccount.accountId,
6807
7081
  peer: {
6808
7082
  kind: isGroup ? "group" : "direct",
@@ -6874,21 +7148,21 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6874
7148
  RawBody: rawBody,
6875
7149
  CommandBody: commandBody,
6876
7150
  InboundHistory: inboundHistory,
6877
- From: isGroup ? `wechat:group:${chatId}` : `wechat:${senderId}`,
6878
- To: `wechat:${chatId}`,
7151
+ From: isGroup ? `${OPENCLAW_CHANNEL_ID}:group:${chatId}` : `${OPENCLAW_CHANNEL_ID}:${senderId}`,
7152
+ To: `${OPENCLAW_CHANNEL_ID}:${chatId}`,
6879
7153
  SessionKey: route.sessionKey,
6880
7154
  AccountId: route.accountId,
6881
7155
  ChatType: isGroup ? "group" : "direct",
6882
7156
  ConversationLabel: fromLabel,
6883
7157
  SenderName: senderName || void 0,
6884
7158
  SenderId: senderId,
6885
- Provider: "wechat",
6886
- Surface: "wechat",
6887
- MessageSid: `wechat:${chatId}:${msg.localId}`,
7159
+ Provider: OPENCLAW_CHANNEL_ID,
7160
+ Surface: OPENCLAW_CHANNEL_ID,
7161
+ MessageSid: `${OPENCLAW_CHANNEL_ID}:${chatId}:${msg.localId}`,
6888
7162
  WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : void 0,
6889
7163
  CommandAuthorized: commandAuthorized,
6890
- OriginatingChannel: "wechat",
6891
- OriginatingTo: `wechat:${chatId}`,
7164
+ OriginatingChannel: OPENCLAW_CHANNEL_ID,
7165
+ OriginatingTo: `${OPENCLAW_CHANNEL_ID}:${chatId}`,
6892
7166
  ...mediaPath ? { MediaPath: mediaPath, MediaUrl: mediaPath, MediaType: mediaMime } : {},
6893
7167
  ...msg.reply ? {
6894
7168
  ReplyToBody: msg.reply.content.length > 50 ? msg.reply.content.slice(0, 50) + "..." : msg.reply.content,
@@ -6908,7 +7182,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6908
7182
  const { onModelSelected, ...prefixOptions } = createChannelReplyPipeline({
6909
7183
  cfg,
6910
7184
  agentId: route.agentId,
6911
- channel: "wechat",
7185
+ channel: OPENCLAW_CHANNEL_ID,
6912
7186
  accountId: liveAccount.accountId
6913
7187
  });
6914
7188
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
@@ -6920,7 +7194,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6920
7194
  const mediaList = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
6921
7195
  const tableMode = core.channel.text.resolveMarkdownTableMode({
6922
7196
  cfg,
6923
- channel: "wechat",
7197
+ channel: OPENCLAW_CHANNEL_ID,
6924
7198
  accountId: liveAccount.accountId
6925
7199
  });
6926
7200
  const text = core.channel.text.convertMarkdownTables(
@@ -6989,7 +7263,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6989
7263
  }
6990
7264
  });
6991
7265
  core.channel.activity?.record?.({
6992
- channel: "wechat",
7266
+ channel: OPENCLAW_CHANNEL_ID,
6993
7267
  accountId: liveAccount.accountId,
6994
7268
  direction: "inbound",
6995
7269
  at: timestamp
@@ -7019,7 +7293,7 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
7019
7293
  const core = getWeChatRuntime();
7020
7294
  const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
7021
7295
  const chatId = chat.username ?? chat.id;
7022
- const storeAllowFrom = await core.channel.pairing.readAllowFromStore({ channel: "wechat", accountId: liveAccount.accountId, env: process.env }).catch(() => []);
7296
+ const storeAllowFrom = await core.channel.pairing.readAllowFromStore({ channel: OPENCLAW_CHANNEL_ID, accountId: liveAccount.accountId, env: process.env }).catch(() => []);
7023
7297
  const policy = resolveWeChatPolicyContext({
7024
7298
  account: liveAccount,
7025
7299
  cfg,
@@ -7028,7 +7302,7 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
7028
7302
  });
7029
7303
  const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
7030
7304
  cfg,
7031
- surface: "wechat"
7305
+ surface: OPENCLAW_CHANNEL_ID
7032
7306
  });
7033
7307
  let opened = false;
7034
7308
  const openChatIfNeeded = async () => {
@@ -7278,7 +7552,7 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
7278
7552
  }
7279
7553
  if (decision.action === "defer") {
7280
7554
  log?.info?.(
7281
- `[wechat:${liveAccount.accountId}] Catch-up hold for ${chatId} (budget ${liveAccount.catchUpChatBudget}); raise channels.wechat.catchUpChatBudget to continue`
7555
+ `[wechat:${liveAccount.accountId}] Catch-up hold for ${chatId} (budget ${liveAccount.catchUpChatBudget}); raise channels.${OPENCLAW_CHANNEL_ID}.catchUpChatBudget to continue`
7282
7556
  );
7283
7557
  return "deferred";
7284
7558
  }
@@ -7344,7 +7618,7 @@ function collectWeChatStatusIssues(accounts) {
7344
7618
  for (const snapshot of accounts) {
7345
7619
  if (snapshot.connected === false) {
7346
7620
  issues.push({
7347
- channel: "wechat",
7621
+ channel: "agent-wechat",
7348
7622
  accountId: snapshot.accountId,
7349
7623
  kind: "runtime",
7350
7624
  message: snapshot.lastError ? `Cannot reach agent-wechat server: ${snapshot.lastError}` : "Cannot reach agent-wechat server.",
@@ -7352,7 +7626,7 @@ function collectWeChatStatusIssues(accounts) {
7352
7626
  });
7353
7627
  } else if (snapshot.authStatus === "app_not_running") {
7354
7628
  issues.push({
7355
- channel: "wechat",
7629
+ channel: "agent-wechat",
7356
7630
  accountId: snapshot.accountId,
7357
7631
  kind: "runtime",
7358
7632
  message: "WeChat application is not running. It should restart automatically.",
@@ -7360,11 +7634,11 @@ function collectWeChatStatusIssues(accounts) {
7360
7634
  });
7361
7635
  } else if (snapshot.linked === false) {
7362
7636
  issues.push({
7363
- channel: "wechat",
7637
+ channel: "agent-wechat",
7364
7638
  accountId: snapshot.accountId,
7365
7639
  kind: "auth",
7366
7640
  message: "WeChat session not authenticated.",
7367
- fix: "Run: openclaw channels login --channel wechat"
7641
+ fix: "Run: openclaw channels login --channel agent-wechat"
7368
7642
  });
7369
7643
  }
7370
7644
  }
@@ -7783,7 +8057,7 @@ async function sendWeChatText(cfg, to, text) {
7783
8057
  }
7784
8058
  throw new Error(result.error ?? "Send failed");
7785
8059
  }
7786
- return `wechat:${to}:${Date.now()}`;
8060
+ return `${OPENCLAW_CHANNEL_ID}:${to}:${Date.now()}`;
7787
8061
  }
7788
8062
  async function sendWeChatMedia(cfg, to, text, mediaUrl) {
7789
8063
  const account = resolveWeChatAccount(cfg);
@@ -7796,7 +8070,7 @@ async function sendWeChatMedia(cfg, to, text, mediaUrl) {
7796
8070
  source: "openclaw"
7797
8071
  });
7798
8072
  if (!result.success) throw new Error(result.error ?? "Send failed");
7799
- return `wechat:${to}:${Date.now()}`;
8073
+ return `${OPENCLAW_CHANNEL_ID}:${to}:${Date.now()}`;
7800
8074
  }
7801
8075
  const fsmod = await import("fs/promises");
7802
8076
  const pathmod = await import("path");
@@ -7833,10 +8107,10 @@ async function sendWeChatMedia(cfg, to, text, mediaUrl) {
7833
8107
  interPartDelayMs: account.mediaPartDelayMs,
7834
8108
  source: "openclaw"
7835
8109
  });
7836
- return `wechat:${to}:${requestId}`;
8110
+ return `${OPENCLAW_CHANNEL_ID}:${to}:${requestId}`;
7837
8111
  }
7838
8112
  var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
7839
- id: "wechat",
8113
+ id: OPENCLAW_CHANNEL_ID,
7840
8114
  capabilities: {
7841
8115
  text: true,
7842
8116
  media: true,
@@ -7844,26 +8118,26 @@ var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
7844
8118
  },
7845
8119
  outbound: {
7846
8120
  sendText: async ({ cfg, to, text }) => ({
7847
- channel: "wechat",
8121
+ channel: OPENCLAW_CHANNEL_ID,
7848
8122
  messageId: await sendWeChatText(cfg, to, text)
7849
8123
  }),
7850
8124
  sendMedia: async ({ cfg, to, text, mediaUrl }) => ({
7851
- channel: "wechat",
8125
+ channel: OPENCLAW_CHANNEL_ID,
7852
8126
  messageId: await sendWeChatMedia(cfg, to, text, mediaUrl)
7853
8127
  })
7854
8128
  }
7855
8129
  });
7856
8130
  var meta = {
7857
- id: "wechat",
8131
+ id: OPENCLAW_CHANNEL_ID,
7858
8132
  label: "WeChat",
7859
- selectionLabel: "WeChat (\u5FAE\u4FE1)",
8133
+ selectionLabel: "WeChat (agent-wechat)",
7860
8134
  blurb: "WeChat messaging via agent-wechat container.",
7861
8135
  docsPath: "wechat",
7862
- aliases: ["weixin"],
8136
+ aliases: [],
7863
8137
  order: 80
7864
8138
  };
7865
8139
  var wechatPlugin = {
7866
- id: "wechat",
8140
+ id: OPENCLAW_CHANNEL_ID,
7867
8141
  meta,
7868
8142
  gatewayMethods: ["web.login.start", "web.login.wait"],
7869
8143
  capabilities: {
@@ -7873,7 +8147,7 @@ var wechatPlugin = {
7873
8147
  media: true,
7874
8148
  reply: true
7875
8149
  },
7876
- reload: { configPrefixes: ["channels.wechat"] },
8150
+ reload: { configPrefixes: [`channels.${OPENCLAW_CHANNEL_ID}`, "channels.wechat"] },
7877
8151
  configSchema: {
7878
8152
  schema: {
7879
8153
  type: "object",
@@ -7949,23 +8223,23 @@ var wechatPlugin = {
7949
8223
  },
7950
8224
  isEnabled: (account) => account.enabled && !!account.serverUrl,
7951
8225
  isConfigured: (account) => !!account.serverUrl,
7952
- unconfiguredReason: () => "No serverUrl configured. Run: openclaw channels setup wechat"
8226
+ unconfiguredReason: () => "No serverUrl configured. Run: openclaw channels setup agent-wechat"
7953
8227
  },
7954
8228
  // ---- Security adapter ----
7955
8229
  security: {
7956
8230
  resolveDmPolicy: ({ account }) => ({
7957
8231
  policy: account.dmPolicy ?? "disabled",
7958
8232
  allowFrom: account.allowFrom ?? [],
7959
- allowFromPath: "channels.wechat.allowFrom",
7960
- policyPath: "channels.wechat.dmPolicy",
7961
- approveHint: "Add the wxid to channels.wechat.allowFrom",
7962
- normalizeEntry: (raw) => raw.replace(/^wechat:/i, "").trim()
8233
+ allowFromPath: `channels.${OPENCLAW_CHANNEL_ID}.allowFrom`,
8234
+ policyPath: `channels.${OPENCLAW_CHANNEL_ID}.dmPolicy`,
8235
+ approveHint: `Add the wxid to channels.${OPENCLAW_CHANNEL_ID}.allowFrom`,
8236
+ normalizeEntry: (raw) => raw.replace(/^(agent-wechat|wechat):/i, "").trim()
7963
8237
  })
7964
8238
  },
7965
8239
  // ---- Groups adapter ----
7966
8240
  groups: {
7967
8241
  resolveRequireMention: ({ cfg, groupId }) => {
7968
- const wechat = cfg?.channels?.wechat;
8242
+ const wechat = cfg?.channels?.[OPENCLAW_CHANNEL_ID] ?? cfg?.channels?.wechat;
7969
8243
  if (!wechat) return true;
7970
8244
  if (!groupId) {
7971
8245
  return wechat.groups?.["*"]?.requireMention ?? true;
@@ -7990,10 +8264,10 @@ var wechatPlugin = {
7990
8264
  },
7991
8265
  // ---- Messaging adapter ----
7992
8266
  messaging: {
7993
- normalizeTarget: (raw) => raw.replace(/^wechat:/i, "").trim() || void 0,
8267
+ normalizeTarget: (raw) => raw.replace(/^(agent-wechat|wechat):/i, "").trim() || void 0,
7994
8268
  targetResolver: {
7995
8269
  looksLikeId: (raw) => {
7996
- const stripped = raw.replace(/^wechat:/i, "").trim();
8270
+ const stripped = raw.replace(/^(agent-wechat|wechat):/i, "").trim();
7997
8271
  return stripped.includes("@chatroom") || stripped.startsWith("wxid_");
7998
8272
  },
7999
8273
  hint: "WeChat ID (wxid_xxx or xxx@chatroom)"
@@ -8003,11 +8277,11 @@ var wechatPlugin = {
8003
8277
  outbound: {
8004
8278
  deliveryMode: "direct",
8005
8279
  sendText: async ({ cfg, to, text }) => ({
8006
- channel: "wechat",
8280
+ channel: OPENCLAW_CHANNEL_ID,
8007
8281
  messageId: await sendWeChatText(cfg, to, text)
8008
8282
  }),
8009
8283
  sendMedia: async ({ cfg, to, text, mediaUrl }) => ({
8010
- channel: "wechat",
8284
+ channel: OPENCLAW_CHANNEL_ID,
8011
8285
  messageId: await sendWeChatMedia(cfg, to, text, mediaUrl)
8012
8286
  })
8013
8287
  },
@@ -8034,7 +8308,7 @@ var wechatPlugin = {
8034
8308
  accountId ?? void 0
8035
8309
  );
8036
8310
  if (!account?.serverUrl) {
8037
- return { message: "No serverUrl configured. Run: openclaw channels setup wechat" };
8311
+ return { message: "No serverUrl configured. Run: openclaw channels setup agent-wechat" };
8038
8312
  }
8039
8313
  const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
8040
8314
  try {
@@ -8075,7 +8349,7 @@ var wechatPlugin = {
8075
8349
  );
8076
8350
  if (!account?.serverUrl) {
8077
8351
  throw new Error(
8078
- "No serverUrl configured. Run: openclaw channels setup wechat"
8352
+ "No serverUrl configured. Run: openclaw channels setup agent-wechat"
8079
8353
  );
8080
8354
  }
8081
8355
  const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
@@ -8235,8 +8509,8 @@ Error: ${event.message}`);
8235
8509
  ...cfg,
8236
8510
  channels: {
8237
8511
  ...cfg.channels,
8238
- wechat: {
8239
- ...cfg.channels?.wechat,
8512
+ [OPENCLAW_CHANNEL_ID]: {
8513
+ ...cfg.channels?.[OPENCLAW_CHANNEL_ID] ?? cfg.channels?.wechat,
8240
8514
  enabled: true,
8241
8515
  serverUrl,
8242
8516
  ...token ? { token } : {}
@@ -8249,7 +8523,7 @@ Error: ${event.message}`);
8249
8523
 
8250
8524
  // index.ts
8251
8525
  var plugin = {
8252
- id: "wechat",
8526
+ id: OPENCLAW_CHANNEL_ID,
8253
8527
  name: "WeChat",
8254
8528
  description: "WeChat channel via agent-wechat container",
8255
8529
  configSchema: emptyPluginConfigSchema(),
@@ -1,13 +1,13 @@
1
1
  {
2
- "id": "wechat",
3
- "channels": ["wechat"],
2
+ "id": "agent-wechat",
3
+ "channels": ["agent-wechat"],
4
4
  "configSchema": {
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
7
  "properties": {}
8
8
  },
9
9
  "channelConfigs": {
10
- "wechat": {
10
+ "agent-wechat": {
11
11
  "label": "WeChat",
12
12
  "description": "WeChat messaging via agent-wechat container.",
13
13
  "schema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyan-du/agent-wechat-openclaw",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -8,7 +8,7 @@
8
8
  "!dist/.release-metafile.json"
9
9
  ],
10
10
  "scripts": {
11
- "build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --metafile=dist/.release-metafile.json --external:openclaw",
11
+ "build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --metafile=dist/.release-metafile.json --external:openclaw --external:sharp",
12
12
  "typecheck": "tsc --noEmit",
13
13
  "test": "node --test --experimental-strip-types src/*.test.ts"
14
14
  },
@@ -24,12 +24,10 @@
24
24
  "./dist/index.js"
25
25
  ],
26
26
  "channel": {
27
- "id": "wechat",
27
+ "id": "agent-wechat",
28
28
  "label": "WeChat",
29
- "selectionLabel": "WeChat (微信)",
30
- "aliases": [
31
- "weixin"
32
- ],
29
+ "selectionLabel": "WeChat (agent-wechat)",
30
+ "aliases": [],
33
31
  "order": 80
34
32
  },
35
33
  "install": {
@@ -48,5 +46,8 @@
48
46
  "main": "./dist/index.js",
49
47
  "exports": {
50
48
  ".": "./dist/index.js"
49
+ },
50
+ "dependencies": {
51
+ "sharp": "^0.35.3"
51
52
  }
52
53
  }