@unlinearity/cli-wechat-bridge 1.1.5 → 1.1.6
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 +157 -142
- package/bin/_run-entry.mjs +20 -5
- package/dist/bridge/bridge-adapters.claude.js +2 -1
- package/dist/bridge/bridge-adapters.codex.js +214 -23
- package/dist/bridge/bridge-adapters.core.js +2 -1
- package/dist/bridge/bridge-adapters.pi.js +1 -1
- package/dist/bridge/bridge-adapters.shared.js +2 -1
- package/dist/bridge/bridge-final-reply.js +2 -65
- package/dist/bridge/bridge-state.js +1 -1
- package/dist/bridge/bridge-utils.js +4 -96
- package/dist/bridge/wechat-bridge.js +226 -189
- package/dist/bridge/wechat-forwarding.js +2 -58
- package/dist/bridge/wechat-outbound-queue.js +2 -0
- package/dist/channels/wechat/bridge-final-reply.js +65 -0
- package/dist/channels/wechat/channel-message.js +24 -0
- package/dist/channels/wechat/index.js +5 -0
- package/dist/channels/wechat/wechat-channel-port.js +67 -0
- package/dist/channels/wechat/wechat-forwarding.js +79 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +72 -0
- package/dist/core/bridge-control.js +32 -0
- package/dist/core/bridge-event-forwarder.js +50 -0
- package/dist/core/bridge-message-router.js +32 -0
- package/dist/core/channel-types.js +1 -0
- package/dist/core/text-utils.js +63 -0
- package/dist/daemon/wechat-daemon.js +214 -149
- package/dist/i18n/messages-en.js +2 -2
- package/dist/i18n/messages-zh.js +2 -2
- package/package.json +2 -2
|
@@ -8,17 +8,21 @@ import { resolveDefaultAdapterCommand, } from "../bridge/bridge-adapters.js";
|
|
|
8
8
|
import { t } from "../i18n/index.js";
|
|
9
9
|
import { delay, getSharedSessionIdFromAdapterState, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
|
|
10
10
|
import { BridgeController } from "../bridge/bridge-controller.js";
|
|
11
|
-
import { forwardWechatFinalReply } from "../bridge/bridge-final-reply.js";
|
|
12
11
|
import { readBridgeLockFile, } from "../bridge/bridge-state.js";
|
|
13
12
|
import { getProcessRecordByPid, isWechatDaemonCommandLine, killProcessTreeSync, listWechatDaemonProcesses, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "../bridge/bridge-process-reaper.js";
|
|
14
13
|
import { buildOneTimeCode, buildWechatInboundPrompt, formatApprovalMessage, formatDuration, formatMirroredUserInputMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatSessionSwitchMessage, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "../bridge/bridge-utils.js";
|
|
15
14
|
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "../bridge/bridge-session-resume.js";
|
|
16
|
-
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../
|
|
15
|
+
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
|
|
17
16
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
18
17
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
19
18
|
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
|
|
20
19
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
|
|
20
|
+
import { getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
21
21
|
import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
|
|
22
|
+
import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
|
|
23
|
+
import { routeBridgeMessage } from "../core/bridge-message-router.js";
|
|
24
|
+
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
25
|
+
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
22
26
|
import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
23
27
|
import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
|
|
24
28
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
@@ -493,6 +497,7 @@ class WechatDaemon {
|
|
|
493
497
|
textSendChain = Promise.resolve();
|
|
494
498
|
attachmentSendChain = Promise.resolve();
|
|
495
499
|
pendingWechatForwardTasks = new Set();
|
|
500
|
+
pendingWechatMessages;
|
|
496
501
|
shutdownPromise = null;
|
|
497
502
|
ipcServer = null;
|
|
498
503
|
endpointToken = "";
|
|
@@ -501,6 +506,7 @@ class WechatDaemon {
|
|
|
501
506
|
this.profile = params.profile;
|
|
502
507
|
this.authorizedUserId = params.authorizedUserId;
|
|
503
508
|
this.transport = params.transport;
|
|
509
|
+
this.pendingWechatMessages = new PendingWechatMessageStore(getPendingWechatMessagesFile(this.cwd));
|
|
504
510
|
}
|
|
505
511
|
async startIpcServer() {
|
|
506
512
|
this.endpointToken = buildDaemonToken();
|
|
@@ -624,6 +630,9 @@ class WechatDaemon {
|
|
|
624
630
|
appendDaemonLog(`poll_recovered: failures=${consecutivePollFailures}`);
|
|
625
631
|
consecutivePollFailures = 0;
|
|
626
632
|
}
|
|
633
|
+
if (pollResult.messages.length > 0 && this.pendingWechatMessages.list().length > 0) {
|
|
634
|
+
await this.flushPendingWechatMessages();
|
|
635
|
+
}
|
|
627
636
|
if (pollResult.ignoredBacklogCount > 0) {
|
|
628
637
|
appendDaemonLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
|
|
629
638
|
if (!this.backlogNoticeSent) {
|
|
@@ -899,6 +908,8 @@ class WechatDaemon {
|
|
|
899
908
|
}),
|
|
900
909
|
activeTask: null,
|
|
901
910
|
lastOutputAt: 0,
|
|
911
|
+
lastFinalReplyAtMs: 0,
|
|
912
|
+
eventForwardChain: Promise.resolve(),
|
|
902
913
|
};
|
|
903
914
|
runtime.setEventSink((event) => {
|
|
904
915
|
this.handleSlotEvent(slot, event);
|
|
@@ -925,162 +936,163 @@ class WechatDaemon {
|
|
|
925
936
|
}
|
|
926
937
|
slot.controller.syncLocalClientEndpoint();
|
|
927
938
|
}
|
|
939
|
+
createWechatChannelPort(adapter) {
|
|
940
|
+
return new WechatChannelPort({
|
|
941
|
+
sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
|
|
942
|
+
sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(filePath, { recipientId })),
|
|
943
|
+
sendFile: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, { recipientId })),
|
|
944
|
+
sendVoice: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(filePath, recipientId)),
|
|
945
|
+
sendVideo: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(filePath, { recipientId })),
|
|
946
|
+
prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
|
|
947
|
+
onEmptyVisibleReply: (currentAdapter, rawText) => {
|
|
948
|
+
appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
|
|
949
|
+
},
|
|
950
|
+
onTextSent: (currentAdapter, text) => {
|
|
951
|
+
appendDaemonLog(`final_reply_sent: adapter=${currentAdapter ?? adapter} chars=${Array.from(text).length}`);
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
}
|
|
928
955
|
handleSlotEvent(slot, event) {
|
|
929
956
|
slot.controller.syncLocalClientEndpoint();
|
|
930
957
|
const adapterState = slot.runtime.getState();
|
|
958
|
+
const channelPort = this.createWechatChannelPort(slot.adapter);
|
|
931
959
|
if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
|
|
932
960
|
slot.pendingConfirmations = [];
|
|
933
961
|
}
|
|
934
962
|
if (slot.pendingUserInput && !adapterState.pendingUserInput) {
|
|
935
963
|
slot.pendingUserInput = null;
|
|
936
964
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
965
|
+
slot.eventForwardChain = slot.eventForwardChain
|
|
966
|
+
.then(() => forwardBridgeEvent(event, {
|
|
967
|
+
stdout: (next) => {
|
|
940
968
|
slot.lastOutputAt = Date.now();
|
|
941
|
-
if (shouldForwardBridgeEventToWechat(slot.adapter,
|
|
942
|
-
slot.outputBatcher.push(
|
|
969
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
970
|
+
slot.outputBatcher.push(next.text);
|
|
943
971
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
972
|
+
},
|
|
973
|
+
stderr: (next) => {
|
|
974
|
+
slot.lastOutputAt = Date.now();
|
|
975
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
976
|
+
slot.outputBatcher.push(next.text);
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
finalReply: (next) => {
|
|
980
|
+
slot.lastFinalReplyAtMs = Date.now();
|
|
981
|
+
appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
|
|
947
982
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
948
|
-
await
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
},
|
|
954
|
-
sender: {
|
|
955
|
-
sendText: async (text) => {
|
|
956
|
-
const sent = await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, text), "final_reply");
|
|
957
|
-
if (sent) {
|
|
958
|
-
appendDaemonLog(`final_reply_sent: adapter=${slot.adapter} chars=${Array.from(text).length}`);
|
|
959
|
-
}
|
|
960
|
-
return sent;
|
|
961
|
-
},
|
|
962
|
-
sendImage: (imagePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(imagePath, {
|
|
963
|
-
recipientId: this.authorizedUserId,
|
|
964
|
-
})),
|
|
965
|
-
sendFile: (filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, {
|
|
966
|
-
recipientId: this.authorizedUserId,
|
|
967
|
-
})),
|
|
968
|
-
sendVoice: (voicePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(voicePath, this.authorizedUserId)),
|
|
969
|
-
sendVideo: (videoPath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(videoPath, {
|
|
970
|
-
recipientId: this.authorizedUserId,
|
|
971
|
-
})),
|
|
983
|
+
await channelPort.send({
|
|
984
|
+
target: {
|
|
985
|
+
channelId: "wechat",
|
|
986
|
+
conversationId: this.authorizedUserId,
|
|
987
|
+
recipientId: this.authorizedUserId,
|
|
972
988
|
},
|
|
989
|
+
kind: "final_reply",
|
|
990
|
+
text: next.text,
|
|
991
|
+
adapter: slot.adapter,
|
|
973
992
|
});
|
|
974
993
|
}));
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if (
|
|
978
|
-
log(`${slot.adapter} ${
|
|
979
|
-
appendDaemonLog(`${slot.adapter}_${
|
|
994
|
+
},
|
|
995
|
+
status: (next) => {
|
|
996
|
+
if (next.message) {
|
|
997
|
+
log(`${slot.adapter} ${next.status}: ${next.message}`);
|
|
998
|
+
appendDaemonLog(`${slot.adapter}_${next.status}: ${next.message}`);
|
|
980
999
|
}
|
|
981
|
-
|
|
982
|
-
|
|
1000
|
+
},
|
|
1001
|
+
notice: (next) => {
|
|
983
1002
|
slot.lastOutputAt = Date.now();
|
|
984
|
-
appendDaemonLog(`${slot.adapter}_${
|
|
985
|
-
if (shouldForwardBridgeEventToWechat(slot.adapter,
|
|
1003
|
+
appendDaemonLog(`${slot.adapter}_${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
1004
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
|
|
986
1005
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
987
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter,
|
|
1006
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice");
|
|
988
1007
|
}));
|
|
989
1008
|
}
|
|
990
|
-
|
|
991
|
-
|
|
1009
|
+
},
|
|
1010
|
+
approvalRequired: (next) => {
|
|
992
1011
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
993
|
-
const pending = toPendingApproval(
|
|
1012
|
+
const pending = toPendingApproval(next);
|
|
994
1013
|
slot.pendingConfirmations.push(pending);
|
|
995
1014
|
appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
|
|
996
1015
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
|
|
997
1016
|
}));
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
// send (which retries with backoff). Otherwise a fast user reply can
|
|
1002
|
-
// race past the still-unwritten mirror and be dispatched as plain
|
|
1003
|
-
// input into a CLI that is waiting for a structured form answer.
|
|
1004
|
-
const pending = toPendingUserInput(event.request);
|
|
1017
|
+
},
|
|
1018
|
+
userInputRequired: (next) => {
|
|
1019
|
+
const pending = toPendingUserInput(next.request);
|
|
1005
1020
|
slot.pendingUserInput = pending;
|
|
1006
1021
|
appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
|
|
1007
1022
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1008
1023
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
|
|
1009
1024
|
}));
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (shouldForwardBridgeEventToWechat(slot.adapter, event.type, { text: event.text })) {
|
|
1025
|
+
},
|
|
1026
|
+
mirroredUserInput: (next) => {
|
|
1027
|
+
appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
|
|
1028
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
|
|
1015
1029
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1016
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter,
|
|
1030
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input");
|
|
1017
1031
|
}));
|
|
1018
1032
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (
|
|
1033
|
+
},
|
|
1034
|
+
sessionSwitched: (next) => {
|
|
1035
|
+
if (next.source === "local")
|
|
1022
1036
|
slot.resumeCoordinator.clear();
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
1026
|
-
shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
|
|
1037
|
+
appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
1038
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1027
1039
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1028
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
|
|
1029
|
-
adapter: slot.adapter,
|
|
1030
|
-
sessionId: event.sessionId,
|
|
1031
|
-
source: event.source,
|
|
1032
|
-
reason: event.reason,
|
|
1033
|
-
})), "session_switched");
|
|
1040
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched");
|
|
1034
1041
|
}));
|
|
1035
1042
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (
|
|
1043
|
+
},
|
|
1044
|
+
threadSwitched: (next) => {
|
|
1045
|
+
if (next.source === "local")
|
|
1039
1046
|
slot.resumeCoordinator.clear();
|
|
1047
|
+
appendDaemonLog(`thread_switched: adapter=${slot.adapter} thread=${next.threadId} source=${next.source} reason=${next.reason}`);
|
|
1048
|
+
if (shouldSuppressCodexLocalThreadNotice({
|
|
1049
|
+
adapter: slot.adapter,
|
|
1050
|
+
source: next.source,
|
|
1051
|
+
activeTurnOrigin: slot.runtime.getState().activeTurnOrigin,
|
|
1052
|
+
lastFinalReplyAtMs: slot.lastFinalReplyAtMs,
|
|
1053
|
+
})) {
|
|
1054
|
+
return;
|
|
1040
1055
|
}
|
|
1041
|
-
|
|
1042
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
1043
|
-
shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
|
|
1056
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1044
1057
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1045
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
|
|
1046
|
-
adapter: slot.adapter,
|
|
1047
|
-
sessionId: event.threadId,
|
|
1048
|
-
source: event.source,
|
|
1049
|
-
reason: event.reason,
|
|
1050
|
-
})), "thread_switched");
|
|
1058
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched");
|
|
1051
1059
|
}));
|
|
1052
1060
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1061
|
+
},
|
|
1062
|
+
taskComplete: () => {
|
|
1055
1063
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
|
|
1056
1064
|
slot.pendingConfirmations = [];
|
|
1057
1065
|
slot.pendingUserInput = null;
|
|
1058
1066
|
slot.activeTask = null;
|
|
1059
1067
|
}));
|
|
1060
|
-
|
|
1061
|
-
|
|
1068
|
+
},
|
|
1069
|
+
taskFailed: (next) => {
|
|
1062
1070
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1063
1071
|
slot.pendingConfirmations = [];
|
|
1064
1072
|
slot.pendingUserInput = null;
|
|
1065
1073
|
slot.activeTask = null;
|
|
1066
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter,
|
|
1074
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed");
|
|
1067
1075
|
}));
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
logError(`${slot.adapter}: ${
|
|
1071
|
-
appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${
|
|
1076
|
+
},
|
|
1077
|
+
fatalError: (next) => {
|
|
1078
|
+
logError(`${slot.adapter}: ${next.message}`);
|
|
1079
|
+
appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${next.message}`);
|
|
1072
1080
|
slot.pendingConfirmations = [];
|
|
1073
1081
|
slot.pendingUserInput = null;
|
|
1074
1082
|
slot.activeTask = null;
|
|
1075
1083
|
this.disposeDeadSlot(slot);
|
|
1076
1084
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1077
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(
|
|
1085
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
|
|
1078
1086
|
}));
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1087
|
+
},
|
|
1088
|
+
shutdownRequested: (next) => {
|
|
1089
|
+
appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${next.reason}`);
|
|
1090
|
+
},
|
|
1091
|
+
}))
|
|
1092
|
+
.catch((error) => {
|
|
1093
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1094
|
+
appendDaemonLog(`event_forward_failed: adapter=${slot.adapter} message=${message}`);
|
|
1095
|
+
});
|
|
1084
1096
|
}
|
|
1085
1097
|
async handleInboundMessage(message) {
|
|
1086
1098
|
if (message.senderId !== this.authorizedUserId) {
|
|
@@ -1151,21 +1163,47 @@ class WechatDaemon {
|
|
|
1151
1163
|
await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
|
|
1152
1164
|
return;
|
|
1153
1165
|
}
|
|
1166
|
+
const routeCurrentMessage = async (currentSlot, command) => {
|
|
1167
|
+
await routeBridgeMessage({
|
|
1168
|
+
message: toChannelInboundMessage(message),
|
|
1169
|
+
authorized: true,
|
|
1170
|
+
command,
|
|
1171
|
+
adapterState: currentSlot.runtime.getState(),
|
|
1172
|
+
hasPendingApproval: currentSlot.pendingConfirmations.length > 0,
|
|
1173
|
+
hasPendingUserInput: Boolean(currentSlot.pendingUserInput),
|
|
1174
|
+
onUnauthorized: async () => undefined,
|
|
1175
|
+
handleCommand: async (nextCommand) => {
|
|
1176
|
+
await this.handleSystemCommand(message, currentSlot, nextCommand);
|
|
1177
|
+
return true;
|
|
1178
|
+
},
|
|
1179
|
+
remindPendingApproval: async () => {
|
|
1180
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, formatPendingApprovalReminder(currentSlot.pendingConfirmations[0], currentSlot.runtime.getState())));
|
|
1181
|
+
},
|
|
1182
|
+
remindPendingUserInput: async () => {
|
|
1183
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, currentSlot.pendingUserInput
|
|
1184
|
+
? formatPendingUserInputReminder(currentSlot.pendingUserInput)
|
|
1185
|
+
: `${currentSlot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
|
|
1186
|
+
},
|
|
1187
|
+
remindBusy: async () => {
|
|
1188
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, `${currentSlot.adapter} is still working. Wait for the current reply or use /stop.`));
|
|
1189
|
+
},
|
|
1190
|
+
defer: async () => undefined,
|
|
1191
|
+
dispatch: async () => {
|
|
1192
|
+
await this.dispatchInboundWechatText(message, currentSlot);
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
};
|
|
1154
1196
|
const command = parseWechatControlCommand(message.text, {
|
|
1155
1197
|
adapter: slot.adapter,
|
|
1156
1198
|
hasPendingConfirmation: slot.pendingConfirmations.length > 0,
|
|
1157
1199
|
hasPendingUserInput: Boolean(slot.pendingUserInput),
|
|
1158
1200
|
});
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
await
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
if (slot.pendingUserInput) {
|
|
1168
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingUserInputReminder(slot.pendingUserInput)));
|
|
1201
|
+
const slotState = slot.runtime.getState();
|
|
1202
|
+
if (command ||
|
|
1203
|
+
slot.pendingConfirmations.length > 0 ||
|
|
1204
|
+
slot.pendingUserInput ||
|
|
1205
|
+
slotState.status === "awaiting_input") {
|
|
1206
|
+
await routeCurrentMessage(slot, command);
|
|
1169
1207
|
return;
|
|
1170
1208
|
}
|
|
1171
1209
|
const visibleResult = await this.ensureSlot(slot.adapter, {
|
|
@@ -1177,21 +1215,7 @@ class WechatDaemon {
|
|
|
1177
1215
|
return;
|
|
1178
1216
|
}
|
|
1179
1217
|
slot = this.getActiveSlot() ?? slot;
|
|
1180
|
-
|
|
1181
|
-
if (adapterState.status === "awaiting_input") {
|
|
1182
|
-
// Belt-and-braces with the pendingUserInput mirror above: the adapter
|
|
1183
|
-
// state is authoritative even when the mirror was cleared or the WeChat
|
|
1184
|
-
// notification is still queued.
|
|
1185
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, slot.pendingUserInput
|
|
1186
|
-
? formatPendingUserInputReminder(slot.pendingUserInput)
|
|
1187
|
-
: `${slot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
if (adapterState.status === "busy" || adapterState.status === "awaiting_approval") {
|
|
1191
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
await this.dispatchInboundWechatText(message, slot);
|
|
1218
|
+
await routeCurrentMessage(slot, null);
|
|
1195
1219
|
}
|
|
1196
1220
|
async handleEmojiBindingsCommand(senderId, cmd) {
|
|
1197
1221
|
switch (cmd.type) {
|
|
@@ -1396,39 +1420,80 @@ class WechatDaemon {
|
|
|
1396
1420
|
this.attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
1397
1421
|
return run;
|
|
1398
1422
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1423
|
+
async sendWechatMessageNow(senderId, text, context = "message") {
|
|
1424
|
+
const startedAtMs = Date.now();
|
|
1425
|
+
appendDaemonLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
1426
|
+
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
1427
|
+
try {
|
|
1428
|
+
await this.transport.sendText(senderId, text);
|
|
1429
|
+
appendDaemonLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
1430
|
+
return { status: "sent" };
|
|
1431
|
+
}
|
|
1432
|
+
catch (error) {
|
|
1433
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1434
|
+
appendDaemonLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
1405
1435
|
}
|
|
1406
|
-
|
|
1436
|
+
if (isWechatContextUnavailableError(error)) {
|
|
1407
1437
|
if (isWechatContextTokenStaleError(error)) {
|
|
1408
1438
|
this.transport.clearCachedContextToken(senderId);
|
|
1409
|
-
|
|
1439
|
+
}
|
|
1440
|
+
appendDaemonLog(isWechatContextTokenStaleError(error)
|
|
1441
|
+
? formatWechatContextTokenStaleLogEntry({
|
|
1442
|
+
context,
|
|
1443
|
+
recipientId: senderId,
|
|
1444
|
+
error,
|
|
1445
|
+
})
|
|
1446
|
+
: formatWechatSendFailureLogEntry({
|
|
1410
1447
|
context,
|
|
1411
1448
|
recipientId: senderId,
|
|
1412
1449
|
error,
|
|
1413
1450
|
}));
|
|
1414
|
-
|
|
1415
|
-
}
|
|
1416
|
-
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(error)) {
|
|
1417
|
-
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
1418
|
-
appendDaemonLog(`wechat_send_retry: context=${context} recipient=${senderId} attempt=${attempt} delay_ms=${delayMs} error=${truncatePreview(describeWechatTransportError(error), 400)}`);
|
|
1419
|
-
await delay(delayMs);
|
|
1420
|
-
continue;
|
|
1421
|
-
}
|
|
1422
|
-
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
|
|
1423
|
-
appendDaemonLog(formatWechatSendFailureLogEntry({
|
|
1424
|
-
context,
|
|
1425
|
-
recipientId: senderId,
|
|
1426
|
-
error,
|
|
1427
|
-
}));
|
|
1428
|
-
return false;
|
|
1451
|
+
return { status: "context_unavailable", error };
|
|
1429
1452
|
}
|
|
1453
|
+
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(error)) {
|
|
1454
|
+
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
1455
|
+
appendDaemonLog(`wechat_send_retry: context=${context} recipient=${senderId} attempt=${attempt} delay_ms=${delayMs} error=${truncatePreview(describeWechatTransportError(error), 400)}`);
|
|
1456
|
+
await delay(delayMs);
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
|
|
1460
|
+
appendDaemonLog(formatWechatSendFailureLogEntry({
|
|
1461
|
+
context,
|
|
1462
|
+
recipientId: senderId,
|
|
1463
|
+
error,
|
|
1464
|
+
}));
|
|
1465
|
+
return { status: "failed" };
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return { status: "failed" };
|
|
1469
|
+
}
|
|
1470
|
+
queueWechatMessage(senderId, text, context = "message") {
|
|
1471
|
+
return this.queueWechatTextAction(async () => {
|
|
1472
|
+
const result = await this.sendWechatMessageNow(senderId, text, context);
|
|
1473
|
+
if (result.status === "context_unavailable") {
|
|
1474
|
+
const pending = this.pendingWechatMessages.enqueue(senderId, text, context);
|
|
1475
|
+
if (pending) {
|
|
1476
|
+
appendDaemonLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${this.pendingWechatMessages.list().length}`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return result.status === "sent";
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
flushPendingWechatMessages() {
|
|
1483
|
+
return this.queueWechatTextAction(async () => {
|
|
1484
|
+
for (const pending of this.pendingWechatMessages.list()) {
|
|
1485
|
+
const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
|
|
1486
|
+
if (result.status === "sent") {
|
|
1487
|
+
this.pendingWechatMessages.remove(pending.id);
|
|
1488
|
+
appendDaemonLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (result.status === "context_unavailable") {
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
appendDaemonLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
1495
|
+
break;
|
|
1430
1496
|
}
|
|
1431
|
-
return false;
|
|
1432
1497
|
});
|
|
1433
1498
|
}
|
|
1434
1499
|
trackWechatForwardTask(task) {
|
package/dist/i18n/messages-en.js
CHANGED
|
@@ -25,7 +25,7 @@ export const messages = {
|
|
|
25
25
|
// === Hook health check ===
|
|
26
26
|
"hook.healthCheck.warning": "[Warning] No hook events received from Claude after 15s.\nThe hook system may not be working — Claude output will not reach WeChat.",
|
|
27
27
|
"hook.healthCheck.logHint": "\nCheck: {logPath}",
|
|
28
|
-
"hook.healthCheck.fixes": "\nCommon fixes:\n- Ensure Node.js >= 22.
|
|
28
|
+
"hook.healthCheck.fixes": "\nCommon fixes:\n- Ensure Node.js >= 22.13.0: node --version\n- Reinstall: npm install -g cli-wechat-bridge@latest\n- Check firewall: allow localhost TCP connections",
|
|
29
29
|
// === PTY fallback ===
|
|
30
30
|
"pty.fallback.warning": "[Warning] PTY unavailable — using fallback mode (TERM=dumb).\nCore features (messaging, approvals, session management) work normally; terminal rendering (colors, progress bars) may be degraded.\nFix: Linux install build-essential python3 | Windows run npm rebuild node-pty | macOS run xcode-select --install\nRun --doctor for detailed environment info.\n",
|
|
31
31
|
// === Spawn diagnostic ===
|
|
@@ -36,7 +36,7 @@ export const messages = {
|
|
|
36
36
|
"spawn.diagnostic.linuxBuildTools": "- Linux requires build tools for node-pty: sudo apt install build-essential python3\n- Then reinstall: npm install -g cli-wechat-bridge@latest",
|
|
37
37
|
"spawn.diagnostic.notFound": "- The command \"{target}\" was not found on PATH.\n- Verify it is installed and accessible from your terminal.",
|
|
38
38
|
"spawn.diagnostic.generic": "- Reinstall: npm install -g cli-wechat-bridge@latest",
|
|
39
|
-
"spawn.diagnostic.nodeVersion": "- Ensure Node.js >= 22.
|
|
39
|
+
"spawn.diagnostic.nodeVersion": "- Ensure Node.js >= 22.13.0: node --version",
|
|
40
40
|
"spawn.diagnostic.winFull": "- Ensure Windows 10 build 18309+ (run 'winver' to check)\n- Install Visual C++ Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe\n- Run: npm rebuild node-pty\n- Try running as Administrator",
|
|
41
41
|
// === Emoji bindings ===
|
|
42
42
|
"binding.usage": "Invalid format.\nUsage: /bind [emoji] command\nExample: /bind 🚀 deploy --prod",
|
package/dist/i18n/messages-zh.js
CHANGED
|
@@ -25,7 +25,7 @@ export const messages = {
|
|
|
25
25
|
// === Hook 健康检查 ===
|
|
26
26
|
"hook.healthCheck.warning": "[警告] 15 秒内未收到 Claude hook 事件。\nhook 系统可能未正常工作 — Claude 的输出将无法到达微信。",
|
|
27
27
|
"hook.healthCheck.logHint": "\n查看日志: {logPath}",
|
|
28
|
-
"hook.healthCheck.fixes": "\n常见修复方法:\n- 确保 Node.js >= 22.
|
|
28
|
+
"hook.healthCheck.fixes": "\n常见修复方法:\n- 确保 Node.js >= 22.13.0: node --version\n- 重新安装: npm install -g cli-wechat-bridge@latest\n- 检查防火墙: 允许 localhost TCP 连接",
|
|
29
29
|
// === PTY 回退 ===
|
|
30
30
|
"pty.fallback.warning": "[警告] PTY 不可用 — 已切换到回退模式(TERM=dumb)。\n核心功能(消息收发、审批、会话管理)正常工作;终端渲染(颜色、进度条)可能受限。\n修复方法: Linux 安装 build-essential python3 | Windows 运行 npm rebuild node-pty | macOS 运行 xcode-select --install\n运行 --doctor 查看详细环境信息。\n",
|
|
31
31
|
// === 启动诊断 ===
|
|
@@ -36,7 +36,7 @@ export const messages = {
|
|
|
36
36
|
"spawn.diagnostic.linuxBuildTools": "- Linux 需要编译工具来构建 node-pty: sudo apt install build-essential python3\n- 然后重新安装: npm install -g cli-wechat-bridge@latest",
|
|
37
37
|
"spawn.diagnostic.notFound": "- 命令 \"{target}\" 未在 PATH 中找到。\n- 请确认已安装并可从终端访问。",
|
|
38
38
|
"spawn.diagnostic.generic": "- 重新安装: npm install -g cli-wechat-bridge@latest",
|
|
39
|
-
"spawn.diagnostic.nodeVersion": "- 确保 Node.js >= 22.
|
|
39
|
+
"spawn.diagnostic.nodeVersion": "- 确保 Node.js >= 22.13.0: node --version",
|
|
40
40
|
"spawn.diagnostic.winFull": "- 确保 Windows 10 build 18309+(运行 winver 查看)\n- 安装 Visual C++ Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe\n- 运行: npm rebuild node-pty\n- 尝试以管理员身份运行",
|
|
41
41
|
// === Emoji 绑定 ===
|
|
42
42
|
"binding.usage": "格式错误。\n用法: /bind [表情] 命令\n示例: /bind 🚀 deploy --prod",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlinearity/cli-wechat-bridge",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and Pi over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
63
|
"bun": ">=1.0.0",
|
|
64
|
-
"node": ">=
|
|
64
|
+
"node": ">=22.13.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"postinstall": "node scripts/ensure-node-pty-permissions.mjs"
|