@kyan-du/agent-wechat-openclaw 0.14.0 → 0.14.2

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 +197 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5296,6 +5296,14 @@ var openChatResultSchema = external_exports.object({
5296
5296
  errorCode: external_exports.string().optional(),
5297
5297
  error: external_exports.string().optional()
5298
5298
  });
5299
+ var downloadFileParamsSchema = external_exports.object({
5300
+ chatId: external_exports.string().min(1),
5301
+ filename: external_exports.string().min(1).optional(),
5302
+ executionTimeoutMs: external_exports.number().int().positive().optional()
5303
+ });
5304
+ var downloadFileResultSchema = openChatResultSchema.extend({
5305
+ clicked: external_exports.boolean().optional()
5306
+ });
5299
5307
  var idempotencyKeySchema = external_exports.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/);
5300
5308
  var forwardedMessageNodeSchema = external_exports.lazy(() => external_exports.object({
5301
5309
  sender: external_exports.string().optional(),
@@ -5482,11 +5490,12 @@ var WeChatClient = class {
5482
5490
  await throwHttpError(res);
5483
5491
  return res.json();
5484
5492
  }
5485
- async post(path, body) {
5493
+ async post(path, body, signal) {
5486
5494
  const res = await fetch(`${this.base}${path}`, {
5487
5495
  method: "POST",
5488
5496
  headers: this.headers,
5489
- body: body != null ? JSON.stringify(body) : void 0
5497
+ body: body != null ? JSON.stringify(body) : void 0,
5498
+ signal
5490
5499
  });
5491
5500
  if (!res.ok)
5492
5501
  await throwHttpError(res);
@@ -5531,8 +5540,11 @@ var WeChatClient = class {
5531
5540
  async markChatRead(chatId) {
5532
5541
  return this.post(`/api/chats/${encodeURIComponent(chatId)}/mark-read`);
5533
5542
  }
5534
- async openChat(chatId, clearUnreads) {
5535
- return this.post(`/api/chats/${encodeURIComponent(chatId)}/open${qs({ clearUnreads })}`);
5543
+ async openChat(chatId, clearUnreads, signal, executionTimeoutMs) {
5544
+ return this.post(`/api/chats/${encodeURIComponent(chatId)}/open${qs({ clearUnreads, executionTimeoutMs })}`, void 0, signal);
5545
+ }
5546
+ async downloadFile(chatId, filename, signal, executionTimeoutMs) {
5547
+ return this.post(`/api/chats/${encodeURIComponent(chatId)}/download-file${qs({ filename, executionTimeoutMs })}`, void 0, signal);
5536
5548
  }
5537
5549
  async listGroupMembersPage(groupId, limit, cursor) {
5538
5550
  const page = groupMembersPageSchema.parse(await this.get(`/api/groups/${encodeURIComponent(groupId)}/members${qs({ limit, cursor })}`));
@@ -6785,27 +6797,113 @@ var RETRYABLE_MEDIA_ERRORS = /* @__PURE__ */ new Set([
6785
6797
  "VOICE_NOT_DOWNLOADED",
6786
6798
  "IMAGE_NOT_STABLE"
6787
6799
  ]);
6788
- var DEFAULT_MEDIA_POLL_ATTEMPTS = 30;
6789
- var DEFAULT_MEDIA_POLL_INTERVAL_MS = 1e3;
6800
+ var DEFAULT_MEDIA_POLL_ATTEMPTS = 6;
6801
+ var DEFAULT_MEDIA_POLL_INTERVAL_MS = 500;
6802
+ var IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT_MS = 400;
6803
+ var FILE_MATERIALIZATION_CLICK_TIMEOUT_MS = 8e3;
6804
+ function shouldTriggerMediaMaterialization(result) {
6805
+ return result.type === "image" || result.type === "file" || result.type === "pending";
6806
+ }
6807
+ function isFileMaterialization(result) {
6808
+ return result.type === "file" || result.errorCode === "FILE_NOT_DOWNLOADED" || result.errorCode === "FILE_NOT_STABLE";
6809
+ }
6810
+ async function triggerImageOpenChat(client, chatId, timeoutMs, result, attempt, options) {
6811
+ options?.log?.info?.(
6812
+ `[wechat:media] triggering chat reopen for ${result.type} attempt=${attempt} skipOpen=${options?.skipOpen === true}`
6813
+ );
6814
+ const controller = new AbortController();
6815
+ const opened = Promise.resolve(client.openChat(chatId, true, controller.signal, timeoutMs));
6816
+ void opened.catch(() => void 0);
6817
+ try {
6818
+ await raceWithTimeout(opened, timeoutMs);
6819
+ } finally {
6820
+ controller.abort();
6821
+ }
6822
+ }
6823
+ function triggerFileBubbleClick(client, chatId, result, attempt, options) {
6824
+ const filename = result.filename?.trim() || void 0;
6825
+ const timeoutMs = options?.timeoutMs ?? FILE_MATERIALIZATION_CLICK_TIMEOUT_MS;
6826
+ options?.log?.info?.(
6827
+ `[wechat:media] triggering file bubble click attempt=${attempt} filename=${filename ?? ""} skipOpen=${options?.skipOpen === true}`
6828
+ );
6829
+ if (!client.downloadFile) {
6830
+ throw Object.assign(new Error("FILE_DOWNLOAD_TRIGGER_UNAVAILABLE"), {
6831
+ code: "FILE_DOWNLOAD_TRIGGER_UNAVAILABLE"
6832
+ });
6833
+ }
6834
+ const clicked = Promise.resolve(client.downloadFile(chatId, filename, void 0, timeoutMs));
6835
+ void clicked.catch(() => void 0);
6836
+ }
6837
+ function createMediaMaterializationTrigger(client, chatId, options) {
6838
+ const timeoutMs = options?.timeoutMs ?? IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT_MS;
6839
+ return async (result, attempt) => {
6840
+ if (!shouldTriggerMediaMaterialization(result)) return;
6841
+ if (isFileMaterialization(result)) {
6842
+ triggerFileBubbleClick(client, chatId, result, attempt, options);
6843
+ return;
6844
+ }
6845
+ await triggerImageOpenChat(client, chatId, timeoutMs, result, attempt, options);
6846
+ };
6847
+ }
6848
+ function mediaMaterializationTriggerForMessage(opts) {
6849
+ const baseType = opts.messageType & 2147483647;
6850
+ if (baseType !== 3 && baseType !== 49) return void 0;
6851
+ return createMediaMaterializationTrigger(opts.client, opts.chatId, {
6852
+ log: opts.log,
6853
+ timeoutMs: opts.timeoutMs,
6854
+ skipOpen: opts.skipOpen
6855
+ });
6856
+ }
6857
+ function raceWithTimeout(promise, timeoutMs) {
6858
+ return new Promise((resolve, reject) => {
6859
+ const timer = setTimeout(() => {
6860
+ reject(Object.assign(new Error("IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT"), {
6861
+ code: "IMAGE_MATERIALIZATION_OPEN_CHAT_TIMEOUT"
6862
+ }));
6863
+ }, timeoutMs);
6864
+ promise.then(
6865
+ (value) => {
6866
+ clearTimeout(timer);
6867
+ resolve(value);
6868
+ },
6869
+ (error) => {
6870
+ clearTimeout(timer);
6871
+ reject(error);
6872
+ }
6873
+ );
6874
+ });
6875
+ }
6790
6876
  function isRetryable(result) {
6791
6877
  if (result.data !== void 0) return false;
6792
6878
  if (result.errorCode !== void 0) return RETRYABLE_MEDIA_ERRORS.has(result.errorCode);
6793
6879
  return result.type === "pending";
6794
6880
  }
6795
- async function pollMedia(client, chatId, localId, log, maxAttempts = DEFAULT_MEDIA_POLL_ATTEMPTS, intervalMs = DEFAULT_MEDIA_POLL_INTERVAL_MS) {
6881
+ async function pollMedia(client, chatId, localId, log, maxAttempts = DEFAULT_MEDIA_POLL_ATTEMPTS, intervalMs = DEFAULT_MEDIA_POLL_INTERVAL_MS, onRetryTrigger) {
6796
6882
  let lastResult;
6883
+ let triggerUsed = false;
6797
6884
  const startedAt = Date.now();
6798
6885
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
6799
6886
  const result = await client.getMedia(chatId, localId);
6800
6887
  lastResult = result;
6801
6888
  if (result.type === "unsupported") return result;
6802
6889
  if (result.data || !isRetryable(result)) return result;
6890
+ if (!triggerUsed && onRetryTrigger) {
6891
+ triggerUsed = true;
6892
+ try {
6893
+ await onRetryTrigger(result, attempt);
6894
+ log?.info?.(`[wechat:media] download trigger completed attempt=${attempt}`);
6895
+ } catch {
6896
+ log?.info?.(`[wechat:media] download trigger failed attempt=${attempt} code=MEDIA_DOWNLOAD_TRIGGER_FAILED`);
6897
+ }
6898
+ }
6803
6899
  if (attempt < maxAttempts) {
6804
6900
  log?.info?.(`[wechat:media] pending attempt=${attempt}/${maxAttempts} elapsedMs=${Date.now() - startedAt}`);
6805
6901
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
6806
6902
  }
6807
6903
  }
6808
- log?.info?.(`[wechat:media] pending exhausted attempts=${maxAttempts} elapsedMs=${Date.now() - startedAt}`);
6904
+ log?.info?.(
6905
+ `[wechat:media] retry exhausted attempts=${maxAttempts} elapsedMs=${Date.now() - startedAt} code=${lastResult?.errorCode ?? "MEDIA_NOT_DOWNLOADED"}`
6906
+ );
6809
6907
  return lastResult ?? null;
6810
6908
  }
6811
6909
 
@@ -7392,13 +7490,54 @@ function shouldBypassNewsappAuthorization(chat) {
7392
7490
  return isNewsappChat(chat);
7393
7491
  }
7394
7492
 
7493
+ // src/monitor-skip.ts
7494
+ var SYSTEM_SERVICE_USERNAMES = /* @__PURE__ */ new Set([
7495
+ "qmessage",
7496
+ "floatbottle",
7497
+ "medianote",
7498
+ "notifymessage",
7499
+ "weixin",
7500
+ "fmessage",
7501
+ "filehelper",
7502
+ "newsapp",
7503
+ "tmessage",
7504
+ "mphelper",
7505
+ "qqmail",
7506
+ "weixingongzhong",
7507
+ "qqsafe",
7508
+ "exmail_tool",
7509
+ "lbsapp",
7510
+ "pc_qq"
7511
+ ]);
7512
+ var EMPTY_UNREAD_BACKOFF_MS = 5e3;
7513
+ var EMPTY_UNREAD_BACKOFF_MAX_MS = 6e4;
7514
+ function isOfficialAccount(chatId) {
7515
+ return chatId.startsWith("gh_") || SYSTEM_SERVICE_USERNAMES.has(chatId);
7516
+ }
7517
+ function isEmptyUnreadBackoffActive(backoff, chatId, now = Date.now()) {
7518
+ const entry = backoff.get(chatId);
7519
+ return entry !== void 0 && now < entry.nextRetryAt;
7520
+ }
7521
+ function applyEmptyUnreadSkip(chatId, opts) {
7522
+ if (opts.unreadCount <= 0) return null;
7523
+ const now = opts.now ?? Date.now();
7524
+ const seededLastSeen = opts.firstPoll && !opts.lastSeenId.has(chatId);
7525
+ if (seededLastSeen) {
7526
+ opts.lastSeenId.set(chatId, opts.prevLastSeen);
7527
+ }
7528
+ const retryCount = (opts.backoff.get(chatId)?.retryCount ?? 0) + 1;
7529
+ const backoffMs = Math.min(
7530
+ EMPTY_UNREAD_BACKOFF_MAX_MS,
7531
+ EMPTY_UNREAD_BACKOFF_MS * 2 ** Math.min(retryCount - 1, 6)
7532
+ );
7533
+ opts.backoff.set(chatId, { nextRetryAt: now + backoffMs, retryCount });
7534
+ return { seededLastSeen, backoffMs };
7535
+ }
7536
+
7395
7537
  // src/monitor.ts
7396
7538
  var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34, 43]);
7397
7539
  var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
7398
7540
  var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
7399
- function isOfficialAccount(chatId) {
7400
- return chatId.startsWith("gh_");
7401
- }
7402
7541
  function sleep(ms, signal) {
7403
7542
  return new Promise((resolve) => {
7404
7543
  const timer = setTimeout(resolve, ms);
@@ -7428,6 +7567,7 @@ async function startWeChatMonitor(opts) {
7428
7567
  const pendingMessageScans = loadPendingResetRetries(account.accountId);
7429
7568
  const inboundLedger = loadInboundEventLedger(account.accountId);
7430
7569
  const mediaPipeline = loadMediaPipeline(account.accountId);
7570
+ const emptyUnreadBackoff = /* @__PURE__ */ new Map();
7431
7571
  const cleanupMedia = async () => {
7432
7572
  await mediaPipeline.cleanup({
7433
7573
  olderThanMs: MEDIA_RETENTION_MS,
@@ -7546,9 +7686,13 @@ async function startWeChatMonitor(opts) {
7546
7686
  }
7547
7687
  }
7548
7688
  }
7549
- const unreadChats = chats.filter(
7550
- (c) => c.unreadCount > 0 && !isOfficialAccount(c.username ?? c.id)
7551
- );
7689
+ const unreadChats = chats.filter((c) => {
7690
+ const chatId = c.username ?? c.id;
7691
+ if (isEmptyUnreadBackoffActive(emptyUnreadBackoff, chatId)) return false;
7692
+ if (c.unreadCount <= 0) return false;
7693
+ if (isNewsappChat(c)) return true;
7694
+ return !isOfficialAccount(chatId);
7695
+ });
7552
7696
  for (const chat of unreadChats) {
7553
7697
  const chatId = chat.username ?? chat.id;
7554
7698
  if (chatScanState.initialScanComplete && !lastSeenId.has(chatId) && !startupBaselines.has(chatId)) {
@@ -7562,8 +7706,9 @@ async function startWeChatMonitor(opts) {
7562
7706
  (chat) => !isNewsappChat(chat) || newsappHandled.get(chat.username ?? chat.id) !== `${chat.lastMsgLocalId ?? ""}:${chat.lastMessagePreview?.trim() ?? ""}`
7563
7707
  );
7564
7708
  for (const chatId of chatsToProcess.keys()) {
7565
- if (isOfficialAccount(chatId) && chatId !== "newsapp")
7709
+ if (isOfficialAccount(chatId) && chatId !== "newsapp" || isEmptyUnreadBackoffActive(emptyUnreadBackoff, chatId)) {
7566
7710
  chatsToProcess.delete(chatId);
7711
+ }
7567
7712
  }
7568
7713
  if (unreadChats.length > 0) {
7569
7714
  log?.info?.(
@@ -7585,6 +7730,7 @@ async function startWeChatMonitor(opts) {
7585
7730
  retryState.persist,
7586
7731
  inboundLedger,
7587
7732
  mediaPipeline,
7733
+ emptyUnreadBackoff,
7588
7734
  account,
7589
7735
  cfg,
7590
7736
  log,
@@ -7615,7 +7761,7 @@ async function startWeChatMonitor(opts) {
7615
7761
  for (const chat of chats) {
7616
7762
  if (abortSignal.aborted) break;
7617
7763
  const chatId = chat.username ?? chat.id;
7618
- if (isOfficialAccount(chatId)) continue;
7764
+ if (isOfficialAccount(chatId) && chatId !== "newsapp" || isEmptyUnreadBackoffActive(emptyUnreadBackoff, chatId)) continue;
7619
7765
  const prevSeen = lastSeenId.get(chatId);
7620
7766
  if (prevSeen === void 0) continue;
7621
7767
  if (unreadChats.some((c) => (c.username ?? c.id) === chatId)) continue;
@@ -7633,6 +7779,7 @@ async function startWeChatMonitor(opts) {
7633
7779
  retryState.persist,
7634
7780
  inboundLedger,
7635
7781
  mediaPipeline,
7782
+ emptyUnreadBackoff,
7636
7783
  account,
7637
7784
  cfg,
7638
7785
  log,
@@ -7677,7 +7824,7 @@ async function startWeChatMonitor(opts) {
7677
7824
  connected: false
7678
7825
  });
7679
7826
  }
7680
- async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAccount, policy, log) {
7827
+ async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAccount, policy, log, skipOpen) {
7681
7828
  const core = getWeChatRuntime();
7682
7829
  if (msg.isSelf) {
7683
7830
  log?.info?.(`[wechat:${liveAccount.accountId}] Skipping self-sent msg ${msg.localId}`);
@@ -7710,7 +7857,21 @@ async function prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAcco
7710
7857
  if (mayHaveMedia) {
7711
7858
  log?.info?.(`[wechat:${liveAccount.accountId}] Checking media for msg ${msg.localId} (type ${baseType})`);
7712
7859
  try {
7713
- const result = await pollMedia(client, chatId, msg.localId, log);
7860
+ const result = await pollMedia(
7861
+ client,
7862
+ chatId,
7863
+ msg.localId,
7864
+ log,
7865
+ void 0,
7866
+ void 0,
7867
+ mediaMaterializationTriggerForMessage({
7868
+ client,
7869
+ chatId,
7870
+ messageType: baseType,
7871
+ log,
7872
+ skipOpen
7873
+ })
7874
+ );
7714
7875
  if (result && result.data && result.type !== "unsupported") {
7715
7876
  hasMedia = true;
7716
7877
  mediaSource = result.source;
@@ -8089,7 +8250,7 @@ function bufferGroupHistory(groupHistory, chatId, pm, limit) {
8089
8250
  if (first) groupHistory.delete(first);
8090
8251
  }
8091
8252
  }
8092
- async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHandled, startupBaselines, pendingMessageScans, persistRetryState, inboundLedger, mediaPipeline, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, catchup) {
8253
+ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHandled, startupBaselines, pendingMessageScans, persistRetryState, inboundLedger, mediaPipeline, emptyUnreadBackoff, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit, catchup) {
8093
8254
  const core = getWeChatRuntime();
8094
8255
  const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
8095
8256
  const chatId = chat.username ?? chat.id;
@@ -8178,7 +8339,22 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
8178
8339
  const fallback = newsappFallbackMessage(chat);
8179
8340
  if (fallback) messages = [fallback];
8180
8341
  }
8181
- if (messages.length === 0) return "skipped";
8342
+ if (messages.length === 0) {
8343
+ const skip = applyEmptyUnreadSkip(chatId, {
8344
+ unreadCount: chat.unreadCount,
8345
+ firstPoll,
8346
+ prevLastSeen,
8347
+ lastSeenId,
8348
+ backoff: emptyUnreadBackoff
8349
+ });
8350
+ if (skip) {
8351
+ log?.info?.(
8352
+ `[wechat:${liveAccount.accountId}] ${chatId}: empty unread fetch, firstPoll=${firstPoll}, unreadCount=${chat.unreadCount}; seedLastSeen=${skip.seededLastSeen}, backoff=${skip.backoffMs}ms`
8353
+ );
8354
+ }
8355
+ return "skipped";
8356
+ }
8357
+ emptyUnreadBackoff.delete(chatId);
8182
8358
  const enrichedBaseline = enrichStartupBaselineFromMessages(startupBaseline, messages);
8183
8359
  if (enrichedBaseline) {
8184
8360
  startupBaselines.set(chatId, enrichedBaseline);
@@ -8292,7 +8468,7 @@ async function processUnreadChat(client, chat, lastSeenId, equalCursorUnreadHand
8292
8468
  log?.info?.(
8293
8469
  `[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}, content=${(msg.content || "").slice(0, 50)}`
8294
8470
  );
8295
- const pm = await prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAccount, policy, log);
8471
+ const pm = await prepareMessage(client, mediaPipeline, msg, chatId, chat, liveAccount, policy, log, skipOpen);
8296
8472
  if (pm) {
8297
8473
  processed.push(pm);
8298
8474
  } 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.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",