@kyan-du/agent-wechat-openclaw 0.12.0-next.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/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,
@@ -5592,6 +5594,248 @@ function selectCatchUpMessages(messages, limits, mode = "latest") {
5592
5594
  };
5593
5595
  }
5594
5596
 
5597
+ // src/monitor-cursor.ts
5598
+ function cursorMessageKey(message) {
5599
+ return `${message.serverId ?? ""}:${message.timestamp}:${message.type}:${message.sender ?? ""}:${message.content ?? ""}`;
5600
+ }
5601
+ function markEqualCursorUnreadHandled(chatId, message, equalCursorUnreadHandled) {
5602
+ if (message === void 0) return;
5603
+ equalCursorUnreadHandled.set(chatId, {
5604
+ localId: message.localId,
5605
+ messageKey: cursorMessageKey(message)
5606
+ });
5607
+ }
5608
+ function markCursorMessagesHandled(chatId, messages, equalCursorUnreadHandled) {
5609
+ if (messages.length === 0) return void 0;
5610
+ const cursorMessage = messages.reduce(
5611
+ (latest, message) => message.localId > latest.localId ? message : latest
5612
+ );
5613
+ markEqualCursorUnreadHandled(chatId, cursorMessage, equalCursorUnreadHandled);
5614
+ return cursorMessage.localId;
5615
+ }
5616
+ function selectCursorMessages(chatId, chat, messages, lastSeenId, equalCursorUnreadHandled, startupBaseline, generationResetConfirmed = false) {
5617
+ const firstPoll = !lastSeenId.has(chatId);
5618
+ const prevLastSeen = lastSeenId.get(chatId) ?? 0;
5619
+ const sorted = [...messages].sort((a, b) => a.localId - b.localId);
5620
+ const baselineActive = startupBaseline !== void 0 && (firstPoll || prevLastSeen < startupBaseline.localId);
5621
+ if (baselineActive) {
5622
+ const baselineRow = sorted.find((message) => message.localId === startupBaseline.localId);
5623
+ const generationReset = sorted.length > 0 && typeof chat.lastMsgLocalId === "number" && chat.lastMsgLocalId <= startupBaseline.localId && (baselineRow === void 0 && sorted.every((message) => message.localId < startupBaseline.localId) || baselineRow !== void 0 && startupBaseline.messageKey !== void 0 && cursorMessageKey(baselineRow) !== startupBaseline.messageKey);
5624
+ if (generationReset || generationResetConfirmed) {
5625
+ return {
5626
+ firstPoll,
5627
+ prevLastSeen,
5628
+ messages: sorted,
5629
+ generationReset: true
5630
+ };
5631
+ }
5632
+ const live = sorted.filter(
5633
+ (message) => message.localId > startupBaseline.localId || message.localId === startupBaseline.localId && startupBaseline.messageKey !== void 0 && cursorMessageKey(message) !== startupBaseline.messageKey
5634
+ );
5635
+ const seedLastSeen = sorted.filter((message) => message.localId <= startupBaseline.localId).at(-1)?.localId;
5636
+ return {
5637
+ firstPoll,
5638
+ prevLastSeen,
5639
+ messages: live,
5640
+ seedLastSeen
5641
+ };
5642
+ }
5643
+ if (firstPoll) {
5644
+ const unread2 = chat.unreadCount ?? 0;
5645
+ if (unread2 > 0 && unread2 < sorted.length) {
5646
+ return {
5647
+ firstPoll,
5648
+ prevLastSeen,
5649
+ messages: sorted.slice(-unread2),
5650
+ seedLastSeen: sorted[sorted.length - unread2 - 1].localId
5651
+ };
5652
+ }
5653
+ if (unread2 >= sorted.length) {
5654
+ return { firstPoll, prevLastSeen, messages: sorted };
5655
+ }
5656
+ return {
5657
+ firstPoll,
5658
+ prevLastSeen,
5659
+ messages: [],
5660
+ seedLastSeen: sorted[sorted.length - 1]?.localId
5661
+ };
5662
+ }
5663
+ const newer = sorted.filter((m) => m.localId > prevLastSeen);
5664
+ if (newer.length > 0) {
5665
+ return { firstPoll, prevLastSeen, messages: newer };
5666
+ }
5667
+ const unread = chat.unreadCount ?? 0;
5668
+ const handled = equalCursorUnreadHandled.get(chatId);
5669
+ const equalCursorRows = sorted.filter((message) => message.localId === prevLastSeen);
5670
+ const equalIdGenerationReset = unread > 0 && chat.lastMsgLocalId === prevLastSeen && sorted.every((message) => message.localId <= prevLastSeen) && equalCursorRows.length === 1 && handled?.localId === prevLastSeen && handled.messageKey !== cursorMessageKey(equalCursorRows[0]);
5671
+ const lowerIdGenerationReset = unread > 0 && sorted.length > 0 && sorted.every((message) => message.localId < prevLastSeen) && typeof chat.lastMsgLocalId === "number" && chat.lastMsgLocalId < prevLastSeen;
5672
+ if (lowerIdGenerationReset || equalIdGenerationReset || generationResetConfirmed) {
5673
+ return {
5674
+ firstPoll,
5675
+ prevLastSeen,
5676
+ messages: sorted,
5677
+ generationReset: true
5678
+ };
5679
+ }
5680
+ if (unread <= 0) {
5681
+ return { firstPoll, prevLastSeen, messages: [] };
5682
+ }
5683
+ const unreadSuffix = sorted.slice(-Math.min(unread, sorted.length));
5684
+ const equalCursorUnread = unreadSuffix.filter((m) => m.localId === prevLastSeen);
5685
+ const recoverable = equalCursorUnread.length === 1 && chat.lastMsgLocalId === prevLastSeen && (handled?.localId !== prevLastSeen || handled.messageKey !== cursorMessageKey(equalCursorUnread[0]));
5686
+ return {
5687
+ firstPoll,
5688
+ prevLastSeen,
5689
+ messages: recoverable ? equalCursorUnread : []
5690
+ };
5691
+ }
5692
+ function seedStartupBaselineFromChat(chat) {
5693
+ return typeof chat.lastMsgLocalId === "number" && chat.lastMsgLocalId > 0 ? { localId: chat.lastMsgLocalId } : void 0;
5694
+ }
5695
+ function enrichStartupBaselineFromMessages(baseline, messages) {
5696
+ if (baseline === void 0 || baseline.messageKey !== void 0) return baseline;
5697
+ const match = messages.find((message) => message.localId === baseline.localId);
5698
+ return match ? { ...baseline, messageKey: cursorMessageKey(match) } : baseline;
5699
+ }
5700
+ function selectMessagesHandledAfterDispatch(messages, successfulSegmentLastLocalIds) {
5701
+ if (successfulSegmentLastLocalIds.length === 0) return [];
5702
+ const lastHandled = Math.max(...successfulSegmentLastLocalIds);
5703
+ return messages.filter((message) => message.localId <= lastHandled);
5704
+ }
5705
+
5706
+ // src/monitor-scan.ts
5707
+ var MONITOR_CHAT_PAGE_LIMIT = 100;
5708
+ var MONITOR_MESSAGE_PAGE_LIMIT = 200;
5709
+ var DEFAULT_MESSAGE_PAGE_BUDGET = 50;
5710
+ var DEFAULT_INITIAL_CHAT_PAGE_BUDGET = 100;
5711
+ async function listInitialMonitorChatSnapshot(client, state, pageBudget = DEFAULT_INITIAL_CHAT_PAGE_BUDGET) {
5712
+ if (state.initialScanComplete) return [];
5713
+ const chats = [];
5714
+ let cursor;
5715
+ for (let pages = 0; pages < pageBudget; pages += 1) {
5716
+ const page = await client.listChatsPage(MONITOR_CHAT_PAGE_LIMIT, cursor);
5717
+ chats.push(...page.items);
5718
+ cursor = page.nextCursor;
5719
+ if (!cursor) {
5720
+ state.initialScanComplete = true;
5721
+ state.cursor = void 0;
5722
+ return chats;
5723
+ }
5724
+ }
5725
+ throw new Error(`initial chat snapshot exceeded ${pageBudget} pages`);
5726
+ }
5727
+ async function listNextMonitorChatPage(client, state) {
5728
+ const isInitialSnapshot = !state.initialScanComplete;
5729
+ const page = await client.listChatsPage(MONITOR_CHAT_PAGE_LIMIT, state.cursor);
5730
+ state.cursor = page.nextCursor;
5731
+ if (!page.nextCursor) {
5732
+ state.initialScanComplete = true;
5733
+ state.cursor = void 0;
5734
+ }
5735
+ return { chats: page.items, isInitialSnapshot };
5736
+ }
5737
+ function hasReachedStableMessageBoundary(messages, chat, firstPoll, prevLastSeen, startupBaseline) {
5738
+ if (messages.length === 0) return false;
5739
+ const sorted = [...messages].sort((a, b) => a.localId - b.localId);
5740
+ if (startupBaseline !== void 0 && (firstPoll || prevLastSeen < startupBaseline.localId)) {
5741
+ if (startupBaseline.localId <= 0) return true;
5742
+ if (sorted.some((message) => message.localId === startupBaseline.localId)) return true;
5743
+ return false;
5744
+ }
5745
+ if (firstPoll) {
5746
+ const unread2 = chat.unreadCount ?? 0;
5747
+ if (unread2 > 0) return messages.length >= unread2 + 1;
5748
+ return true;
5749
+ }
5750
+ if (sorted.some((message) => message.localId <= prevLastSeen)) return true;
5751
+ const unread = chat.unreadCount ?? 0;
5752
+ return unread > 0 && messages.length >= unread;
5753
+ }
5754
+ function detectsGenerationReset(messages, chat, firstPoll, prevLastSeen, startupBaseline, handledCursor) {
5755
+ if (messages.length === 0 || typeof chat.lastMsgLocalId !== "number") return false;
5756
+ const boundary = startupBaseline !== void 0 && (firstPoll || prevLastSeen < startupBaseline.localId) ? startupBaseline : handledCursor?.localId === prevLastSeen ? handledCursor : void 0;
5757
+ const boundaryLocalId = boundary?.localId ?? prevLastSeen;
5758
+ if (chat.lastMsgLocalId < boundaryLocalId) return true;
5759
+ if (chat.lastMsgLocalId !== boundaryLocalId || boundary?.messageKey === void 0) return false;
5760
+ const equalRow = messages.find((message) => message.localId === boundaryLocalId);
5761
+ return equalRow !== void 0 && cursorMessageKey(equalRow) !== boundary.messageKey;
5762
+ }
5763
+ function generationBoundaryIndex(messages) {
5764
+ for (let index = 1; index < messages.length; index += 1) {
5765
+ if (messages[index].localId >= messages[index - 1].localId) return index;
5766
+ }
5767
+ return void 0;
5768
+ }
5769
+ function shouldProbeGenerationReset(messages, chat, firstPoll, prevLastSeen, startupBaseline, handledCursor) {
5770
+ if (typeof chat.lastMsgLocalId !== "number") return false;
5771
+ const boundary = startupBaseline !== void 0 && (firstPoll || prevLastSeen < startupBaseline.localId) ? startupBaseline : handledCursor?.localId === prevLastSeen ? handledCursor : void 0;
5772
+ if (boundary === void 0 || chat.lastMsgLocalId > boundary.localId) return false;
5773
+ return !messages.some((message) => message.localId === boundary.localId);
5774
+ }
5775
+ async function listMessagesForMonitorCursor(client, chatId, options) {
5776
+ const pageBudget = options.pageBudget ?? DEFAULT_MESSAGE_PAGE_BUDGET;
5777
+ const messages = [...options.continuation?.messages ?? []];
5778
+ let cursor = options.continuation?.cursor;
5779
+ let generationReset = options.continuation?.generationReset ?? false;
5780
+ let pagesScanned = 0;
5781
+ while (pagesScanned < pageBudget) {
5782
+ const page = await client.listMessagesPage(chatId, MONITOR_MESSAGE_PAGE_LIMIT, cursor);
5783
+ pagesScanned += 1;
5784
+ messages.push(...page.items);
5785
+ cursor = page.nextCursor;
5786
+ generationReset ||= detectsGenerationReset(
5787
+ messages,
5788
+ options.chat,
5789
+ options.firstPoll,
5790
+ options.prevLastSeen,
5791
+ options.startupBaseline,
5792
+ options.handledCursor
5793
+ );
5794
+ if (generationReset) {
5795
+ const boundaryIndex = generationBoundaryIndex(messages);
5796
+ if (boundaryIndex !== void 0 || !cursor) {
5797
+ return {
5798
+ messages: boundaryIndex === void 0 ? messages : messages.slice(0, boundaryIndex),
5799
+ complete: true,
5800
+ nextCursor: void 0,
5801
+ pagesScanned,
5802
+ generationReset: true
5803
+ };
5804
+ }
5805
+ continue;
5806
+ }
5807
+ const probingReset = shouldProbeGenerationReset(
5808
+ messages,
5809
+ options.chat,
5810
+ options.firstPoll,
5811
+ options.prevLastSeen,
5812
+ options.startupBaseline,
5813
+ options.handledCursor
5814
+ );
5815
+ if (!cursor || !probingReset && hasReachedStableMessageBoundary(
5816
+ messages,
5817
+ options.chat,
5818
+ options.firstPoll,
5819
+ options.prevLastSeen,
5820
+ options.startupBaseline
5821
+ )) {
5822
+ return {
5823
+ messages,
5824
+ complete: true,
5825
+ nextCursor: void 0,
5826
+ pagesScanned
5827
+ };
5828
+ }
5829
+ }
5830
+ return {
5831
+ messages,
5832
+ complete: false,
5833
+ nextCursor: cursor,
5834
+ pagesScanned,
5835
+ generationReset: generationReset || void 0
5836
+ };
5837
+ }
5838
+
5595
5839
  // src/outbound.ts
5596
5840
  import { randomUUID } from "node:crypto";
5597
5841
  async function sendLogicalMediaTask(opts) {
@@ -5623,6 +5867,255 @@ async function sendLogicalMediaTask(opts) {
5623
5867
  return requestId;
5624
5868
  }
5625
5869
 
5870
+ // src/monitor-retry.ts
5871
+ import {
5872
+ existsSync,
5873
+ closeSync,
5874
+ mkdirSync,
5875
+ openSync,
5876
+ readFileSync,
5877
+ renameSync,
5878
+ rmSync,
5879
+ writeFileSync,
5880
+ fsyncSync
5881
+ } from "node:fs";
5882
+ import { homedir } from "node:os";
5883
+ import { dirname, join } from "node:path";
5884
+ var MAX_PENDING_RESET_CHATS = 100;
5885
+ var MAX_PENDING_RESET_MESSAGES = 1e4;
5886
+ var MAX_RESET_RETRY_DELAY_MS = 6e4;
5887
+ var PendingRetryStateError = class extends Error {
5888
+ code;
5889
+ constructor(message, code) {
5890
+ super(message);
5891
+ this.name = "PendingRetryStateError";
5892
+ this.code = code;
5893
+ }
5894
+ };
5895
+ function retryStorePath(accountId, stateDir) {
5896
+ const root = stateDir ?? process.env.OPENCLAW_STATE_DIR?.trim() ?? join(homedir(), ".openclaw");
5897
+ const safeAccount = accountId.replace(/[^A-Za-z0-9._-]/g, "_");
5898
+ return join(root, "wechat", `monitor-retry-${safeAccount}.json`);
5899
+ }
5900
+ function quarantinePath(path) {
5901
+ return `${path}.corrupt`;
5902
+ }
5903
+ function blockerPath(path) {
5904
+ return `${path}.blocked`;
5905
+ }
5906
+ function syncPath(path) {
5907
+ const fd = openSync(path, "r");
5908
+ try {
5909
+ fsyncSync(fd);
5910
+ } finally {
5911
+ closeSync(fd);
5912
+ }
5913
+ }
5914
+ function syncParent(path) {
5915
+ syncPath(dirname(path));
5916
+ }
5917
+ function isStringArray(value) {
5918
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
5919
+ }
5920
+ function isFiniteNumber(value) {
5921
+ return typeof value === "number" && Number.isFinite(value);
5922
+ }
5923
+ function validateMessage(message) {
5924
+ if (message === null || typeof message !== "object") return false;
5925
+ const row = message;
5926
+ return Number.isSafeInteger(row.localId) && typeof row.chatId === "string" && Number.isSafeInteger(row.type) && typeof row.timestamp === "string" && (row.serverId === void 0 || Number.isSafeInteger(row.serverId)) && (row.content === void 0 || typeof row.content === "string") && (row.sender === void 0 || typeof row.sender === "string") && (row.isSelf === void 0 || typeof row.isSelf === "boolean") && (row.isMentioned === void 0 || typeof row.isMentioned === "boolean");
5927
+ }
5928
+ function validateChat(chat) {
5929
+ if (chat === null || typeof chat !== "object") return false;
5930
+ const row = chat;
5931
+ return typeof row.id === "string" && typeof row.name === "string" && Number.isSafeInteger(row.unreadCount) && typeof row.isGroup === "boolean" && (row.username === void 0 || typeof row.username === "string") && (row.lastMsgLocalId === void 0 || Number.isSafeInteger(row.lastMsgLocalId)) && (row.remark === void 0 || typeof row.remark === "string") && (row.members === void 0 || isStringArray(row.members));
5932
+ }
5933
+ function validateEntries(entries) {
5934
+ if (entries.length > MAX_PENDING_RESET_CHATS) {
5935
+ throw new PendingRetryStateError("WeChat pending retry chat capacity exceeded", "RETRY_CAPACITY");
5936
+ }
5937
+ let total = 0;
5938
+ for (const entry of entries) {
5939
+ if (!Array.isArray(entry) || typeof entry[0] !== "string") {
5940
+ throw new PendingRetryStateError("WeChat retry state entry is invalid", "RETRY_STATE_CORRUPT");
5941
+ }
5942
+ const pending = entry[1];
5943
+ if (pending?.readyForDispatch !== true || !validateChat(pending.chat) || !Array.isArray(pending.messages) || !pending.messages.every(validateMessage) || pending.generationReset !== void 0 && typeof pending.generationReset !== "boolean" || pending.retryCount !== void 0 && !Number.isSafeInteger(pending.retryCount) || pending.nextRetryAt !== void 0 && !isFiniteNumber(pending.nextRetryAt) || pending.createdAt !== void 0 && !isFiniteNumber(pending.createdAt)) {
5944
+ throw new PendingRetryStateError("WeChat pending retry payload is invalid", "RETRY_STATE_CORRUPT");
5945
+ }
5946
+ total += pending.messages.length;
5947
+ }
5948
+ if (total > MAX_PENDING_RESET_MESSAGES) {
5949
+ throw new PendingRetryStateError("WeChat pending retry message capacity exceeded", "RETRY_CAPACITY");
5950
+ }
5951
+ }
5952
+ function loadPendingResetRetries(accountId, stateDir) {
5953
+ const path = retryStorePath(accountId, stateDir);
5954
+ const blocked = blockerPath(path);
5955
+ if (existsSync(blocked)) {
5956
+ let quarantine = quarantinePath(path);
5957
+ try {
5958
+ const marker = JSON.parse(readFileSync(blocked, "utf8"));
5959
+ if (typeof marker.quarantine === "string") quarantine = marker.quarantine;
5960
+ } catch {
5961
+ }
5962
+ throw new PendingRetryStateError(
5963
+ `WeChat retry recovery is blocked by ${blocked}. Reconcile ${quarantine}, then explicitly remove the blocker to resume.`,
5964
+ "RETRY_STATE_BLOCKED"
5965
+ );
5966
+ }
5967
+ if (!existsSync(path)) return /* @__PURE__ */ new Map();
5968
+ try {
5969
+ const raw = JSON.parse(readFileSync(path, "utf8"));
5970
+ if (raw.version !== 1 || !Array.isArray(raw.entries)) {
5971
+ throw new PendingRetryStateError("WeChat retry state schema is invalid", "RETRY_STATE_CORRUPT");
5972
+ }
5973
+ const entries = raw.entries;
5974
+ validateEntries(entries);
5975
+ return new Map(entries);
5976
+ } catch (error) {
5977
+ const quarantine = quarantinePath(path);
5978
+ const blocked2 = blockerPath(path);
5979
+ try {
5980
+ rmSync(quarantine, { force: true });
5981
+ renameSync(path, quarantine);
5982
+ writeFileSync(blocked2, JSON.stringify({ version: 1, quarantine }), { mode: 384 });
5983
+ syncPath(blocked2);
5984
+ syncParent(path);
5985
+ } catch (quarantineError) {
5986
+ throw new PendingRetryStateError(
5987
+ `WeChat retry state is unreadable at ${path}; quarantine failed (${String(quarantineError)}). Stop the channel and move this file aside manually.`,
5988
+ "RETRY_STATE_CORRUPT"
5989
+ );
5990
+ }
5991
+ throw new PendingRetryStateError(
5992
+ `WeChat retry state was quarantined to ${quarantine} and blocked by ${blocked2}. Inspect/restore the quarantine, then explicitly remove the blocker to resume or discard after reconciliation. Cause: ${String(error)}`,
5993
+ "RETRY_STATE_CORRUPT"
5994
+ );
5995
+ }
5996
+ }
5997
+ function persistPendingResetRetries(accountId, pending, stateDir) {
5998
+ const path = retryStorePath(accountId, stateDir);
5999
+ const entries = [...pending.entries()].filter(([, value]) => value.readyForDispatch);
6000
+ validateEntries(entries);
6001
+ if (entries.length === 0) {
6002
+ if (!existsSync(path)) return;
6003
+ rmSync(path, { force: true });
6004
+ if (existsSync(dirname(path))) syncParent(path);
6005
+ return;
6006
+ }
6007
+ mkdirSync(dirname(path), { recursive: true });
6008
+ const temp = `${path}.tmp-${process.pid}`;
6009
+ writeFileSync(temp, JSON.stringify({ version: 1, entries }), { mode: 384 });
6010
+ syncPath(temp);
6011
+ renameSync(temp, path);
6012
+ syncParent(path);
6013
+ }
6014
+ function persist(state) {
6015
+ state.persist?.();
6016
+ }
6017
+ function mergeMessages(current, incoming) {
6018
+ const merged = /* @__PURE__ */ new Map();
6019
+ for (const message of [...current, ...incoming]) {
6020
+ merged.set(cursorMessageKey(message), message);
6021
+ }
6022
+ return [...merged.values()].sort((a, b) => a.localId - b.localId);
6023
+ }
6024
+ function assertStateCapacity(pending) {
6025
+ validateEntries([...pending.entries()]);
6026
+ }
6027
+ function trimPendingResetRetries(state) {
6028
+ assertStateCapacity(state.pendingMessageScans);
6029
+ persist(state);
6030
+ }
6031
+ function clearPendingResetRetry(state, chatId) {
6032
+ if (state.pendingMessageScans.delete(chatId)) persist(state);
6033
+ }
6034
+ function queueResetGenerationRetry(state, chatId, chat, messages, now = Date.now()) {
6035
+ const existing = state.pendingMessageScans.get(chatId);
6036
+ const merged = mergeMessages(existing?.messages ?? [], messages);
6037
+ const next = {
6038
+ chat,
6039
+ messages: merged,
6040
+ generationReset: true,
6041
+ readyForDispatch: true,
6042
+ retryCount: existing?.retryCount ?? 0,
6043
+ nextRetryAt: existing?.nextRetryAt ?? now,
6044
+ createdAt: existing?.createdAt ?? now
6045
+ };
6046
+ const projected = new Map(state.pendingMessageScans);
6047
+ projected.set(chatId, next);
6048
+ assertStateCapacity(projected);
6049
+ state.pendingMessageScans.delete(chatId);
6050
+ state.pendingMessageScans.set(chatId, next);
6051
+ persist(state);
6052
+ }
6053
+ function mergePendingResetMessages(state, chatId, chat, messages) {
6054
+ const pending = state.pendingMessageScans.get(chatId);
6055
+ if (!pending?.readyForDispatch) return messages;
6056
+ const merged = mergeMessages(pending.messages, messages);
6057
+ const projected = new Map(state.pendingMessageScans);
6058
+ projected.set(chatId, { ...pending, chat, messages: merged });
6059
+ assertStateCapacity(projected);
6060
+ state.pendingMessageScans.set(chatId, { ...pending, chat, messages: merged });
6061
+ persist(state);
6062
+ return merged;
6063
+ }
6064
+ function recordResetRetryFailure(state, chatId, now = Date.now()) {
6065
+ const pending = state.pendingMessageScans.get(chatId);
6066
+ if (!pending?.readyForDispatch) return;
6067
+ const retryCount = (pending.retryCount ?? 0) + 1;
6068
+ const delay = Math.min(MAX_RESET_RETRY_DELAY_MS, 1e3 * 2 ** Math.min(retryCount - 1, 6));
6069
+ state.pendingMessageScans.set(chatId, {
6070
+ ...pending,
6071
+ retryCount,
6072
+ nextRetryAt: now + delay
6073
+ });
6074
+ persist(state);
6075
+ }
6076
+ function monitorChatsToProcess(chats, state, now = Date.now()) {
6077
+ const result = /* @__PURE__ */ new Map();
6078
+ for (const pending of state.pendingMessageScans.values()) {
6079
+ const chatId = pending.chat.username ?? pending.chat.id;
6080
+ if ((pending.nextRetryAt ?? 0) <= now) result.set(chatId, pending.chat);
6081
+ }
6082
+ for (const chat of chats) {
6083
+ const chatId = chat.username ?? chat.id;
6084
+ if (chat.unreadCount > 0 || state.pendingMessageScans.has(chatId)) result.set(chatId, chat);
6085
+ }
6086
+ return result;
6087
+ }
6088
+ function commitResetDispatchPrefix(state, chatId, cursorMessage) {
6089
+ if (cursorMessage === void 0) return;
6090
+ const cursor = cursorMessage.localId;
6091
+ const pending = state.pendingMessageScans.get(chatId);
6092
+ if (!pending?.readyForDispatch) {
6093
+ state.lastSeenId.set(chatId, cursor);
6094
+ return;
6095
+ }
6096
+ const exactIndex = pending.messages.findIndex(
6097
+ (message) => cursorMessageKey(message) === cursorMessageKey(cursorMessage)
6098
+ );
6099
+ if (exactIndex < 0) {
6100
+ throw new PendingRetryStateError(
6101
+ `Successful reset cursor identity is absent from pending chat ${chatId}`,
6102
+ "RETRY_STATE_CORRUPT"
6103
+ );
6104
+ }
6105
+ const remaining = pending.messages.slice(exactIndex + 1);
6106
+ const projected = new Map(state.pendingMessageScans);
6107
+ if (remaining.length === 0) {
6108
+ projected.delete(chatId);
6109
+ } else {
6110
+ projected.set(chatId, { ...pending, messages: remaining, retryCount: 0, nextRetryAt: 0 });
6111
+ }
6112
+ assertStateCapacity(projected);
6113
+ state.lastSeenId.set(chatId, cursor);
6114
+ state.pendingMessageScans.clear();
6115
+ for (const [key, value] of projected) state.pendingMessageScans.set(key, value);
6116
+ persist(state);
6117
+ }
6118
+
5626
6119
  // src/access-control.ts
5627
6120
  import {
5628
6121
  buildChannelKeyCandidates,
@@ -5663,7 +6156,7 @@ function normalizeWeChatId(raw) {
5663
6156
  if (!trimmed) {
5664
6157
  return "";
5665
6158
  }
5666
- return trimmed.replace(/^wechat:/i, "").trim();
6159
+ return trimmed.replace(/^(agent-wechat|wechat):/i, "").trim();
5667
6160
  }
5668
6161
  function normalizeWeChatAllowFrom(values) {
5669
6162
  const normalized = (values ?? []).map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry === "*" ? "*" : normalizeWeChatId(entry)).filter(Boolean);
@@ -5749,7 +6242,7 @@ function resolveWeChatPolicyContext(params) {
5749
6242
  const requireMention = firstDefined(groupEntry?.requireMention, wildcardEntry?.requireMention, true) !== false;
5750
6243
  const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
5751
6244
  const { groupPolicy: fallbackGroupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
5752
- providerConfigPresent: params.cfg.channels?.wechat !== void 0,
6245
+ providerConfigPresent: params.cfg.channels?.["agent-wechat"] !== void 0 || params.cfg.channels?.wechat !== void 0,
5753
6246
  groupPolicy: normalizeGroupPolicy2(params.account.groupPolicy),
5754
6247
  defaultGroupPolicy: normalizeGroupPolicy2(defaultGroupPolicy)
5755
6248
  });
@@ -5864,6 +6357,310 @@ function nextReconnectState(input) {
5864
6357
  return { reconnect: true, dispatched: input.dispatched };
5865
6358
  }
5866
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
+
5867
6664
  // src/monitor.ts
5868
6665
  var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34, 43]);
5869
6666
  var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
@@ -5880,22 +6677,6 @@ function sleep(ms, signal) {
5880
6677
  });
5881
6678
  });
5882
6679
  }
5883
- async function pollMedia(client, chatId, localId, log, maxAttempts = 15, intervalMs = 1e3) {
5884
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
5885
- const result = await client.getMedia(chatId, localId);
5886
- if (result.type === "unsupported") {
5887
- return null;
5888
- }
5889
- if (result.data) {
5890
- return result;
5891
- }
5892
- if (attempt < maxAttempts) {
5893
- log?.info?.(`[media] Attempt ${attempt}/${maxAttempts} for ${chatId}:${localId} returned no data, retrying...`);
5894
- await new Promise((r) => setTimeout(r, intervalMs));
5895
- }
5896
- }
5897
- return null;
5898
- }
5899
6680
  function enqueueWeChatSystemEvent(text, contextKey) {
5900
6681
  try {
5901
6682
  const core = getWeChatRuntime();
@@ -5910,6 +6691,23 @@ async function startWeChatMonitor(opts) {
5910
6691
  const { account, abortSignal, setStatus, log } = opts;
5911
6692
  const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
5912
6693
  const lastSeenId = /* @__PURE__ */ new Map();
6694
+ const equalCursorUnreadHandled = /* @__PURE__ */ new Map();
6695
+ const startupBaselines = /* @__PURE__ */ new Map();
6696
+ const chatScanState = { initialScanComplete: false };
6697
+ const pendingMessageScans = loadPendingResetRetries(account.accountId);
6698
+ for (const [chatId, pending] of pendingMessageScans) {
6699
+ const cursor = pending.messages.reduce(
6700
+ (max, message) => Math.max(max, message.localId),
6701
+ 0
6702
+ );
6703
+ if (cursor > 0) lastSeenId.set(chatId, cursor);
6704
+ }
6705
+ const retryState = {
6706
+ pendingMessageScans,
6707
+ lastSeenId,
6708
+ persist: () => persistPendingResetRetries(account.accountId, pendingMessageScans)
6709
+ };
6710
+ trimPendingResetRetries(retryState);
5913
6711
  const groupHistory = /* @__PURE__ */ new Map();
5914
6712
  const GROUP_HISTORY_LIMIT = 50;
5915
6713
  let lastAuthCheck = 0;
@@ -5978,8 +6776,15 @@ async function startWeChatMonitor(opts) {
5978
6776
  }
5979
6777
  }
5980
6778
  let chats;
6779
+ let isInitialSnapshot = false;
5981
6780
  try {
5982
- chats = await client.listChats(50);
6781
+ if (!chatScanState.initialScanComplete) {
6782
+ chats = await listInitialMonitorChatSnapshot(client, chatScanState);
6783
+ isInitialSnapshot = true;
6784
+ } else {
6785
+ const page = await listNextMonitorChatPage(client, chatScanState);
6786
+ chats = page.chats;
6787
+ }
5983
6788
  } catch (err) {
5984
6789
  log?.error?.(
5985
6790
  `[wechat:${account.accountId}] Failed to list chats: ${err}`
@@ -5987,9 +6792,28 @@ async function startWeChatMonitor(opts) {
5987
6792
  await sleep(account.pollIntervalMs, abortSignal);
5988
6793
  continue;
5989
6794
  }
6795
+ if (isInitialSnapshot) {
6796
+ for (const chat of chats) {
6797
+ const chatId = chat.username ?? chat.id;
6798
+ if (!startupBaselines.has(chatId)) {
6799
+ const baseline = seedStartupBaselineFromChat(chat);
6800
+ if (baseline) startupBaselines.set(chatId, baseline);
6801
+ }
6802
+ }
6803
+ }
5990
6804
  const unreadChats = chats.filter(
5991
6805
  (c) => c.unreadCount > 0 && !isOfficialAccount(c.username ?? c.id)
5992
6806
  );
6807
+ for (const chat of unreadChats) {
6808
+ const chatId = chat.username ?? chat.id;
6809
+ if (chatScanState.initialScanComplete && !lastSeenId.has(chatId) && !startupBaselines.has(chatId)) {
6810
+ startupBaselines.set(chatId, { localId: 0 });
6811
+ }
6812
+ }
6813
+ const chatsToProcess = monitorChatsToProcess(chats, retryState);
6814
+ for (const chatId of chatsToProcess.keys()) {
6815
+ if (isOfficialAccount(chatId)) chatsToProcess.delete(chatId);
6816
+ }
5993
6817
  if (unreadChats.length > 0) {
5994
6818
  log?.info?.(
5995
6819
  `[wechat:${account.accountId}] ${unreadChats.length} chat(s) with unreads`
@@ -5997,13 +6821,17 @@ async function startWeChatMonitor(opts) {
5997
6821
  }
5998
6822
  let deferredThisTick = 0;
5999
6823
  let dispatchedThisTick = 0;
6000
- if (unreadChats.length > 0) {
6001
- for (const chat of unreadChats) {
6824
+ if (chatsToProcess.size > 0) {
6825
+ for (const chat of chatsToProcess.values()) {
6002
6826
  if (abortSignal.aborted) break;
6003
6827
  const result = await processUnreadChat(
6004
6828
  client,
6005
6829
  chat,
6006
6830
  lastSeenId,
6831
+ equalCursorUnreadHandled,
6832
+ startupBaselines,
6833
+ pendingMessageScans,
6834
+ retryState.persist,
6007
6835
  account,
6008
6836
  cfg,
6009
6837
  log,
@@ -6040,6 +6868,10 @@ async function startWeChatMonitor(opts) {
6040
6868
  client,
6041
6869
  chat,
6042
6870
  lastSeenId,
6871
+ equalCursorUnreadHandled,
6872
+ startupBaselines,
6873
+ pendingMessageScans,
6874
+ retryState.persist,
6043
6875
  account,
6044
6876
  cfg,
6045
6877
  log,
@@ -6115,44 +6947,28 @@ async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, lo
6115
6947
  const result = await pollMedia(client, chatId, msg.localId, log);
6116
6948
  if (result && result.data && result.type !== "unsupported") {
6117
6949
  hasMedia = true;
6118
- log?.info?.(`[wechat:${liveAccount.accountId}] Media result: type=${result.type}, format=${result.format}, hasData=${!!result.data}, filename=${result.filename}`);
6119
- const mimeMap = {
6120
- jpeg: "image/jpeg",
6121
- jpg: "image/jpeg",
6122
- png: "image/png",
6123
- gif: "image/gif",
6124
- mp3: "audio/mpeg",
6125
- pdf: "application/pdf",
6126
- doc: "application/msword",
6127
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
6128
- xls: "application/vnd.ms-excel",
6129
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
6130
- ppt: "application/vnd.ms-powerpoint",
6131
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
6132
- zip: "application/zip",
6133
- txt: "text/plain"
6134
- };
6135
- mediaMime = mimeMap[result.format] ?? `application/${result.format || "octet-stream"}`;
6136
- const buf = Buffer.from(result.data, "base64");
6137
- const saved = await core.channel.media.saveMediaBuffer(
6138
- buf,
6139
- mediaMime,
6140
- "inbound",
6141
- void 0,
6142
- 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)
6143
6954
  );
6144
- mediaPath = saved?.path;
6145
- 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
+ }
6146
6962
  } else if (MEDIA_TYPES.has(baseType)) {
6147
6963
  hasMedia = true;
6148
- log?.info?.(`[wechat:${liveAccount.accountId}] Media not available after retries for msg ${msg.localId}`);
6964
+ log?.info?.("[wechat:media] inbound media unavailable after retries");
6149
6965
  }
6150
6966
  } catch (err) {
6151
- log?.error?.(`[wechat:${liveAccount.accountId}] Media download failed: ${err}`);
6967
+ log?.error?.("[wechat:media] inbound media failed code=MEDIA_DOWNLOAD_FAILED");
6152
6968
  }
6153
6969
  }
6154
6970
  const timestamp = new Date(msg.timestamp).getTime();
6155
- let rawBody = msg.content || "";
6971
+ let rawBody = mediaPath ? msg.content || "" : safeBodyAfterKnownMediaFailure(baseType, msg.content || "");
6156
6972
  if (mediaPath && mediaMime) {
6157
6973
  if (!rawBody) {
6158
6974
  if (mediaMime.startsWith("audio/")) {
@@ -6220,7 +7036,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6220
7036
  const mediaPath = mediaMsg?.mediaPath;
6221
7037
  const mediaMime = mediaMsg?.mediaMime;
6222
7038
  log?.info?.(
6223
- `[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" : ""}`
6224
7040
  );
6225
7041
  const hasControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(commandBody, cfg);
6226
7042
  const commandAuthorized = await resolveWeChatCommandAuthorization({
@@ -6260,7 +7076,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6260
7076
  try {
6261
7077
  const route = core.channel.routing.resolveAgentRoute({
6262
7078
  cfg,
6263
- channel: "wechat",
7079
+ channel: OPENCLAW_CHANNEL_ID,
6264
7080
  accountId: liveAccount.accountId,
6265
7081
  peer: {
6266
7082
  kind: isGroup ? "group" : "direct",
@@ -6332,21 +7148,21 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6332
7148
  RawBody: rawBody,
6333
7149
  CommandBody: commandBody,
6334
7150
  InboundHistory: inboundHistory,
6335
- From: isGroup ? `wechat:group:${chatId}` : `wechat:${senderId}`,
6336
- To: `wechat:${chatId}`,
7151
+ From: isGroup ? `${OPENCLAW_CHANNEL_ID}:group:${chatId}` : `${OPENCLAW_CHANNEL_ID}:${senderId}`,
7152
+ To: `${OPENCLAW_CHANNEL_ID}:${chatId}`,
6337
7153
  SessionKey: route.sessionKey,
6338
7154
  AccountId: route.accountId,
6339
7155
  ChatType: isGroup ? "group" : "direct",
6340
7156
  ConversationLabel: fromLabel,
6341
7157
  SenderName: senderName || void 0,
6342
7158
  SenderId: senderId,
6343
- Provider: "wechat",
6344
- Surface: "wechat",
6345
- MessageSid: `wechat:${chatId}:${msg.localId}`,
7159
+ Provider: OPENCLAW_CHANNEL_ID,
7160
+ Surface: OPENCLAW_CHANNEL_ID,
7161
+ MessageSid: `${OPENCLAW_CHANNEL_ID}:${chatId}:${msg.localId}`,
6346
7162
  WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : void 0,
6347
7163
  CommandAuthorized: commandAuthorized,
6348
- OriginatingChannel: "wechat",
6349
- OriginatingTo: `wechat:${chatId}`,
7164
+ OriginatingChannel: OPENCLAW_CHANNEL_ID,
7165
+ OriginatingTo: `${OPENCLAW_CHANNEL_ID}:${chatId}`,
6350
7166
  ...mediaPath ? { MediaPath: mediaPath, MediaUrl: mediaPath, MediaType: mediaMime } : {},
6351
7167
  ...msg.reply ? {
6352
7168
  ReplyToBody: msg.reply.content.length > 50 ? msg.reply.content.slice(0, 50) + "..." : msg.reply.content,
@@ -6366,7 +7182,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6366
7182
  const { onModelSelected, ...prefixOptions } = createChannelReplyPipeline({
6367
7183
  cfg,
6368
7184
  agentId: route.agentId,
6369
- channel: "wechat",
7185
+ channel: OPENCLAW_CHANNEL_ID,
6370
7186
  accountId: liveAccount.accountId
6371
7187
  });
6372
7188
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
@@ -6378,7 +7194,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6378
7194
  const mediaList = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
6379
7195
  const tableMode = core.channel.text.resolveMarkdownTableMode({
6380
7196
  cfg,
6381
- channel: "wechat",
7197
+ channel: OPENCLAW_CHANNEL_ID,
6382
7198
  accountId: liveAccount.accountId
6383
7199
  });
6384
7200
  const text = core.channel.text.convertMarkdownTables(
@@ -6447,7 +7263,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6447
7263
  }
6448
7264
  });
6449
7265
  core.channel.activity?.record?.({
6450
- channel: "wechat",
7266
+ channel: OPENCLAW_CHANNEL_ID,
6451
7267
  accountId: liveAccount.accountId,
6452
7268
  direction: "inbound",
6453
7269
  at: timestamp
@@ -6473,11 +7289,11 @@ function bufferGroupHistory(groupHistory, chatId, pm, limit) {
6473
7289
  if (first) groupHistory.delete(first);
6474
7290
  }
6475
7291
  }
6476
- async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, catchup) {
7292
+ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHandled, startupBaselines, pendingMessageScans, persistRetryState, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, catchup) {
6477
7293
  const core = getWeChatRuntime();
6478
7294
  const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
6479
7295
  const chatId = chat.username ?? chat.id;
6480
- 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(() => []);
6481
7297
  const policy = resolveWeChatPolicyContext({
6482
7298
  account: liveAccount,
6483
7299
  cfg,
@@ -6486,9 +7302,12 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6486
7302
  });
6487
7303
  const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
6488
7304
  cfg,
6489
- surface: "wechat"
7305
+ surface: OPENCLAW_CHANNEL_ID
6490
7306
  });
6491
- if (!skipOpen) {
7307
+ let opened = false;
7308
+ const openChatIfNeeded = async () => {
7309
+ if (skipOpen || opened) return;
7310
+ opened = true;
6492
7311
  log?.info?.(`[wechat:${liveAccount.accountId}] Opening chat ${chatId}...`);
6493
7312
  try {
6494
7313
  await client.openChat(chatId, true);
@@ -6498,14 +7317,54 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6498
7317
  `[wechat:${liveAccount.accountId}] Failed to open chat ${chatId}: ${err}`
6499
7318
  );
6500
7319
  }
6501
- }
7320
+ };
6502
7321
  const firstPoll = !lastSeenId.has(chatId);
6503
7322
  const prevLastSeen = lastSeenId.get(chatId) ?? 0;
6504
- const recoveryWindow = Math.max(liveAccount.catchUpMaxMessages, 20);
6505
- const fetchLimit = Math.min(Math.max(chat.unreadCount, 20), recoveryWindow);
7323
+ const startupBaseline = startupBaselines.get(chatId);
6506
7324
  let messages;
7325
+ let generationResetConfirmed = false;
6507
7326
  try {
6508
- messages = await client.listMessages(chatId, fetchLimit);
7327
+ const pending = pendingMessageScans.get(chatId);
7328
+ if (pending?.readyForDispatch) {
7329
+ const pendingTail = pending.messages.at(-1)?.localId ?? 0;
7330
+ const scan = await listMessagesForMonitorCursor(client, chatId, {
7331
+ chat,
7332
+ firstPoll: false,
7333
+ prevLastSeen: pendingTail,
7334
+ handledCursor: equalCursorUnreadHandled.get(chatId)
7335
+ });
7336
+ messages = mergePendingResetMessages(
7337
+ { pendingMessageScans, lastSeenId, persist: persistRetryState },
7338
+ chatId,
7339
+ chat,
7340
+ scan.messages
7341
+ );
7342
+ generationResetConfirmed = pending.generationReset === true;
7343
+ } else {
7344
+ const scan = await listMessagesForMonitorCursor(client, chatId, {
7345
+ chat,
7346
+ firstPoll,
7347
+ prevLastSeen,
7348
+ startupBaseline,
7349
+ handledCursor: equalCursorUnreadHandled.get(chatId),
7350
+ continuation: pending
7351
+ });
7352
+ if (!scan.complete) {
7353
+ pendingMessageScans.set(chatId, {
7354
+ chat,
7355
+ cursor: scan.nextCursor,
7356
+ messages: scan.messages,
7357
+ generationReset: scan.generationReset
7358
+ });
7359
+ log?.info?.(
7360
+ `[wechat:${liveAccount.accountId}] ${chatId}: deferred after ${scan.pagesScanned} message page(s); continuing paged scan next tick`
7361
+ );
7362
+ return "deferred";
7363
+ }
7364
+ messages = scan.messages;
7365
+ generationResetConfirmed = scan.generationReset === true;
7366
+ pendingMessageScans.delete(chatId);
7367
+ }
6509
7368
  } catch (err) {
6510
7369
  log?.error?.(
6511
7370
  `[wechat:${liveAccount.accountId}] Failed to list messages for ${chatId}: ${err}`
@@ -6516,33 +7375,72 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6516
7375
  `[wechat:${liveAccount.accountId}] ${chatId}: fetched ${messages.length} msgs, firstPoll=${firstPoll}, prevLastSeen=${prevLastSeen}, unreadCount=${chat.unreadCount}`
6517
7376
  );
6518
7377
  if (messages.length === 0) return "skipped";
6519
- let newMessages;
6520
- if (firstPoll) {
6521
- messages.sort((a, b) => a.localId - b.localId);
6522
- const unread = chat.unreadCount ?? 0;
6523
- if (unread > 0 && unread < messages.length) {
6524
- newMessages = messages.slice(-unread);
6525
- const seenMax = messages[messages.length - unread - 1].localId;
6526
- lastSeenId.set(chatId, seenMax);
6527
- } else if (unread >= messages.length) {
6528
- newMessages = messages;
6529
- } else {
6530
- const maxId2 = messages[messages.length - 1].localId;
6531
- lastSeenId.set(chatId, maxId2);
6532
- return "skipped";
7378
+ const enrichedBaseline = enrichStartupBaselineFromMessages(startupBaseline, messages);
7379
+ if (enrichedBaseline) {
7380
+ startupBaselines.set(chatId, enrichedBaseline);
7381
+ }
7382
+ const selected = selectCursorMessages(
7383
+ chatId,
7384
+ chat,
7385
+ messages,
7386
+ lastSeenId,
7387
+ equalCursorUnreadHandled,
7388
+ enrichedBaseline,
7389
+ generationResetConfirmed
7390
+ );
7391
+ let newMessages = selected.messages;
7392
+ if (selected.generationReset && !pendingMessageScans.get(chatId)?.readyForDispatch) {
7393
+ queueResetGenerationRetry(
7394
+ { pendingMessageScans, lastSeenId, persist: persistRetryState },
7395
+ chatId,
7396
+ chat,
7397
+ newMessages
7398
+ );
7399
+ }
7400
+ const advanceLastSeen = (handledMessages) => {
7401
+ const cursor = markCursorMessagesHandled(chatId, handledMessages, equalCursorUnreadHandled);
7402
+ if (cursor !== void 0) {
7403
+ lastSeenId.set(chatId, cursor);
7404
+ if (selected.generationReset) {
7405
+ startupBaselines.delete(chatId);
7406
+ }
7407
+ const baseline = startupBaselines.get(chatId);
7408
+ if (baseline && cursor >= baseline.localId) {
7409
+ startupBaselines.delete(chatId);
7410
+ }
7411
+ const exactCursorMessage = handledMessages.reduce(
7412
+ (latest, message) => latest === void 0 || message.localId > latest.localId ? message : message.localId === latest.localId ? message : latest,
7413
+ void 0
7414
+ );
7415
+ commitResetDispatchPrefix(
7416
+ { pendingMessageScans, lastSeenId, persist: persistRetryState },
7417
+ chatId,
7418
+ exactCursorMessage
7419
+ );
6533
7420
  }
6534
- } else {
6535
- newMessages = messages.filter((m) => m.localId > prevLastSeen);
6536
- if (newMessages.length === 0) {
6537
- return "skipped";
7421
+ };
7422
+ const seedLastSeen = selected.seedLastSeen;
7423
+ if (seedLastSeen !== void 0) {
7424
+ advanceLastSeen(messages.filter((message) => message.localId <= seedLastSeen));
7425
+ }
7426
+ if (newMessages.length === 0) {
7427
+ if (selected.generationReset) {
7428
+ clearPendingResetRetry(
7429
+ { pendingMessageScans, lastSeenId, persist: persistRetryState },
7430
+ chatId
7431
+ );
6538
7432
  }
6539
- newMessages.sort((a, b) => a.localId - b.localId);
7433
+ if (chat.unreadCount > 0) {
7434
+ await openChatIfNeeded();
7435
+ }
7436
+ return "skipped";
6540
7437
  }
7438
+ await openChatIfNeeded();
6541
7439
  const catchUpLimits = {
6542
7440
  maxMessages: liveAccount.catchUpMaxMessages,
6543
7441
  maxAgeMs: liveAccount.catchUpMaxAgeMs
6544
7442
  };
6545
- const isRecovery = firstPoll || skipOpen === true && isCatchUpBatch(newMessages, catchUpLimits);
7443
+ const isRecovery = firstPoll && startupBaseline === void 0 || skipOpen === true && isCatchUpBatch(newMessages, catchUpLimits);
6546
7444
  if (isRecovery) {
6547
7445
  const selection = selectCatchUpMessages(
6548
7446
  newMessages,
@@ -6551,7 +7449,7 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6551
7449
  );
6552
7450
  const observedCursor = recoveryCursor(selection);
6553
7451
  if (liveAccount.catchUpMode === "read-only") {
6554
- lastSeenId.set(chatId, observedCursor);
7452
+ advanceLastSeen(messages.filter((message) => message.localId <= observedCursor));
6555
7453
  log?.info?.(
6556
7454
  `[wechat:${liveAccount.accountId}] Recovery read-only: advanced ${chatId} to ${observedCursor} without dispatching ${newMessages.length} msg(s)`
6557
7455
  );
@@ -6559,7 +7457,7 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6559
7457
  }
6560
7458
  newMessages = selection.messages;
6561
7459
  if (newMessages.length === 0) {
6562
- lastSeenId.set(chatId, observedCursor);
7460
+ advanceLastSeen(messages.filter((message) => message.localId <= observedCursor));
6563
7461
  log?.info?.(
6564
7462
  `[wechat:${liveAccount.accountId}] Recovery skipped ${selection.skipped} stale msg(s) in ${chatId}`
6565
7463
  );
@@ -6597,7 +7495,7 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6597
7495
  }
6598
7496
  log?.info?.(`[wechat:${liveAccount.accountId}] Buffered ${processed.length} msg(s) for group history in ${chatId}`);
6599
7497
  const maxId2 = Math.max(...newMessages.map((m) => m.localId));
6600
- lastSeenId.set(chatId, maxId2);
7498
+ advanceLastSeen(newMessages.filter((message) => message.localId <= maxId2));
6601
7499
  return "skipped";
6602
7500
  }
6603
7501
  if (hasMention) {
@@ -6654,7 +7552,7 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6654
7552
  }
6655
7553
  if (decision.action === "defer") {
6656
7554
  log?.info?.(
6657
- `[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`
6658
7556
  );
6659
7557
  return "deferred";
6660
7558
  }
@@ -6662,8 +7560,8 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6662
7560
  log?.info?.(
6663
7561
  `[wechat:${liveAccount.accountId}] Skipping stale group chatter in ${chatId}`
6664
7562
  );
6665
- const maxId3 = Math.max(...newMessages.map((m) => m.localId));
6666
- lastSeenId.set(chatId, maxId3);
7563
+ const maxId2 = Math.max(...newMessages.map((m) => m.localId));
7564
+ advanceLastSeen(newMessages.filter((message) => message.localId <= maxId2));
6667
7565
  return "skipped";
6668
7566
  }
6669
7567
  const fold = shouldFoldSegments(catchup?.isReconnect ?? false) && !hasControlCommandInWindow;
@@ -6672,6 +7570,7 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6672
7570
  `[wechat:${liveAccount.accountId}] ${chatId}: ${processed.length} dispatchable msg(s) in ${segments.length} segment(s) (${decision.reason})`
6673
7571
  );
6674
7572
  let allDispatched = true;
7573
+ const successfulSegmentLastLocalIds = [];
6675
7574
  for (let i = 0; i < segments.length; i++) {
6676
7575
  const remaining = segments.length - i - 1;
6677
7576
  const dispatched = await dispatchSegment(
@@ -6689,17 +7588,27 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6689
7588
  );
6690
7589
  if (!dispatched) {
6691
7590
  allDispatched = false;
7591
+ break;
6692
7592
  }
7593
+ successfulSegmentLastLocalIds.push(
7594
+ Math.max(...segments[i].map((pm) => pm.msg.localId))
7595
+ );
6693
7596
  }
6694
7597
  if (clearBufferedHistory && allDispatched && groupHistory) {
6695
7598
  groupHistory.set(chatId, []);
6696
7599
  }
6697
- const maxId2 = Math.max(...newMessages.map((m) => m.localId));
6698
- lastSeenId.set(chatId, maxId2);
7600
+ const handledMessages = allDispatched ? newMessages : selectMessagesHandledAfterDispatch(newMessages, successfulSegmentLastLocalIds);
7601
+ advanceLastSeen(handledMessages);
7602
+ if (!allDispatched) {
7603
+ recordResetRetryFailure(
7604
+ { pendingMessageScans, lastSeenId, persist: persistRetryState },
7605
+ chatId
7606
+ );
7607
+ }
6699
7608
  return allDispatched ? "dispatched" : "skipped";
6700
7609
  }
6701
7610
  const maxId = Math.max(...newMessages.map((m) => m.localId));
6702
- lastSeenId.set(chatId, maxId);
7611
+ advanceLastSeen(newMessages.filter((message) => message.localId <= maxId));
6703
7612
  return "skipped";
6704
7613
  }
6705
7614
 
@@ -6709,7 +7618,7 @@ function collectWeChatStatusIssues(accounts) {
6709
7618
  for (const snapshot of accounts) {
6710
7619
  if (snapshot.connected === false) {
6711
7620
  issues.push({
6712
- channel: "wechat",
7621
+ channel: "agent-wechat",
6713
7622
  accountId: snapshot.accountId,
6714
7623
  kind: "runtime",
6715
7624
  message: snapshot.lastError ? `Cannot reach agent-wechat server: ${snapshot.lastError}` : "Cannot reach agent-wechat server.",
@@ -6717,7 +7626,7 @@ function collectWeChatStatusIssues(accounts) {
6717
7626
  });
6718
7627
  } else if (snapshot.authStatus === "app_not_running") {
6719
7628
  issues.push({
6720
- channel: "wechat",
7629
+ channel: "agent-wechat",
6721
7630
  accountId: snapshot.accountId,
6722
7631
  kind: "runtime",
6723
7632
  message: "WeChat application is not running. It should restart automatically.",
@@ -6725,11 +7634,11 @@ function collectWeChatStatusIssues(accounts) {
6725
7634
  });
6726
7635
  } else if (snapshot.linked === false) {
6727
7636
  issues.push({
6728
- channel: "wechat",
7637
+ channel: "agent-wechat",
6729
7638
  accountId: snapshot.accountId,
6730
7639
  kind: "auth",
6731
7640
  message: "WeChat session not authenticated.",
6732
- fix: "Run: openclaw channels login --channel wechat"
7641
+ fix: "Run: openclaw channels login --channel agent-wechat"
6733
7642
  });
6734
7643
  }
6735
7644
  }
@@ -7148,7 +8057,7 @@ async function sendWeChatText(cfg, to, text) {
7148
8057
  }
7149
8058
  throw new Error(result.error ?? "Send failed");
7150
8059
  }
7151
- return `wechat:${to}:${Date.now()}`;
8060
+ return `${OPENCLAW_CHANNEL_ID}:${to}:${Date.now()}`;
7152
8061
  }
7153
8062
  async function sendWeChatMedia(cfg, to, text, mediaUrl) {
7154
8063
  const account = resolveWeChatAccount(cfg);
@@ -7161,7 +8070,7 @@ async function sendWeChatMedia(cfg, to, text, mediaUrl) {
7161
8070
  source: "openclaw"
7162
8071
  });
7163
8072
  if (!result.success) throw new Error(result.error ?? "Send failed");
7164
- return `wechat:${to}:${Date.now()}`;
8073
+ return `${OPENCLAW_CHANNEL_ID}:${to}:${Date.now()}`;
7165
8074
  }
7166
8075
  const fsmod = await import("fs/promises");
7167
8076
  const pathmod = await import("path");
@@ -7198,10 +8107,10 @@ async function sendWeChatMedia(cfg, to, text, mediaUrl) {
7198
8107
  interPartDelayMs: account.mediaPartDelayMs,
7199
8108
  source: "openclaw"
7200
8109
  });
7201
- return `wechat:${to}:${requestId}`;
8110
+ return `${OPENCLAW_CHANNEL_ID}:${to}:${requestId}`;
7202
8111
  }
7203
8112
  var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
7204
- id: "wechat",
8113
+ id: OPENCLAW_CHANNEL_ID,
7205
8114
  capabilities: {
7206
8115
  text: true,
7207
8116
  media: true,
@@ -7209,26 +8118,26 @@ var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
7209
8118
  },
7210
8119
  outbound: {
7211
8120
  sendText: async ({ cfg, to, text }) => ({
7212
- channel: "wechat",
8121
+ channel: OPENCLAW_CHANNEL_ID,
7213
8122
  messageId: await sendWeChatText(cfg, to, text)
7214
8123
  }),
7215
8124
  sendMedia: async ({ cfg, to, text, mediaUrl }) => ({
7216
- channel: "wechat",
8125
+ channel: OPENCLAW_CHANNEL_ID,
7217
8126
  messageId: await sendWeChatMedia(cfg, to, text, mediaUrl)
7218
8127
  })
7219
8128
  }
7220
8129
  });
7221
8130
  var meta = {
7222
- id: "wechat",
8131
+ id: OPENCLAW_CHANNEL_ID,
7223
8132
  label: "WeChat",
7224
- selectionLabel: "WeChat (\u5FAE\u4FE1)",
8133
+ selectionLabel: "WeChat (agent-wechat)",
7225
8134
  blurb: "WeChat messaging via agent-wechat container.",
7226
8135
  docsPath: "wechat",
7227
- aliases: ["weixin"],
8136
+ aliases: [],
7228
8137
  order: 80
7229
8138
  };
7230
8139
  var wechatPlugin = {
7231
- id: "wechat",
8140
+ id: OPENCLAW_CHANNEL_ID,
7232
8141
  meta,
7233
8142
  gatewayMethods: ["web.login.start", "web.login.wait"],
7234
8143
  capabilities: {
@@ -7238,7 +8147,7 @@ var wechatPlugin = {
7238
8147
  media: true,
7239
8148
  reply: true
7240
8149
  },
7241
- reload: { configPrefixes: ["channels.wechat"] },
8150
+ reload: { configPrefixes: [`channels.${OPENCLAW_CHANNEL_ID}`, "channels.wechat"] },
7242
8151
  configSchema: {
7243
8152
  schema: {
7244
8153
  type: "object",
@@ -7314,23 +8223,23 @@ var wechatPlugin = {
7314
8223
  },
7315
8224
  isEnabled: (account) => account.enabled && !!account.serverUrl,
7316
8225
  isConfigured: (account) => !!account.serverUrl,
7317
- unconfiguredReason: () => "No serverUrl configured. Run: openclaw channels setup wechat"
8226
+ unconfiguredReason: () => "No serverUrl configured. Run: openclaw channels setup agent-wechat"
7318
8227
  },
7319
8228
  // ---- Security adapter ----
7320
8229
  security: {
7321
8230
  resolveDmPolicy: ({ account }) => ({
7322
8231
  policy: account.dmPolicy ?? "disabled",
7323
8232
  allowFrom: account.allowFrom ?? [],
7324
- allowFromPath: "channels.wechat.allowFrom",
7325
- policyPath: "channels.wechat.dmPolicy",
7326
- approveHint: "Add the wxid to channels.wechat.allowFrom",
7327
- 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()
7328
8237
  })
7329
8238
  },
7330
8239
  // ---- Groups adapter ----
7331
8240
  groups: {
7332
8241
  resolveRequireMention: ({ cfg, groupId }) => {
7333
- const wechat = cfg?.channels?.wechat;
8242
+ const wechat = cfg?.channels?.[OPENCLAW_CHANNEL_ID] ?? cfg?.channels?.wechat;
7334
8243
  if (!wechat) return true;
7335
8244
  if (!groupId) {
7336
8245
  return wechat.groups?.["*"]?.requireMention ?? true;
@@ -7355,10 +8264,10 @@ var wechatPlugin = {
7355
8264
  },
7356
8265
  // ---- Messaging adapter ----
7357
8266
  messaging: {
7358
- normalizeTarget: (raw) => raw.replace(/^wechat:/i, "").trim() || void 0,
8267
+ normalizeTarget: (raw) => raw.replace(/^(agent-wechat|wechat):/i, "").trim() || void 0,
7359
8268
  targetResolver: {
7360
8269
  looksLikeId: (raw) => {
7361
- const stripped = raw.replace(/^wechat:/i, "").trim();
8270
+ const stripped = raw.replace(/^(agent-wechat|wechat):/i, "").trim();
7362
8271
  return stripped.includes("@chatroom") || stripped.startsWith("wxid_");
7363
8272
  },
7364
8273
  hint: "WeChat ID (wxid_xxx or xxx@chatroom)"
@@ -7368,11 +8277,11 @@ var wechatPlugin = {
7368
8277
  outbound: {
7369
8278
  deliveryMode: "direct",
7370
8279
  sendText: async ({ cfg, to, text }) => ({
7371
- channel: "wechat",
8280
+ channel: OPENCLAW_CHANNEL_ID,
7372
8281
  messageId: await sendWeChatText(cfg, to, text)
7373
8282
  }),
7374
8283
  sendMedia: async ({ cfg, to, text, mediaUrl }) => ({
7375
- channel: "wechat",
8284
+ channel: OPENCLAW_CHANNEL_ID,
7376
8285
  messageId: await sendWeChatMedia(cfg, to, text, mediaUrl)
7377
8286
  })
7378
8287
  },
@@ -7399,7 +8308,7 @@ var wechatPlugin = {
7399
8308
  accountId ?? void 0
7400
8309
  );
7401
8310
  if (!account?.serverUrl) {
7402
- return { message: "No serverUrl configured. Run: openclaw channels setup wechat" };
8311
+ return { message: "No serverUrl configured. Run: openclaw channels setup agent-wechat" };
7403
8312
  }
7404
8313
  const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
7405
8314
  try {
@@ -7440,7 +8349,7 @@ var wechatPlugin = {
7440
8349
  );
7441
8350
  if (!account?.serverUrl) {
7442
8351
  throw new Error(
7443
- "No serverUrl configured. Run: openclaw channels setup wechat"
8352
+ "No serverUrl configured. Run: openclaw channels setup agent-wechat"
7444
8353
  );
7445
8354
  }
7446
8355
  const client = new WeChatClient({ baseUrl: account.serverUrl, token: account.token });
@@ -7600,8 +8509,8 @@ Error: ${event.message}`);
7600
8509
  ...cfg,
7601
8510
  channels: {
7602
8511
  ...cfg.channels,
7603
- wechat: {
7604
- ...cfg.channels?.wechat,
8512
+ [OPENCLAW_CHANNEL_ID]: {
8513
+ ...cfg.channels?.[OPENCLAW_CHANNEL_ID] ?? cfg.channels?.wechat,
7605
8514
  enabled: true,
7606
8515
  serverUrl,
7607
8516
  ...token ? { token } : {}
@@ -7614,7 +8523,7 @@ Error: ${event.message}`);
7614
8523
 
7615
8524
  // index.ts
7616
8525
  var plugin = {
7617
- id: "wechat",
8526
+ id: OPENCLAW_CHANNEL_ID,
7618
8527
  name: "WeChat",
7619
8528
  description: "WeChat channel via agent-wechat container",
7620
8529
  configSchema: emptyPluginConfigSchema(),