@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.
Files changed (2) hide show
  1. package/dist/index.js +158 -21
  2. 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 = 30;
6789
- var DEFAULT_MEDIA_POLL_INTERVAL_MS = 1e3;
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?.(`[wechat:media] pending exhausted attempts=${maxAttempts} elapsedMs=${Date.now() - startedAt}`);
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
- (c) => c.unreadCount > 0 && !isOfficialAccount(c.username ?? c.id)
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(client, chatId, msg.localId, log);
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) return "skipped";
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyan-du/agent-wechat-openclaw",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",