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