@kyan-du/agent-wechat-openclaw 0.14.2 → 0.14.4

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.
Files changed (2) hide show
  1. package/dist/index.js +350 -34
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5304,6 +5304,15 @@ var downloadFileParamsSchema = external_exports.object({
5304
5304
  var downloadFileResultSchema = openChatResultSchema.extend({
5305
5305
  clicked: external_exports.boolean().optional()
5306
5306
  });
5307
+ var materializeChatHistoryParamsSchema = external_exports.object({
5308
+ chatId: external_exports.string().min(1),
5309
+ title: external_exports.string().min(1).optional(),
5310
+ localId: external_exports.number().int().optional(),
5311
+ executionTimeoutMs: external_exports.number().int().positive().optional()
5312
+ });
5313
+ var materializeChatHistoryResultSchema = openChatResultSchema.extend({
5314
+ clicked: external_exports.boolean().optional()
5315
+ });
5307
5316
  var idempotencyKeySchema = external_exports.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/);
5308
5317
  var forwardedMessageNodeSchema = external_exports.lazy(() => external_exports.object({
5309
5318
  sender: external_exports.string().optional(),
@@ -5417,7 +5426,8 @@ var mediaResultSchema = external_exports.object({
5417
5426
  format: external_exports.string(),
5418
5427
  filename: external_exports.string(),
5419
5428
  source: external_exports.enum(["original", "thumbnail"]).optional(),
5420
- errorCode: external_exports.string().optional()
5429
+ errorCode: external_exports.string().optional(),
5430
+ items: external_exports.array(external_exports.lazy(() => mediaResultSchema)).optional()
5421
5431
  });
5422
5432
  var agentConfigSchema = external_exports.object({
5423
5433
  maxTurns: external_exports.number().int().positive().default(30),
@@ -5501,6 +5511,17 @@ var WeChatClient = class {
5501
5511
  await throwHttpError(res);
5502
5512
  return res.json();
5503
5513
  }
5514
+ async put(path, body, signal) {
5515
+ const res = await fetch(`${this.base}${path}`, {
5516
+ method: "PUT",
5517
+ headers: this.headers,
5518
+ body: body != null ? JSON.stringify(body) : void 0,
5519
+ signal
5520
+ });
5521
+ if (!res.ok)
5522
+ await throwHttpError(res);
5523
+ return res.json();
5524
+ }
5504
5525
  // ---- Status ----
5505
5526
  async status() {
5506
5527
  return this.get("/api/status");
@@ -5522,12 +5543,18 @@ var WeChatClient = class {
5522
5543
  return this.post("/api/status/auth/reset");
5523
5544
  }
5524
5545
  // ---- Chats ----
5525
- async listChatsPage(limit, cursor, unreadOnly) {
5526
- const page = await this.get(`/api/chats${qs({ limit, cursor, unreadOnly })}`);
5546
+ async listChatsPage(limit, cursor, unreadOnly, interestOnly) {
5547
+ const page = await this.get(`/api/chats${qs({ limit, cursor, unreadOnly, interestOnly })}`);
5527
5548
  if (page.errorCode)
5528
5549
  throw new WeChatHttpError(400, "Bad Request", page.errorCode, page.errorCode);
5529
5550
  return page;
5530
5551
  }
5552
+ async getMonitorInterest() {
5553
+ return this.get("/api/interest");
5554
+ }
5555
+ async setMonitorInterest(interest) {
5556
+ return this.put("/api/interest", interest);
5557
+ }
5531
5558
  async listChats(limit) {
5532
5559
  return (await this.listChatsPage(limit)).items;
5533
5560
  }
@@ -5546,6 +5573,10 @@ var WeChatClient = class {
5546
5573
  async downloadFile(chatId, filename, signal, executionTimeoutMs) {
5547
5574
  return this.post(`/api/chats/${encodeURIComponent(chatId)}/download-file${qs({ filename, executionTimeoutMs })}`, void 0, signal);
5548
5575
  }
5576
+ async materializeChatHistory(chatId, options, signal) {
5577
+ const { title, localId, executionTimeoutMs } = options ?? {};
5578
+ return this.post(`/api/chats/${encodeURIComponent(chatId)}/materialize-chat-history${qs({ title, localId, executionTimeoutMs })}`, void 0, signal);
5579
+ }
5549
5580
  async listGroupMembersPage(groupId, limit, cursor) {
5550
5581
  const page = groupMembersPageSchema.parse(await this.get(`/api/groups/${encodeURIComponent(groupId)}/members${qs({ limit, cursor })}`));
5551
5582
  if (page.errorCode)
@@ -5671,6 +5702,65 @@ var WeChatClient = class {
5671
5702
  // src/monitor.ts
5672
5703
  import { createChannelMessageReplyPipeline as createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
5673
5704
 
5705
+ // src/monitor-interest.ts
5706
+ function normalizeAllowFrom(values) {
5707
+ const out = [];
5708
+ const seen = /* @__PURE__ */ new Set();
5709
+ for (const entry of values ?? []) {
5710
+ let id = String(entry).trim();
5711
+ if (!id) continue;
5712
+ if (id !== "*") {
5713
+ id = id.replace(/^(agent-wechat|wechat):/i, "").trim();
5714
+ }
5715
+ if (!id || seen.has(id)) continue;
5716
+ seen.add(id);
5717
+ out.push(id);
5718
+ }
5719
+ return out;
5720
+ }
5721
+ function buildMonitorInterest(account) {
5722
+ return {
5723
+ dmPolicy: account.dmPolicy ?? "disabled",
5724
+ dmAllowFrom: normalizeAllowFrom(account.allowFrom),
5725
+ groupPolicy: account.groupPolicy ?? "allowlist",
5726
+ groupAllowFrom: normalizeAllowFrom(account.groupAllowFrom),
5727
+ includeSystemFeeds: true
5728
+ };
5729
+ }
5730
+ function monitorInterestFingerprint(interest) {
5731
+ return JSON.stringify({
5732
+ dmPolicy: interest.dmPolicy,
5733
+ dmAllowFrom: [...interest.dmAllowFrom ?? []].sort(),
5734
+ groupPolicy: interest.groupPolicy ?? "allowlist",
5735
+ groupAllowFrom: [...interest.groupAllowFrom ?? []].sort(),
5736
+ includeSystemFeeds: interest.includeSystemFeeds !== false
5737
+ });
5738
+ }
5739
+ function shouldListChatsInterestOnly(interest) {
5740
+ return interest.dmPolicy === "allowlist" || interest.dmPolicy === "disabled";
5741
+ }
5742
+ async function ensureMonitorInterestSynced(client, account, log) {
5743
+ const interest = buildMonitorInterest(account);
5744
+ const desiredFp = monitorInterestFingerprint(interest);
5745
+ try {
5746
+ const current = await client.getMonitorInterest();
5747
+ const serverInterest = current.interest;
5748
+ if (serverInterest && monitorInterestFingerprint(serverInterest) === desiredFp) {
5749
+ return { interest, synced: true };
5750
+ }
5751
+ await client.setMonitorInterest(interest);
5752
+ log?.info?.(
5753
+ `[wechat:${account.accountId}] Synced monitor interest to agent-server (dmPolicy=${interest.dmPolicy}, dmAllowFrom=${interest.dmAllowFrom.length})`
5754
+ );
5755
+ return { interest, synced: true };
5756
+ } catch (err) {
5757
+ log?.error?.(
5758
+ `[wechat:${account.accountId}] Failed to sync monitor interest: ${err}`
5759
+ );
5760
+ return { interest, synced: false };
5761
+ }
5762
+ }
5763
+
5674
5764
  // src/catch-up.ts
5675
5765
  function recoveryCursor(selection) {
5676
5766
  return selection.cursor;
@@ -5824,7 +5914,7 @@ async function listInitialMonitorChatSnapshot(client, state, pageBudget = DEFAUL
5824
5914
  const chats = [];
5825
5915
  let cursor;
5826
5916
  for (let pages = 0; pages < pageBudget; pages += 1) {
5827
- const page = await client.listChatsPage(MONITOR_CHAT_PAGE_LIMIT, cursor);
5917
+ const page = await client.listChatsPage(MONITOR_CHAT_PAGE_LIMIT, cursor, void 0, state.interestOnly);
5828
5918
  chats.push(...page.items);
5829
5919
  cursor = page.nextCursor;
5830
5920
  if (!cursor) {
@@ -5837,7 +5927,7 @@ async function listInitialMonitorChatSnapshot(client, state, pageBudget = DEFAUL
5837
5927
  }
5838
5928
  async function listNextMonitorChatPage(client, state) {
5839
5929
  const isInitialSnapshot = !state.initialScanComplete;
5840
- const page = await client.listChatsPage(MONITOR_CHAT_PAGE_LIMIT, state.cursor);
5930
+ const page = await client.listChatsPage(MONITOR_CHAT_PAGE_LIMIT, state.cursor, void 0, state.interestOnly);
5841
5931
  state.cursor = page.nextCursor;
5842
5932
  if (!page.nextCursor) {
5843
5933
  state.initialScanComplete = true;
@@ -6787,20 +6877,70 @@ async function validateInboundMedia(result) {
6787
6877
  function safeBodyAfterKnownMediaFailure(baseType, content) {
6788
6878
  return baseType === 3 ? "[Image unavailable]" : content;
6789
6879
  }
6880
+ function formatType49MediaFailureBody(content, mediaErrorCode) {
6881
+ const text = content || "File attachment";
6882
+ return `${text}
6883
+ [Attachment unavailable: ${mediaErrorCode}]`;
6884
+ }
6790
6885
 
6791
6886
  // src/inbound-media-poll.ts
6887
+ var DOWNLOADABLE_MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34, 43]);
6888
+ var CHAT_HISTORY_APPMSG_TYPE = 19;
6889
+ function isWeChatChatHistoryMessage(msg) {
6890
+ if (msg.forwarded) return true;
6891
+ const content = msg.content ?? "";
6892
+ if (content.startsWith("[Chat History]")) return true;
6893
+ const baseType = msg.type & 2147483647;
6894
+ if (baseType !== 49) return false;
6895
+ const packedSubtype = Math.floor(msg.type / 4294967296);
6896
+ if (packedSubtype === CHAT_HISTORY_APPMSG_TYPE) return true;
6897
+ return /<type>\s*19\s*<\/type>/.test(content);
6898
+ }
6899
+ function shouldPollInboundMedia(msg) {
6900
+ const baseType = msg.type & 2147483647;
6901
+ if (DOWNLOADABLE_MEDIA_TYPES.has(baseType)) return true;
6902
+ return baseType === 49;
6903
+ }
6904
+ function nestedChatHistoryMedia(result) {
6905
+ return (result?.items ?? []).filter(
6906
+ (item) => Boolean(item?.data) && Boolean(item?.type) && item.type !== "unsupported"
6907
+ );
6908
+ }
6909
+ function isUnsupportedInboundMedia(result) {
6910
+ return result?.type === "unsupported";
6911
+ }
6912
+ function mediaFlagsFromPollResult(result, baseType) {
6913
+ if (nestedChatHistoryMedia(result).length > 0) {
6914
+ return { hasMedia: true };
6915
+ }
6916
+ if (result && result.data && result.type !== "unsupported") {
6917
+ return { hasMedia: true };
6918
+ }
6919
+ if (isUnsupportedInboundMedia(result)) {
6920
+ return { hasMedia: false };
6921
+ }
6922
+ if (result?.errorCode || result && result.type !== "unsupported" || DOWNLOADABLE_MEDIA_TYPES.has(baseType)) {
6923
+ return {
6924
+ hasMedia: true,
6925
+ mediaErrorCode: result?.errorCode ?? "MEDIA_DOWNLOAD_UNAVAILABLE"
6926
+ };
6927
+ }
6928
+ return { hasMedia: false };
6929
+ }
6792
6930
  var RETRYABLE_MEDIA_ERRORS = /* @__PURE__ */ new Set([
6793
6931
  "MEDIA_NOT_DOWNLOADED",
6794
6932
  "IMAGE_RESOURCE_UNAVAILABLE",
6795
6933
  "FILE_NOT_DOWNLOADED",
6796
6934
  "FILE_NOT_STABLE",
6797
6935
  "VOICE_NOT_DOWNLOADED",
6798
- "IMAGE_NOT_STABLE"
6936
+ "IMAGE_NOT_STABLE",
6937
+ "CHAT_HISTORY_NOT_MATERIALIZED"
6799
6938
  ]);
6800
6939
  var DEFAULT_MEDIA_POLL_ATTEMPTS = 6;
6801
6940
  var DEFAULT_MEDIA_POLL_INTERVAL_MS = 500;
6802
6941
  var IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT_MS = 400;
6803
6942
  var FILE_MATERIALIZATION_CLICK_TIMEOUT_MS = 8e3;
6943
+ var CHAT_HISTORY_MATERIALIZATION_CLICK_TIMEOUT_MS = 12e3;
6804
6944
  function shouldTriggerMediaMaterialization(result) {
6805
6945
  return result.type === "image" || result.type === "file" || result.type === "pending";
6806
6946
  }
@@ -6834,6 +6974,50 @@ function triggerFileBubbleClick(client, chatId, result, attempt, options) {
6834
6974
  const clicked = Promise.resolve(client.downloadFile(chatId, filename, void 0, timeoutMs));
6835
6975
  void clicked.catch(() => void 0);
6836
6976
  }
6977
+ function shouldTriggerChatHistoryMaterialization(result) {
6978
+ if (result.errorCode === "CHAT_HISTORY_NOT_MATERIALIZED") return true;
6979
+ if (result.type === "pending" && result.errorCode === "IMAGE_RESOURCE_UNAVAILABLE") return true;
6980
+ if (result.type === "pending" && !result.items?.some((item) => item?.data)) return true;
6981
+ return false;
6982
+ }
6983
+ function triggerChatHistoryMaterialize(client, chatId, result, attempt, options) {
6984
+ const timeoutMs = options?.timeoutMs ?? CHAT_HISTORY_MATERIALIZATION_CLICK_TIMEOUT_MS;
6985
+ options?.log?.info?.(
6986
+ `[wechat:media] triggering chat-history materialize attempt=${attempt} title=${options?.title ?? ""} localId=${options?.localId ?? ""} skipOpen=${options?.skipOpen === true}`
6987
+ );
6988
+ if (!client.materializeChatHistory) {
6989
+ throw Object.assign(new Error("CHAT_HISTORY_MATERIALIZE_TRIGGER_UNAVAILABLE"), {
6990
+ code: "CHAT_HISTORY_MATERIALIZE_TRIGGER_UNAVAILABLE"
6991
+ });
6992
+ }
6993
+ const clicked = Promise.resolve(
6994
+ client.materializeChatHistory(
6995
+ chatId,
6996
+ {
6997
+ title: options?.title,
6998
+ localId: options?.localId,
6999
+ executionTimeoutMs: timeoutMs
7000
+ },
7001
+ void 0
7002
+ )
7003
+ );
7004
+ void clicked.catch(() => void 0);
7005
+ }
7006
+ function createChatHistoryMaterializationTrigger(client, chatId, options) {
7007
+ return async (result, attempt) => {
7008
+ if (!shouldTriggerChatHistoryMaterialization(result)) return;
7009
+ triggerChatHistoryMaterialize(client, chatId, result, attempt, options);
7010
+ };
7011
+ }
7012
+ function chatHistoryTitleFromMessage(msg) {
7013
+ const fromForwarded = msg.forwarded?.title?.trim();
7014
+ if (fromForwarded) return fromForwarded;
7015
+ const content = msg.content ?? "";
7016
+ const en = content.match(/^\[Chat History\]\s*([^\n]+)/);
7017
+ if (en?.[1]?.trim()) return en[1].trim();
7018
+ const zh = content.match(/^\[聊天记录\]\s*([^\n]+)/);
7019
+ return zh?.[1]?.trim() || void 0;
7020
+ }
6837
7021
  function createMediaMaterializationTrigger(client, chatId, options) {
6838
7022
  const timeoutMs = options?.timeoutMs ?? IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT_MS;
6839
7023
  return async (result, attempt) => {
@@ -6847,6 +7031,16 @@ function createMediaMaterializationTrigger(client, chatId, options) {
6847
7031
  }
6848
7032
  function mediaMaterializationTriggerForMessage(opts) {
6849
7033
  const baseType = opts.messageType & 2147483647;
7034
+ if (opts.chatHistory) {
7035
+ const extra = typeof opts.chatHistory === "object" ? opts.chatHistory : {};
7036
+ return createChatHistoryMaterializationTrigger(opts.client, opts.chatId, {
7037
+ log: opts.log,
7038
+ timeoutMs: opts.timeoutMs ?? CHAT_HISTORY_MATERIALIZATION_CLICK_TIMEOUT_MS,
7039
+ skipOpen: opts.skipOpen,
7040
+ title: extra.title,
7041
+ localId: extra.localId
7042
+ });
7043
+ }
6850
7044
  if (baseType !== 3 && baseType !== 49) return void 0;
6851
7045
  return createMediaMaterializationTrigger(opts.client, opts.chatId, {
6852
7046
  log: opts.log,
@@ -7533,9 +7727,29 @@ function applyEmptyUnreadSkip(chatId, opts) {
7533
7727
  opts.backoff.set(chatId, { nextRetryAt: now + backoffMs, retryCount });
7534
7728
  return { seededLastSeen, backoffMs };
7535
7729
  }
7730
+ function stickyUnreadTip(chat) {
7731
+ return `${chat.lastMsgLocalId ?? ""}:${chat.unreadCount ?? 0}`;
7732
+ }
7733
+ function isStickyUnclearedAcked(chat, acked) {
7734
+ const chatId = chat.username ?? chat.id;
7735
+ return acked.get(chatId) === stickyUnreadTip(chat);
7736
+ }
7737
+ function ackStickyUnclearedUnread(chatId, chat, acked, backoff, now = Date.now()) {
7738
+ const tip = stickyUnreadTip(chat);
7739
+ acked.set(chatId, tip);
7740
+ const retryCount = (backoff.get(chatId)?.retryCount ?? 0) + 1;
7741
+ const backoffMs = Math.min(
7742
+ EMPTY_UNREAD_BACKOFF_MAX_MS,
7743
+ EMPTY_UNREAD_BACKOFF_MS * 2 ** Math.min(retryCount - 1, 6)
7744
+ );
7745
+ backoff.set(chatId, { nextRetryAt: now + backoffMs, retryCount });
7746
+ return { tip, backoffMs };
7747
+ }
7748
+ function isOpenImChat(chatId) {
7749
+ return chatId.includes("@openim");
7750
+ }
7536
7751
 
7537
7752
  // src/monitor.ts
7538
- var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34, 43]);
7539
7753
  var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
7540
7754
  var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
7541
7755
  function sleep(ms, signal) {
@@ -7589,6 +7803,7 @@ async function startWeChatMonitor(opts) {
7589
7803
  };
7590
7804
  trimPendingResetRetries(retryState);
7591
7805
  const newsappHandled = /* @__PURE__ */ new Map();
7806
+ const stickyUnreadAck = /* @__PURE__ */ new Map();
7592
7807
  const groupHistory = /* @__PURE__ */ new Map();
7593
7808
  const GROUP_HISTORY_LIMIT = 50;
7594
7809
  let lastAuthCheck = 0;
@@ -7604,6 +7819,13 @@ async function startWeChatMonitor(opts) {
7604
7819
  while (!abortSignal.aborted) {
7605
7820
  try {
7606
7821
  const cfg = getWeChatRuntime().config.current();
7822
+ const liveAccountForInterest = resolveWeChatAccount(cfg, account.accountId) ?? account;
7823
+ const { interest: nextInterest, synced: interestSynced } = await ensureMonitorInterestSynced(
7824
+ client,
7825
+ liveAccountForInterest,
7826
+ log
7827
+ );
7828
+ chatScanState.interestOnly = shouldListChatsInterestOnly(nextInterest) && interestSynced;
7607
7829
  const now = Date.now();
7608
7830
  if (now - lastMediaCleanup >= MEDIA_RETENTION_MS / 4) {
7609
7831
  lastMediaCleanup = now;
@@ -7689,6 +7911,7 @@ async function startWeChatMonitor(opts) {
7689
7911
  const unreadChats = chats.filter((c) => {
7690
7912
  const chatId = c.username ?? c.id;
7691
7913
  if (isEmptyUnreadBackoffActive(emptyUnreadBackoff, chatId)) return false;
7914
+ if (isStickyUnclearedAcked(c, stickyUnreadAck)) return false;
7692
7915
  if (c.unreadCount <= 0) return false;
7693
7916
  if (isNewsappChat(c)) return true;
7694
7917
  return !isOfficialAccount(chatId);
@@ -7703,16 +7926,23 @@ async function startWeChatMonitor(opts) {
7703
7926
  chats,
7704
7927
  retryState,
7705
7928
  Date.now(),
7706
- (chat) => !isNewsappChat(chat) || newsappHandled.get(chat.username ?? chat.id) !== `${chat.lastMsgLocalId ?? ""}:${chat.lastMessagePreview?.trim() ?? ""}`
7929
+ (chat) => {
7930
+ if (isStickyUnclearedAcked(chat, stickyUnreadAck)) return false;
7931
+ if (!isNewsappChat(chat)) return true;
7932
+ return newsappHandled.get(chat.username ?? chat.id) !== `${chat.lastMsgLocalId ?? ""}:${chat.lastMessagePreview?.trim() ?? ""}`;
7933
+ }
7707
7934
  );
7708
7935
  for (const chatId of chatsToProcess.keys()) {
7709
7936
  if (isOfficialAccount(chatId) && chatId !== "newsapp" || isEmptyUnreadBackoffActive(emptyUnreadBackoff, chatId)) {
7710
7937
  chatsToProcess.delete(chatId);
7711
7938
  }
7712
7939
  }
7713
- if (unreadChats.length > 0) {
7940
+ const actionableUnread = unreadChats.filter(
7941
+ (c) => chatsToProcess.has(c.username ?? c.id)
7942
+ );
7943
+ if (actionableUnread.length > 0) {
7714
7944
  log?.info?.(
7715
- `[wechat:${account.accountId}] ${unreadChats.length} chat(s) with unreads`
7945
+ `[wechat:${account.accountId}] ${actionableUnread.length} chat(s) with unreads`
7716
7946
  );
7717
7947
  }
7718
7948
  let deferredThisTick = 0;
@@ -7731,6 +7961,7 @@ async function startWeChatMonitor(opts) {
7731
7961
  inboundLedger,
7732
7962
  mediaPipeline,
7733
7963
  emptyUnreadBackoff,
7964
+ stickyUnreadAck,
7734
7965
  account,
7735
7966
  cfg,
7736
7967
  log,
@@ -7780,6 +8011,7 @@ async function startWeChatMonitor(opts) {
7780
8011
  inboundLedger,
7781
8012
  mediaPipeline,
7782
8013
  emptyUnreadBackoff,
8014
+ stickyUnreadAck,
7783
8015
  account,
7784
8016
  cfg,
7785
8017
  log,
@@ -7849,11 +8081,14 @@ async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAcco
7849
8081
  let mediaMime;
7850
8082
  let mediaHash;
7851
8083
  let mediaPreviewPath;
8084
+ let mediaPaths;
8085
+ let mediaMimes;
7852
8086
  let mediaSource;
7853
8087
  let mediaErrorCode;
7854
8088
  let hasMedia = false;
7855
8089
  const baseType = msg.type & 2147483647;
7856
- const mayHaveMedia = MEDIA_TYPES.has(baseType) || baseType === 49;
8090
+ const isChatHistory = isWeChatChatHistoryMessage(msg);
8091
+ const mayHaveMedia = shouldPollInboundMedia(msg);
7857
8092
  if (mayHaveMedia) {
7858
8093
  log?.info?.(`[wechat:${liveAccount.accountId}] Checking media for msg ${msg.localId} (type ${baseType})`);
7859
8094
  try {
@@ -7869,10 +8104,54 @@ async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAcco
7869
8104
  chatId,
7870
8105
  messageType: baseType,
7871
8106
  log,
7872
- skipOpen
8107
+ skipOpen,
8108
+ chatHistory: isChatHistory ? {
8109
+ title: chatHistoryTitleFromMessage(msg),
8110
+ localId: msg.localId
8111
+ } : void 0
7873
8112
  })
7874
8113
  );
7875
- if (result && result.data && result.type !== "unsupported") {
8114
+ const nestedItems = nestedChatHistoryMedia(result);
8115
+ if (isChatHistory && nestedItems.length > 0) {
8116
+ const savedPaths = [];
8117
+ const savedMimes = [];
8118
+ for (const [index, item] of nestedItems.entries()) {
8119
+ const saved = await mediaPipeline.process(
8120
+ item,
8121
+ {
8122
+ eventId: inboundEventId(liveAccount.accountId, chatId, {
8123
+ ...msg,
8124
+ localId: msg.localId * 1e3 + index
8125
+ }),
8126
+ chatId,
8127
+ localId: msg.localId * 1e3 + index
8128
+ },
8129
+ async (buffer, mime, filename) => core.channel.media.saveMediaBuffer(buffer, mime, "inbound", void 0, filename),
8130
+ async (buffer, mime, filename) => core.channel.media.saveMediaBuffer(buffer, mime, "inbound", void 0, filename),
8131
+ async (path) => unlink(path)
8132
+ );
8133
+ if (!saved.ok) {
8134
+ log?.error?.(`[wechat:media] nested chat-history media rejected index=${index} code=${saved.code}`);
8135
+ continue;
8136
+ }
8137
+ if (!saved.originalPath || savedPaths.includes(saved.originalPath)) continue;
8138
+ savedPaths.push(saved.originalPath);
8139
+ savedMimes.push(saved.mime);
8140
+ if (!mediaPath) {
8141
+ mediaPath = saved.originalPath;
8142
+ mediaMime = saved.mime;
8143
+ mediaHash = saved.hash;
8144
+ mediaPreviewPath = saved.previewPath;
8145
+ mediaSource = item.source;
8146
+ }
8147
+ }
8148
+ if (savedPaths.length > 0) {
8149
+ hasMedia = true;
8150
+ mediaPaths = savedPaths;
8151
+ mediaMimes = savedMimes;
8152
+ log?.info?.(`[wechat:media] nested chat-history media saved count=${savedPaths.length}`);
8153
+ }
8154
+ } else if (result && result.data && result.type !== "unsupported") {
7876
8155
  hasMedia = true;
7877
8156
  mediaSource = result.source;
7878
8157
  log?.info?.(`[wechat:media] inbound media received type=${result.type} format=${result.format}`);
@@ -7893,15 +8172,22 @@ async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAcco
7893
8172
  mediaPreviewPath = saved.previewPath;
7894
8173
  log?.info?.(`[wechat:media] inbound media saved hash=${saved.hash}${saved.previewPath ? " preview=present" : ""}`);
7895
8174
  }
7896
- } else if (result?.errorCode || result && result.type !== "unsupported" || MEDIA_TYPES.has(baseType)) {
7897
- hasMedia = true;
7898
- mediaErrorCode = result?.errorCode ?? "MEDIA_DOWNLOAD_UNAVAILABLE";
7899
- log?.info?.(`[wechat:media] inbound media unavailable code=${mediaErrorCode}`);
8175
+ } else {
8176
+ const flags = mediaFlagsFromPollResult(result, baseType);
8177
+ hasMedia = flags.hasMedia;
8178
+ mediaErrorCode = isChatHistory ? void 0 : flags.mediaErrorCode;
8179
+ if (mediaErrorCode) {
8180
+ log?.info?.(`[wechat:media] inbound media unavailable code=${mediaErrorCode}`);
8181
+ }
7900
8182
  }
7901
8183
  } catch (err) {
7902
- hasMedia = true;
7903
- mediaErrorCode = "MEDIA_DOWNLOAD_FAILED";
7904
- log?.error?.("[wechat:media] inbound media failed code=MEDIA_DOWNLOAD_FAILED");
8184
+ if (isChatHistory) {
8185
+ log?.error?.("[wechat:media] nested chat-history media failed code=MEDIA_DOWNLOAD_FAILED");
8186
+ } else {
8187
+ hasMedia = true;
8188
+ mediaErrorCode = "MEDIA_DOWNLOAD_FAILED";
8189
+ log?.error?.("[wechat:media] inbound media failed code=MEDIA_DOWNLOAD_FAILED");
8190
+ }
7905
8191
  }
7906
8192
  }
7907
8193
  const timestamp = new Date(msg.timestamp).getTime();
@@ -7918,10 +8204,8 @@ async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAcco
7918
8204
  } else if (!mediaMime.startsWith("image/") && !mediaMime.startsWith("audio/")) {
7919
8205
  rawBody = `[File: ${rawBody}]`;
7920
8206
  }
7921
- } else if (mediaErrorCode && baseType === 49) {
7922
- const text = msg.content || "File attachment";
7923
- rawBody = `${text}
7924
- [Attachment unavailable: ${mediaErrorCode}]`;
8207
+ } else if (mediaErrorCode && baseType === 49 && !isChatHistory) {
8208
+ rawBody = formatType49MediaFailureBody(msg.content || "", mediaErrorCode);
7925
8209
  }
7926
8210
  if (mediaSource === "thumbnail") {
7927
8211
  rawBody = rawBody ? `${rawBody}
@@ -7950,6 +8234,8 @@ ${replyBlock}` : replyBlock;
7950
8234
  mediaMime,
7951
8235
  mediaHash,
7952
8236
  mediaPreviewPath,
8237
+ mediaPaths,
8238
+ mediaMimes,
7953
8239
  senderName,
7954
8240
  senderId,
7955
8241
  isGroup,
@@ -7981,9 +8267,11 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
7981
8267
  const core = getWeChatRuntime();
7982
8268
  const lastMsg = segment[segment.length - 1];
7983
8269
  const { isGroup, senderId, senderName, timestamp, rawBody, commandBody, msg } = lastMsg;
7984
- const mediaMsg = segment.find((pm) => pm.mediaPath);
7985
- const mediaPath = mediaMsg?.mediaPath;
7986
- const mediaMime = mediaMsg?.mediaMime;
8270
+ const mediaMsg = segment.find((pm) => pm.mediaPath || pm.mediaPaths && pm.mediaPaths.length > 0);
8271
+ const mediaPaths = mediaMsg?.mediaPaths?.length ? mediaMsg.mediaPaths : mediaMsg?.mediaPath ? [mediaMsg.mediaPath] : [];
8272
+ const mediaMimes = mediaMsg?.mediaMimes?.length ? mediaMsg.mediaMimes : mediaMsg?.mediaMime ? [mediaMsg.mediaMime] : [];
8273
+ const mediaPath = mediaPaths[0];
8274
+ const mediaMime = mediaMimes[0];
7987
8275
  log?.info?.(
7988
8276
  `[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ? " media=present" : ""}`
7989
8277
  );
@@ -8119,9 +8407,9 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
8119
8407
  MediaPath: mediaPath,
8120
8408
  MediaUrl: mediaPath,
8121
8409
  MediaType: mediaMime,
8122
- MediaPaths: [mediaPath],
8123
- MediaUrls: [mediaPath],
8124
- MediaTypes: mediaMime ? [mediaMime] : [],
8410
+ MediaPaths: mediaPaths,
8411
+ MediaUrls: mediaPaths,
8412
+ MediaTypes: mediaMimes,
8125
8413
  ...mediaMsg?.mediaHash ? { MediaHash: mediaMsg.mediaHash } : {},
8126
8414
  ...mediaMsg?.mediaPreviewPath ? { MediaPreviewPath: mediaMsg.mediaPreviewPath } : {}
8127
8415
  } : {},
@@ -8250,7 +8538,7 @@ function bufferGroupHistory(groupHistory, chatId, pm, limit) {
8250
8538
  if (first) groupHistory.delete(first);
8251
8539
  }
8252
8540
  }
8253
- async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHandled, startupBaselines, pendingMessageScans, persistRetryState, inboundLedger, mediaPipeline, emptyUnreadBackoff, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, catchup) {
8541
+ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHandled, startupBaselines, pendingMessageScans, persistRetryState, inboundLedger, mediaPipeline, emptyUnreadBackoff, stickyUnreadAck, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, catchup) {
8254
8542
  const core = getWeChatRuntime();
8255
8543
  const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
8256
8544
  const chatId = chat.username ?? chat.id;
@@ -8417,11 +8705,26 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
8417
8705
  );
8418
8706
  }
8419
8707
  if (chat.unreadCount > 0 && !isNewsappChat(chat)) {
8420
- await openChatIfNeeded();
8708
+ const sticky = ackStickyUnclearedUnread(
8709
+ chatId,
8710
+ chat,
8711
+ stickyUnreadAck,
8712
+ emptyUnreadBackoff
8713
+ );
8714
+ log?.info?.(
8715
+ `[wechat:${liveAccount.accountId}] ${chatId}: ignoring sticky unread after catch-up (unreadCount=${chat.unreadCount}, tip=${sticky.tip}, openim=${isOpenImChat(chatId)}); badge kept; backoff=${sticky.backoffMs}ms`
8716
+ );
8421
8717
  }
8422
8718
  return "skipped";
8423
8719
  }
8424
- if (!isNewsappChat(chat)) await openChatIfNeeded();
8720
+ const anyAllowlistedInbound = newMessages.some((msg) => {
8721
+ if (msg.isSelf) return false;
8722
+ if (shouldBypassNewsappAuthorization(chat)) return true;
8723
+ const isGroup2 = chatId.includes("@chatroom");
8724
+ const senderId = msg.sender ?? chatId;
8725
+ return resolveWeChatInboundAccessDecision({ isGroup: isGroup2, senderId, policy }).allowed;
8726
+ });
8727
+ if (anyAllowlistedInbound && !isNewsappChat(chat)) await openChatIfNeeded();
8425
8728
  const catchUpLimits = {
8426
8729
  maxMessages: liveAccount.catchUpMaxMessages,
8427
8730
  maxAgeMs: liveAccount.catchUpMaxAgeMs
@@ -8517,6 +8820,8 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
8517
8820
  ...processed[i],
8518
8821
  mediaPath: void 0,
8519
8822
  mediaMime: void 0,
8823
+ mediaPaths: void 0,
8824
+ mediaMimes: void 0,
8520
8825
  hasMedia: false
8521
8826
  };
8522
8827
  }
@@ -8618,6 +8923,17 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
8618
8923
  }
8619
8924
  const maxId = Math.max(...newMessages.map((m) => m.localId));
8620
8925
  advanceLastSeen(newMessages.filter((message) => message.localId <= maxId));
8926
+ if (chat.unreadCount > 0 && !isNewsappChat(chat)) {
8927
+ const sticky = ackStickyUnclearedUnread(
8928
+ chatId,
8929
+ chat,
8930
+ stickyUnreadAck,
8931
+ emptyUnreadBackoff
8932
+ );
8933
+ log?.info?.(
8934
+ `[wechat:${liveAccount.accountId}] ${chatId}: ignoring sticky unread after filtered window (tip=${sticky.tip}, openim=${isOpenImChat(chatId)}); badge kept; backoff=${sticky.backoffMs}ms`
8935
+ );
8936
+ }
8621
8937
  return "skipped";
8622
8938
  }
8623
8939
 
@@ -9295,7 +9611,7 @@ var wechatPlugin = {
9295
9611
  allowFrom: account.allowFrom ?? [],
9296
9612
  allowFromPath: `channels.${OPENCLAW_CHANNEL_ID}.allowFrom`,
9297
9613
  policyPath: `channels.${OPENCLAW_CHANNEL_ID}.dmPolicy`,
9298
- approveHint: `Add the wxid to channels.${OPENCLAW_CHANNEL_ID}.allowFrom`,
9614
+ approveHint: `Add the wxid or enterprise @openim id to channels.${OPENCLAW_CHANNEL_ID}.allowFrom`,
9299
9615
  normalizeEntry: (raw) => raw.replace(/^(agent-wechat|wechat):/i, "").trim()
9300
9616
  })
9301
9617
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyan-du/agent-wechat-openclaw",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",