@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/README.md +12 -12
- package/dist/index.js +1040 -131
- package/openclaw.plugin.json +3 -3
- package/package.json +8 -7
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
|
|
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
|
-
|
|
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 (
|
|
6001
|
-
for (const chat of
|
|
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
|
|
6119
|
-
const
|
|
6120
|
-
|
|
6121
|
-
|
|
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
|
-
|
|
6145
|
-
|
|
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?.(
|
|
6964
|
+
log?.info?.("[wechat:media] inbound media unavailable after retries");
|
|
6149
6965
|
}
|
|
6150
6966
|
} catch (err) {
|
|
6151
|
-
log?.error?.(
|
|
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 ?
|
|
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:
|
|
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 ?
|
|
6336
|
-
To:
|
|
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:
|
|
6344
|
-
Surface:
|
|
6345
|
-
MessageSid:
|
|
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:
|
|
6349
|
-
OriginatingTo:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
7305
|
+
surface: OPENCLAW_CHANNEL_ID
|
|
6490
7306
|
});
|
|
6491
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6520
|
-
if (
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
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
|
-
}
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
6666
|
-
|
|
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
|
|
6698
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
8110
|
+
return `${OPENCLAW_CHANNEL_ID}:${to}:${requestId}`;
|
|
7202
8111
|
}
|
|
7203
8112
|
var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
7204
|
-
id:
|
|
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:
|
|
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:
|
|
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:
|
|
8131
|
+
id: OPENCLAW_CHANNEL_ID,
|
|
7223
8132
|
label: "WeChat",
|
|
7224
|
-
selectionLabel: "WeChat (
|
|
8133
|
+
selectionLabel: "WeChat (agent-wechat)",
|
|
7225
8134
|
blurb: "WeChat messaging via agent-wechat container.",
|
|
7226
8135
|
docsPath: "wechat",
|
|
7227
|
-
aliases: [
|
|
8136
|
+
aliases: [],
|
|
7228
8137
|
order: 80
|
|
7229
8138
|
};
|
|
7230
8139
|
var wechatPlugin = {
|
|
7231
|
-
id:
|
|
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:
|
|
7325
|
-
policyPath:
|
|
7326
|
-
approveHint:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
8526
|
+
id: OPENCLAW_CHANNEL_ID,
|
|
7618
8527
|
name: "WeChat",
|
|
7619
8528
|
description: "WeChat channel via agent-wechat container",
|
|
7620
8529
|
configSchema: emptyPluginConfigSchema(),
|