@kyan-du/agent-wechat-openclaw 0.14.0 → 0.14.1
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 +158 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5482,11 +5482,12 @@ var WeChatClient = class {
|
|
|
5482
5482
|
await throwHttpError(res);
|
|
5483
5483
|
return res.json();
|
|
5484
5484
|
}
|
|
5485
|
-
async post(path, body) {
|
|
5485
|
+
async post(path, body, signal) {
|
|
5486
5486
|
const res = await fetch(`${this.base}${path}`, {
|
|
5487
5487
|
method: "POST",
|
|
5488
5488
|
headers: this.headers,
|
|
5489
|
-
body: body != null ? JSON.stringify(body) : void 0
|
|
5489
|
+
body: body != null ? JSON.stringify(body) : void 0,
|
|
5490
|
+
signal
|
|
5490
5491
|
});
|
|
5491
5492
|
if (!res.ok)
|
|
5492
5493
|
await throwHttpError(res);
|
|
@@ -5531,8 +5532,8 @@ var WeChatClient = class {
|
|
|
5531
5532
|
async markChatRead(chatId) {
|
|
5532
5533
|
return this.post(`/api/chats/${encodeURIComponent(chatId)}/mark-read`);
|
|
5533
5534
|
}
|
|
5534
|
-
async openChat(chatId, clearUnreads) {
|
|
5535
|
-
return this.post(`/api/chats/${encodeURIComponent(chatId)}/open${qs({ clearUnreads })}
|
|
5535
|
+
async openChat(chatId, clearUnreads, signal, executionTimeoutMs) {
|
|
5536
|
+
return this.post(`/api/chats/${encodeURIComponent(chatId)}/open${qs({ clearUnreads, executionTimeoutMs })}`, void 0, signal);
|
|
5536
5537
|
}
|
|
5537
5538
|
async listGroupMembersPage(groupId, limit, cursor) {
|
|
5538
5539
|
const page = groupMembersPageSchema.parse(await this.get(`/api/groups/${encodeURIComponent(groupId)}/members${qs({ limit, cursor })}`));
|
|
@@ -6785,27 +6786,85 @@ var RETRYABLE_MEDIA_ERRORS = /* @__PURE__ */ new Set([
|
|
|
6785
6786
|
"VOICE_NOT_DOWNLOADED",
|
|
6786
6787
|
"IMAGE_NOT_STABLE"
|
|
6787
6788
|
]);
|
|
6788
|
-
var DEFAULT_MEDIA_POLL_ATTEMPTS =
|
|
6789
|
-
var DEFAULT_MEDIA_POLL_INTERVAL_MS =
|
|
6789
|
+
var DEFAULT_MEDIA_POLL_ATTEMPTS = 6;
|
|
6790
|
+
var DEFAULT_MEDIA_POLL_INTERVAL_MS = 500;
|
|
6791
|
+
var IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT_MS = 400;
|
|
6792
|
+
function createImageMaterializationTrigger(client, chatId, options) {
|
|
6793
|
+
const timeoutMs = options?.timeoutMs ?? IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT_MS;
|
|
6794
|
+
return async (result, attempt) => {
|
|
6795
|
+
if (result.type === "file") return;
|
|
6796
|
+
options?.log?.info?.(
|
|
6797
|
+
`[wechat:media] triggering chat reopen for image attempt=${attempt} skipOpen=${options?.skipOpen === true}`
|
|
6798
|
+
);
|
|
6799
|
+
const controller = new AbortController();
|
|
6800
|
+
const opened = Promise.resolve(client.openChat(chatId, true, controller.signal, timeoutMs));
|
|
6801
|
+
void opened.catch(() => void 0);
|
|
6802
|
+
try {
|
|
6803
|
+
await raceWithTimeout(opened, timeoutMs);
|
|
6804
|
+
} finally {
|
|
6805
|
+
controller.abort();
|
|
6806
|
+
}
|
|
6807
|
+
};
|
|
6808
|
+
}
|
|
6809
|
+
function imageMaterializationTriggerForMessage(opts) {
|
|
6810
|
+
const baseType = opts.messageType & 2147483647;
|
|
6811
|
+
if (baseType !== 3) return void 0;
|
|
6812
|
+
return createImageMaterializationTrigger(opts.client, opts.chatId, {
|
|
6813
|
+
log: opts.log,
|
|
6814
|
+
timeoutMs: opts.timeoutMs,
|
|
6815
|
+
skipOpen: opts.skipOpen
|
|
6816
|
+
});
|
|
6817
|
+
}
|
|
6818
|
+
function raceWithTimeout(promise, timeoutMs) {
|
|
6819
|
+
return new Promise((resolve, reject) => {
|
|
6820
|
+
const timer = setTimeout(() => {
|
|
6821
|
+
reject(Object.assign(new Error("IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT"), {
|
|
6822
|
+
code: "IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT"
|
|
6823
|
+
}));
|
|
6824
|
+
}, timeoutMs);
|
|
6825
|
+
promise.then(
|
|
6826
|
+
(value) => {
|
|
6827
|
+
clearTimeout(timer);
|
|
6828
|
+
resolve(value);
|
|
6829
|
+
},
|
|
6830
|
+
(error) => {
|
|
6831
|
+
clearTimeout(timer);
|
|
6832
|
+
reject(error);
|
|
6833
|
+
}
|
|
6834
|
+
);
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6790
6837
|
function isRetryable(result) {
|
|
6791
6838
|
if (result.data !== void 0) return false;
|
|
6792
6839
|
if (result.errorCode !== void 0) return RETRYABLE_MEDIA_ERRORS.has(result.errorCode);
|
|
6793
6840
|
return result.type === "pending";
|
|
6794
6841
|
}
|
|
6795
|
-
async function pollMedia(client, chatId, localId, log, maxAttempts = DEFAULT_MEDIA_POLL_ATTEMPTS, intervalMs = DEFAULT_MEDIA_POLL_INTERVAL_MS) {
|
|
6842
|
+
async function pollMedia(client, chatId, localId, log, maxAttempts = DEFAULT_MEDIA_POLL_ATTEMPTS, intervalMs = DEFAULT_MEDIA_POLL_INTERVAL_MS, onRetryTrigger) {
|
|
6796
6843
|
let lastResult;
|
|
6844
|
+
let triggerUsed = false;
|
|
6797
6845
|
const startedAt = Date.now();
|
|
6798
6846
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
6799
6847
|
const result = await client.getMedia(chatId, localId);
|
|
6800
6848
|
lastResult = result;
|
|
6801
6849
|
if (result.type === "unsupported") return result;
|
|
6802
6850
|
if (result.data || !isRetryable(result)) return result;
|
|
6851
|
+
if (!triggerUsed && onRetryTrigger) {
|
|
6852
|
+
triggerUsed = true;
|
|
6853
|
+
try {
|
|
6854
|
+
await onRetryTrigger(result, attempt);
|
|
6855
|
+
log?.info?.(`[wechat:media] download trigger completed attempt=${attempt}`);
|
|
6856
|
+
} catch {
|
|
6857
|
+
log?.info?.(`[wechat:media] download trigger failed attempt=${attempt} code=MEDIA_DOWNLOAD_TRIGGER_FAILED`);
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6803
6860
|
if (attempt < maxAttempts) {
|
|
6804
6861
|
log?.info?.(`[wechat:media] pending attempt=${attempt}/${maxAttempts} elapsedMs=${Date.now() - startedAt}`);
|
|
6805
6862
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
6806
6863
|
}
|
|
6807
6864
|
}
|
|
6808
|
-
log?.info?.(
|
|
6865
|
+
log?.info?.(
|
|
6866
|
+
`[wechat:media] retry exhausted attempts=${maxAttempts} elapsedMs=${Date.now() - startedAt} code=${lastResult?.errorCode ?? "MEDIA_NOT_DOWNLOADED"}`
|
|
6867
|
+
);
|
|
6809
6868
|
return lastResult ?? null;
|
|
6810
6869
|
}
|
|
6811
6870
|
|
|
@@ -7392,13 +7451,54 @@ function shouldBypassNewsappAuthorization(chat) {
|
|
|
7392
7451
|
return isNewsappChat(chat);
|
|
7393
7452
|
}
|
|
7394
7453
|
|
|
7454
|
+
// src/monitor-skip.ts
|
|
7455
|
+
var SYSTEM_SERVICE_USERNAMES = /* @__PURE__ */ new Set([
|
|
7456
|
+
"qmessage",
|
|
7457
|
+
"floatbottle",
|
|
7458
|
+
"medianote",
|
|
7459
|
+
"notifymessage",
|
|
7460
|
+
"weixin",
|
|
7461
|
+
"fmessage",
|
|
7462
|
+
"filehelper",
|
|
7463
|
+
"newsapp",
|
|
7464
|
+
"tmessage",
|
|
7465
|
+
"mphelper",
|
|
7466
|
+
"qqmail",
|
|
7467
|
+
"weixingongzhong",
|
|
7468
|
+
"qqsafe",
|
|
7469
|
+
"exmail_tool",
|
|
7470
|
+
"lbsapp",
|
|
7471
|
+
"pc_qq"
|
|
7472
|
+
]);
|
|
7473
|
+
var EMPTY_UNREAD_BACKOFF_MS = 5e3;
|
|
7474
|
+
var EMPTY_UNREAD_BACKOFF_MAX_MS = 6e4;
|
|
7475
|
+
function isOfficialAccount(chatId) {
|
|
7476
|
+
return chatId.startsWith("gh_") || SYSTEM_SERVICE_USERNAMES.has(chatId);
|
|
7477
|
+
}
|
|
7478
|
+
function isEmptyUnreadBackoffActive(backoff, chatId, now = Date.now()) {
|
|
7479
|
+
const entry = backoff.get(chatId);
|
|
7480
|
+
return entry !== void 0 && now < entry.nextRetryAt;
|
|
7481
|
+
}
|
|
7482
|
+
function applyEmptyUnreadSkip(chatId, opts) {
|
|
7483
|
+
if (opts.unreadCount <= 0) return null;
|
|
7484
|
+
const now = opts.now ?? Date.now();
|
|
7485
|
+
const seededLastSeen = opts.firstPoll && !opts.lastSeenId.has(chatId);
|
|
7486
|
+
if (seededLastSeen) {
|
|
7487
|
+
opts.lastSeenId.set(chatId, opts.prevLastSeen);
|
|
7488
|
+
}
|
|
7489
|
+
const retryCount = (opts.backoff.get(chatId)?.retryCount ?? 0) + 1;
|
|
7490
|
+
const backoffMs = Math.min(
|
|
7491
|
+
EMPTY_UNREAD_BACKOFF_MAX_MS,
|
|
7492
|
+
EMPTY_UNREAD_BACKOFF_MS * 2 ** Math.min(retryCount - 1, 6)
|
|
7493
|
+
);
|
|
7494
|
+
opts.backoff.set(chatId, { nextRetryAt: now + backoffMs, retryCount });
|
|
7495
|
+
return { seededLastSeen, backoffMs };
|
|
7496
|
+
}
|
|
7497
|
+
|
|
7395
7498
|
// src/monitor.ts
|
|
7396
7499
|
var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34, 43]);
|
|
7397
7500
|
var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
|
|
7398
7501
|
var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
|
|
7399
|
-
function isOfficialAccount(chatId) {
|
|
7400
|
-
return chatId.startsWith("gh_");
|
|
7401
|
-
}
|
|
7402
7502
|
function sleep(ms, signal) {
|
|
7403
7503
|
return new Promise((resolve) => {
|
|
7404
7504
|
const timer = setTimeout(resolve, ms);
|
|
@@ -7428,6 +7528,7 @@ async function startWeChatMonitor(opts) {
|
|
|
7428
7528
|
const pendingMessageScans = loadPendingResetRetries(account.accountId);
|
|
7429
7529
|
const inboundLedger = loadInboundEventLedger(account.accountId);
|
|
7430
7530
|
const mediaPipeline = loadMediaPipeline(account.accountId);
|
|
7531
|
+
const emptyUnreadBackoff = /* @__PURE__ */ new Map();
|
|
7431
7532
|
const cleanupMedia = async () => {
|
|
7432
7533
|
await mediaPipeline.cleanup({
|
|
7433
7534
|
olderThanMs: MEDIA_RETENTION_MS,
|
|
@@ -7546,9 +7647,13 @@ async function startWeChatMonitor(opts) {
|
|
|
7546
7647
|
}
|
|
7547
7648
|
}
|
|
7548
7649
|
}
|
|
7549
|
-
const unreadChats = chats.filter(
|
|
7550
|
-
|
|
7551
|
-
|
|
7650
|
+
const unreadChats = chats.filter((c) => {
|
|
7651
|
+
const chatId = c.username ?? c.id;
|
|
7652
|
+
if (isEmptyUnreadBackoffActive(emptyUnreadBackoff, chatId)) return false;
|
|
7653
|
+
if (c.unreadCount <= 0) return false;
|
|
7654
|
+
if (isNewsappChat(c)) return true;
|
|
7655
|
+
return !isOfficialAccount(chatId);
|
|
7656
|
+
});
|
|
7552
7657
|
for (const chat of unreadChats) {
|
|
7553
7658
|
const chatId = chat.username ?? chat.id;
|
|
7554
7659
|
if (chatScanState.initialScanComplete && !lastSeenId.has(chatId) && !startupBaselines.has(chatId)) {
|
|
@@ -7562,8 +7667,9 @@ async function startWeChatMonitor(opts) {
|
|
|
7562
7667
|
(chat) => !isNewsappChat(chat) || newsappHandled.get(chat.username ?? chat.id) !== `${chat.lastMsgLocalId ?? ""}:${chat.lastMessagePreview?.trim() ?? ""}`
|
|
7563
7668
|
);
|
|
7564
7669
|
for (const chatId of chatsToProcess.keys()) {
|
|
7565
|
-
if (isOfficialAccount(chatId) && chatId !== "newsapp")
|
|
7670
|
+
if (isOfficialAccount(chatId) && chatId !== "newsapp" || isEmptyUnreadBackoffActive(emptyUnreadBackoff, chatId)) {
|
|
7566
7671
|
chatsToProcess.delete(chatId);
|
|
7672
|
+
}
|
|
7567
7673
|
}
|
|
7568
7674
|
if (unreadChats.length > 0) {
|
|
7569
7675
|
log?.info?.(
|
|
@@ -7585,6 +7691,7 @@ async function startWeChatMonitor(opts) {
|
|
|
7585
7691
|
retryState.persist,
|
|
7586
7692
|
inboundLedger,
|
|
7587
7693
|
mediaPipeline,
|
|
7694
|
+
emptyUnreadBackoff,
|
|
7588
7695
|
account,
|
|
7589
7696
|
cfg,
|
|
7590
7697
|
log,
|
|
@@ -7615,7 +7722,7 @@ async function startWeChatMonitor(opts) {
|
|
|
7615
7722
|
for (const chat of chats) {
|
|
7616
7723
|
if (abortSignal.aborted) break;
|
|
7617
7724
|
const chatId = chat.username ?? chat.id;
|
|
7618
|
-
if (isOfficialAccount(chatId)) continue;
|
|
7725
|
+
if (isOfficialAccount(chatId) && chatId !== "newsapp" || isEmptyUnreadBackoffActive(emptyUnreadBackoff, chatId)) continue;
|
|
7619
7726
|
const prevSeen = lastSeenId.get(chatId);
|
|
7620
7727
|
if (prevSeen === void 0) continue;
|
|
7621
7728
|
if (unreadChats.some((c) => (c.username ?? c.id) === chatId)) continue;
|
|
@@ -7633,6 +7740,7 @@ async function startWeChatMonitor(opts) {
|
|
|
7633
7740
|
retryState.persist,
|
|
7634
7741
|
inboundLedger,
|
|
7635
7742
|
mediaPipeline,
|
|
7743
|
+
emptyUnreadBackoff,
|
|
7636
7744
|
account,
|
|
7637
7745
|
cfg,
|
|
7638
7746
|
log,
|
|
@@ -7677,7 +7785,7 @@ async function startWeChatMonitor(opts) {
|
|
|
7677
7785
|
connected: false
|
|
7678
7786
|
});
|
|
7679
7787
|
}
|
|
7680
|
-
async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAccount, policy, log) {
|
|
7788
|
+
async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAccount, policy, log, skipOpen) {
|
|
7681
7789
|
const core = getWeChatRuntime();
|
|
7682
7790
|
if (msg.isSelf) {
|
|
7683
7791
|
log?.info?.(`[wechat:${liveAccount.accountId}] Skipping self-sent msg ${msg.localId}`);
|
|
@@ -7710,7 +7818,21 @@ async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAcco
|
|
|
7710
7818
|
if (mayHaveMedia) {
|
|
7711
7819
|
log?.info?.(`[wechat:${liveAccount.accountId}] Checking media for msg ${msg.localId} (type ${baseType})`);
|
|
7712
7820
|
try {
|
|
7713
|
-
const result = await pollMedia(
|
|
7821
|
+
const result = await pollMedia(
|
|
7822
|
+
client,
|
|
7823
|
+
chatId,
|
|
7824
|
+
msg.localId,
|
|
7825
|
+
log,
|
|
7826
|
+
void 0,
|
|
7827
|
+
void 0,
|
|
7828
|
+
imageMaterializationTriggerForMessage({
|
|
7829
|
+
client,
|
|
7830
|
+
chatId,
|
|
7831
|
+
messageType: baseType,
|
|
7832
|
+
log,
|
|
7833
|
+
skipOpen
|
|
7834
|
+
})
|
|
7835
|
+
);
|
|
7714
7836
|
if (result && result.data && result.type !== "unsupported") {
|
|
7715
7837
|
hasMedia = true;
|
|
7716
7838
|
mediaSource = result.source;
|
|
@@ -8089,7 +8211,7 @@ function bufferGroupHistory(groupHistory, chatId, pm, limit) {
|
|
|
8089
8211
|
if (first) groupHistory.delete(first);
|
|
8090
8212
|
}
|
|
8091
8213
|
}
|
|
8092
|
-
async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHandled, startupBaselines, pendingMessageScans, persistRetryState, inboundLedger, mediaPipeline, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, catchup) {
|
|
8214
|
+
async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHandled, startupBaselines, pendingMessageScans, persistRetryState, inboundLedger, mediaPipeline, emptyUnreadBackoff, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, catchup) {
|
|
8093
8215
|
const core = getWeChatRuntime();
|
|
8094
8216
|
const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
|
|
8095
8217
|
const chatId = chat.username ?? chat.id;
|
|
@@ -8178,7 +8300,22 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
|
|
|
8178
8300
|
const fallback = newsappFallbackMessage(chat);
|
|
8179
8301
|
if (fallback) messages = [fallback];
|
|
8180
8302
|
}
|
|
8181
|
-
if (messages.length === 0)
|
|
8303
|
+
if (messages.length === 0) {
|
|
8304
|
+
const skip = applyEmptyUnreadSkip(chatId, {
|
|
8305
|
+
unreadCount: chat.unreadCount,
|
|
8306
|
+
firstPoll,
|
|
8307
|
+
prevLastSeen,
|
|
8308
|
+
lastSeenId,
|
|
8309
|
+
backoff: emptyUnreadBackoff
|
|
8310
|
+
});
|
|
8311
|
+
if (skip) {
|
|
8312
|
+
log?.info?.(
|
|
8313
|
+
`[wechat:${liveAccount.accountId}] ${chatId}: empty unread fetch, firstPoll=${firstPoll}, unreadCount=${chat.unreadCount}; seedLastSeen=${skip.seededLastSeen}, backoff=${skip.backoffMs}ms`
|
|
8314
|
+
);
|
|
8315
|
+
}
|
|
8316
|
+
return "skipped";
|
|
8317
|
+
}
|
|
8318
|
+
emptyUnreadBackoff.delete(chatId);
|
|
8182
8319
|
const enrichedBaseline = enrichStartupBaselineFromMessages(startupBaseline, messages);
|
|
8183
8320
|
if (enrichedBaseline) {
|
|
8184
8321
|
startupBaselines.set(chatId, enrichedBaseline);
|
|
@@ -8292,7 +8429,7 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
|
|
|
8292
8429
|
log?.info?.(
|
|
8293
8430
|
`[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}, content=${(msg.content || "").slice(0, 50)}`
|
|
8294
8431
|
);
|
|
8295
|
-
const pm = await prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAccount, policy, log);
|
|
8432
|
+
const pm = await prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAccount, policy, log, skipOpen);
|
|
8296
8433
|
if (pm) {
|
|
8297
8434
|
processed.push(pm);
|
|
8298
8435
|
} else {
|