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 +5 -5
- package/dist/server/cli.js +108 -4
- package/package.json +1 -1
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
|
-
|
|
49
|
-
# 浏览器访问 http://127.0.0.1:8787
|
|
50
|
+
**2. 绑定微信** — 浏览器打开 <http://127.0.0.1:8787>,用微信扫码登录。
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
```
|
|
52
|
+
**3. 开始用** — 绑定成功后,直接在微信里给机器人发消息即可。
|
|
53
53
|
|
|
54
54
|
搞定。之后不管电脑锁屏还是你人在外面,只要开发机开着,就能用微信指挥它。
|
|
55
55
|
|
package/dist/server/cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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();
|