claude-codex-wechat 0.1.53 → 0.1.54

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 CHANGED
@@ -41,15 +41,15 @@ claude-codex-wechat restart
41
41
 
42
42
  安装完,三步就能跑通:
43
43
 
44
+ **1. 启动**(首次会自动注册成系统后台服务,并自动配好接入公网中转,无需手动配置)
45
+
44
46
  ```bash
45
- # 1. 启动(首次会自动注册成系统后台服务,并自动配好接入公网中转,无需手动配置)
46
47
  claude-codex-wechat start
48
+ ```
47
49
 
48
- # 2. 打开管理页,用微信扫码登录完成绑定
49
- # 浏览器访问 http://127.0.0.1:8787
50
+ **2. 绑定微信** — 浏览器打开 <http://127.0.0.1:8787>,用微信扫码登录。
50
51
 
51
- # 3. 绑定成功后,直接在微信里给机器人发消息即可
52
- ```
52
+ **3. 开始用** — 绑定成功后,直接在微信里给机器人发消息即可。
53
53
 
54
54
  搞定。之后不管电脑锁屏还是你人在外面,只要开发机开着,就能用微信指挥它。
55
55
 
@@ -2760,6 +2760,8 @@ var WeixinOutboundGate = class _WeixinOutboundGate {
2760
2760
  hintedReplies = /* @__PURE__ */ new Map();
2761
2761
  /** The message held back on the final quota slot, pending proof of a follow-up. */
2762
2762
  pendingLast = /* @__PURE__ */ new Map();
2763
+ /** 配额归零导致消息入队、且尚未告知用户的会话。 */
2764
+ quotaExhaustedNotified = /* @__PURE__ */ new Set();
2763
2765
  static HINT_REPLY_TTL_MS = 2 * 60 * 60 * 1e3;
2764
2766
  hasPending(chatId) {
2765
2767
  return this.options.store.hasPendingOutbound(chatId);
@@ -2781,16 +2783,19 @@ var WeixinOutboundGate = class _WeixinOutboundGate {
2781
2783
  this.pendingLast.delete(chatId);
2782
2784
  await this.options.send(chatId, appendHintIfText(buffered));
2783
2785
  this.hintedReplies.set(chatId, Date.now());
2784
- store.enqueueOutbound(chatId, message);
2786
+ store.enqueueOutbound(chatId, stamp(message));
2785
2787
  return;
2786
2788
  }
2787
2789
  if (store.hasPendingOutbound(chatId)) {
2788
- store.enqueueOutbound(chatId, message);
2790
+ store.enqueueOutbound(chatId, stamp(message));
2789
2791
  return;
2790
2792
  }
2791
2793
  const quota = store.getQuota(chatId);
2792
2794
  if (quota.expired || quota.remaining <= 0) {
2793
- store.enqueueOutbound(chatId, message);
2795
+ if (!this.quotaExhaustedNotified.has(chatId)) {
2796
+ this.quotaExhaustedNotified.add(chatId);
2797
+ }
2798
+ store.enqueueOutbound(chatId, stamp(message));
2794
2799
  return;
2795
2800
  }
2796
2801
  if (quota.remaining === 1) {
@@ -2810,6 +2815,24 @@ var WeixinOutboundGate = class _WeixinOutboundGate {
2810
2815
  this.pendingLast.delete(chatId);
2811
2816
  await this.options.send(chatId, buffered);
2812
2817
  }
2818
+ /**
2819
+ * 取出「配额耗尽导致回复被排队」的一次性提示文案,没有则返回 null。
2820
+ * 取走即清除,保证同一次耗尽只提示一次,不会每条排队消息都刷屏。
2821
+ */
2822
+ takeQuotaExhaustedNotice(chatId) {
2823
+ if (!this.quotaExhaustedNotified.delete(chatId)) return null;
2824
+ return "\u26A0\uFE0F \u672C\u8F6E\u56DE\u590D\u5DF2\u8D85\u51FA\u5FAE\u4FE1 24 \u5C0F\u65F6\u63A8\u9001\u989D\u5EA6\uFF0C\u5269\u4F59\u5185\u5BB9\u5DF2\u6682\u5B58\u3002\u56DE\u590D\u4EFB\u610F\u6D88\u606F\u5373\u53EF\u7EE7\u7EED\u63A5\u6536\u3002";
2825
+ }
2826
+ /**
2827
+ * 丢弃该会话全部待发消息(含缓冲在最后一格配额上的那条)。
2828
+ * 会话被替换时调用,避免旧会话的回复稍后串进新会话。
2829
+ */
2830
+ async discardPending(chatId) {
2831
+ this.pendingLast.delete(chatId);
2832
+ this.hintedReplies.delete(chatId);
2833
+ this.quotaExhaustedNotified.delete(chatId);
2834
+ this.options.store.clearOutbound(chatId);
2835
+ }
2813
2836
  async drain(chatId) {
2814
2837
  const { store } = this.options;
2815
2838
  while (store.hasPendingOutbound(chatId)) {
@@ -2817,6 +2840,10 @@ var WeixinOutboundGate = class _WeixinOutboundGate {
2817
2840
  if (quota.expired || quota.remaining <= 0) break;
2818
2841
  const pending = store.peekOutbound(chatId);
2819
2842
  const next = pending[0];
2843
+ if (isStale(next)) {
2844
+ store.shiftOutbound(chatId);
2845
+ continue;
2846
+ }
2820
2847
  const isLastSlot = quota.remaining === 1;
2821
2848
  const hasMore = pending.length > 1;
2822
2849
  if (isLastSlot && hasMore) {
@@ -2836,6 +2863,13 @@ function appendHintIfText(item) {
2836
2863
  if (MEDIA_KINDS.has(item.kind)) return item;
2837
2864
  return { ...item, text: item.text + CONTINUATION_HINT };
2838
2865
  }
2866
+ function isStale(item, now = Date.now()) {
2867
+ if (typeof item.enqueuedAt !== "number") return false;
2868
+ return now - item.enqueuedAt >= PUSH_WINDOW_MS;
2869
+ }
2870
+ function stamp(item) {
2871
+ return item.enqueuedAt === void 0 ? { ...item, enqueuedAt: Date.now() } : item;
2872
+ }
2839
2873
 
2840
2874
  // src/providers/claude-code/claudeStreamingRunner.ts
2841
2875
  import { spawn as spawn3 } from "node:child_process";
@@ -3680,6 +3714,21 @@ function readTurnId(value) {
3680
3714
  if (typeof record.turnId === "string" && record.turnId) return record.turnId;
3681
3715
  return void 0;
3682
3716
  }
3717
+ function readTurnAgentMessages(value) {
3718
+ if (!value || typeof value !== "object") return [];
3719
+ const turn = value.turn;
3720
+ if (!turn || typeof turn !== "object") return [];
3721
+ const items = turn.items;
3722
+ if (!Array.isArray(items)) return [];
3723
+ const texts = [];
3724
+ for (const item of items) {
3725
+ if (!item || typeof item !== "object") continue;
3726
+ const record = item;
3727
+ if (record.type !== "agentMessage") continue;
3728
+ if (typeof record.text === "string" && record.text.trim()) texts.push(record.text);
3729
+ }
3730
+ return texts;
3731
+ }
3683
3732
  function readTurnError(value) {
3684
3733
  if (!value || typeof value !== "object") return void 0;
3685
3734
  const record = value;
@@ -3734,6 +3783,7 @@ var CodexInteractiveRunner = class {
3734
3783
  session.activeMessage = void 0;
3735
3784
  session.activeTurnId = void 0;
3736
3785
  session.turnClosed = false;
3786
+ session.emittedTextThisTurn = false;
3737
3787
  session.turnCompletedPromise = new Promise((resolve2) => {
3738
3788
  session.turnCompletedResolver = resolve2;
3739
3789
  });
@@ -3880,6 +3930,7 @@ var CodexInteractiveRunner = class {
3880
3930
  });
3881
3931
  client.onNotification("turn/completed", (params) => {
3882
3932
  this.flushActiveMessage(session);
3933
+ this.recoverTurnMessages(session, params);
3883
3934
  const turnError = readTurnError(params);
3884
3935
  if (turnError) this.enqueueEvent(session, { type: "error", error: turnError });
3885
3936
  session.turnClosed = true;
@@ -3920,6 +3971,18 @@ var CodexInteractiveRunner = class {
3920
3971
  this.enqueueEvent(session, { type: "text_delta", text: session.activeMessage.text });
3921
3972
  this.enqueueEvent(session, { type: "message_done" });
3922
3973
  session.activeMessage = void 0;
3974
+ session.emittedTextThisTurn = true;
3975
+ }
3976
+ // 瞬时(非流式)回合里 app-server 可能完全不发 item/agentMessage/delta,
3977
+ // 助手文本只出现在 turn/completed 的 items 中。此时若不兜底,回复会静默丢失。
3978
+ // 只在本轮一条文本都没发出时才补发——宁可少发,也不要重复刷屏、白耗微信配额。
3979
+ recoverTurnMessages(session, params) {
3980
+ if (session.emittedTextThisTurn) return;
3981
+ for (const text of readTurnAgentMessages(params)) {
3982
+ this.enqueueEvent(session, { type: "text_delta", text });
3983
+ this.enqueueEvent(session, { type: "message_done" });
3984
+ session.emittedTextThisTurn = true;
3985
+ }
3923
3986
  }
3924
3987
  enqueueEvent(session, event) {
3925
3988
  session.pendingEvents.push(event);
@@ -4363,7 +4426,19 @@ var MessageRouter = class _MessageRouter {
4363
4426
  }
4364
4427
  }
4365
4428
  async handleMessage(message) {
4366
- return this.withSessionChangeNotification(() => this.handleMessageInner(message));
4429
+ try {
4430
+ return await this.withSessionChangeNotification(() => this.handleMessageInner(message));
4431
+ } catch (error) {
4432
+ const detail = error instanceof Error ? error.message : String(error);
4433
+ console.error("[bridge] handleMessage failed:", message.chatId, error);
4434
+ await this.sendToChat({
4435
+ chatId: message.chatId,
4436
+ kind: "status",
4437
+ text: `\u26A0\uFE0F \u6865\u63A5\u5C42\u5185\u90E8\u9519\u8BEF\uFF1A${detail}
4438
+ \u53EF\u91CD\u53D1\u6D88\u606F\u91CD\u8BD5\uFF1B\u82E5\u6301\u7EED\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5 provider \u662F\u5426\u53EF\u7528\uFF08claude-codex-wechat doctor\uFF09\u3002`
4439
+ }).catch(() => void 0);
4440
+ return { status: "accepted" };
4441
+ }
4367
4442
  }
4368
4443
  async handleMessageInner(message) {
4369
4444
  let user = this.options.resolveUser(message);
@@ -4569,6 +4644,7 @@ var MessageRouter = class _MessageRouter {
4569
4644
  });
4570
4645
  const isLive = () => this.activeGenerations.get(message.chatId)?.genId === genId && (this.latestMutatingSeq.get(message.chatId) ?? 0) <= seq;
4571
4646
  let bufferedText = "";
4647
+ let deliveredSomething = false;
4572
4648
  const typingMinIntervalMs = this.options.typingKeepaliveMs ?? 5e3;
4573
4649
  let lastTypingAt = Date.now();
4574
4650
  await this.options.channel.setTyping?.(message.chatId, true);
@@ -4593,6 +4669,7 @@ var MessageRouter = class _MessageRouter {
4593
4669
  if (event.type === "message_done" && bufferedText.trim()) {
4594
4670
  await this.sendToChat({ chatId: message.chatId, kind: "text", text: bufferedText });
4595
4671
  bufferedText = "";
4672
+ deliveredSomething = true;
4596
4673
  }
4597
4674
  if (event.type === "choice_prompt" && event.labels.length > 0) {
4598
4675
  this.pendingChoices.set(message.chatId, { sessionId: session.id, labels: event.labels, multiSelect: event.multiSelect });
@@ -4622,11 +4699,20 @@ var MessageRouter = class _MessageRouter {
4622
4699
  kind: "status",
4623
4700
  text: errorText
4624
4701
  });
4702
+ deliveredSomething = true;
4625
4703
  }
4626
4704
  }
4627
4705
  if (isLive() && bufferedText.trim()) {
4628
4706
  await this.sendToChat({ chatId: message.chatId, kind: "text", text: bufferedText });
4629
4707
  bufferedText = "";
4708
+ deliveredSomething = true;
4709
+ }
4710
+ if (isLive() && !deliveredSomething) {
4711
+ await this.sendToChat({
4712
+ chatId: message.chatId,
4713
+ kind: "status",
4714
+ text: "\u26A0\uFE0F \u672C\u8F6E\u6CA1\u6709\u8FD4\u56DE\u4EFB\u4F55\u5185\u5BB9\uFF08provider \u5DF2\u7ED3\u675F\u4F46\u672A\u4EA7\u51FA\u56DE\u590D\uFF09\u3002\u53EF\u91CD\u53D1\u6D88\u606F\u91CD\u8BD5\u3002"
4715
+ });
4630
4716
  }
4631
4717
  if (isLive()) {
4632
4718
  const finalBinding = this.conversation.getCurrent();
@@ -4643,6 +4729,10 @@ var MessageRouter = class _MessageRouter {
4643
4729
  const stillMine = this.activeGenerations.get(message.chatId)?.genId === genId;
4644
4730
  if (stillMine) this.activeGenerations.delete(message.chatId);
4645
4731
  await Promise.resolve(this.options.outboundGate?.finalize?.(message.chatId)).catch(() => void 0);
4732
+ const quotaNotice = this.options.outboundGate?.takeQuotaExhaustedNotice?.(message.chatId);
4733
+ if (quotaNotice) {
4734
+ await this.sendToChat({ chatId: message.chatId, kind: "status", text: quotaNotice }).catch(() => void 0);
4735
+ }
4646
4736
  if (stillMine) await this.options.channel.setTyping?.(message.chatId, false);
4647
4737
  }
4648
4738
  }
@@ -4718,6 +4808,7 @@ var MessageRouter = class _MessageRouter {
4718
4808
  providerId,
4719
4809
  cwd
4720
4810
  });
4811
+ await Promise.resolve(this.options.outboundGate?.discardPending?.(chatId)).catch(() => void 0);
4721
4812
  await this.sendToChat({
4722
4813
  chatId,
4723
4814
  kind: "status",
@@ -5713,7 +5804,20 @@ function createUpdateChecker(opts) {
5713
5804
  }
5714
5805
 
5715
5806
  // src/daemon/bootstrap.ts
5807
+ var crashLoggingInstalled = false;
5808
+ function installCrashLogging() {
5809
+ if (crashLoggingInstalled) return;
5810
+ crashLoggingInstalled = true;
5811
+ process.on("unhandledRejection", (reason) => {
5812
+ console.error("[bridge] unhandled rejection:", reason);
5813
+ });
5814
+ process.on("uncaughtException", (error) => {
5815
+ console.error("[bridge] uncaught exception, exiting:", error);
5816
+ process.exit(1);
5817
+ });
5818
+ }
5716
5819
  async function startDaemon(options) {
5820
+ installCrashLogging();
5717
5821
  const port = options.port ?? Number(process.env.BRIDGE_PORT ?? 8787);
5718
5822
  const host = options.host ?? "0.0.0.0";
5719
5823
  const configPath = options.configPath ?? process.env.BRIDGE_CONFIG ?? defaultConfigPath();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-codex-wechat",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.5.0",