@unlinearity/cli-wechat-bridge 1.1.4 → 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 +153 -131
- package/bin/_run-entry.mjs +20 -17
- package/dist/bridge/bridge-adapters.claude.js +429 -11
- package/dist/bridge/bridge-adapters.codex.js +441 -22
- package/dist/bridge/bridge-adapters.core.js +2 -1
- package/dist/bridge/bridge-adapters.opencode.js +392 -53
- package/dist/bridge/bridge-adapters.pi.js +33 -7
- package/dist/bridge/bridge-adapters.shared.js +2 -1
- package/dist/bridge/bridge-controller.js +3 -0
- package/dist/bridge/bridge-final-reply.js +2 -65
- package/dist/bridge/bridge-session-resume.js +151 -0
- package/dist/bridge/bridge-state.js +1 -1
- package/dist/bridge/bridge-utils.js +30 -101
- package/dist/bridge/claude-hooks.js +1 -0
- package/dist/bridge/wechat-bridge.js +256 -211
- 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/companion/codex-remote-client.js +291 -32
- package/dist/companion/codex-visible-client-link.js +122 -0
- package/dist/companion/local-companion-link.js +26 -0
- package/dist/companion/local-companion-start.js +0 -1
- package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
- package/dist/companion/pi-tui-bridge-extension.js +42 -3
- 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 +242 -159
- package/dist/i18n/messages-en.js +2 -2
- package/dist/i18n/messages-zh.js +2 -2
- package/package.json +2 -6
- package/bin/wechat-claude-start.mjs +0 -10
- package/bin/wechat-codex-start.mjs +0 -10
- package/bin/wechat-opencode-start.mjs +0 -10
- package/bin/wechat-pi-start.mjs +0 -10
|
@@ -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
|
-
import {
|
|
14
|
+
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "../bridge/bridge-session-resume.js";
|
|
15
|
+
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
|
|
16
16
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
17
17
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
18
18
|
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
|
|
19
|
-
import { formatResumeSessionList } from "../bridge/bridge-utils.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) {
|
|
@@ -893,8 +902,14 @@ class WechatDaemon {
|
|
|
893
902
|
}),
|
|
894
903
|
pendingConfirmations: [],
|
|
895
904
|
pendingUserInput: null,
|
|
905
|
+
resumeCoordinator: new ResumeSessionCoordinator({
|
|
906
|
+
adapter,
|
|
907
|
+
runtime,
|
|
908
|
+
}),
|
|
896
909
|
activeTask: null,
|
|
897
910
|
lastOutputAt: 0,
|
|
911
|
+
lastFinalReplyAtMs: 0,
|
|
912
|
+
eventForwardChain: Promise.resolve(),
|
|
898
913
|
};
|
|
899
914
|
runtime.setEventSink((event) => {
|
|
900
915
|
this.handleSlotEvent(slot, event);
|
|
@@ -921,154 +936,163 @@ class WechatDaemon {
|
|
|
921
936
|
}
|
|
922
937
|
slot.controller.syncLocalClientEndpoint();
|
|
923
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
|
+
}
|
|
924
955
|
handleSlotEvent(slot, event) {
|
|
925
956
|
slot.controller.syncLocalClientEndpoint();
|
|
926
957
|
const adapterState = slot.runtime.getState();
|
|
958
|
+
const channelPort = this.createWechatChannelPort(slot.adapter);
|
|
927
959
|
if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
|
|
928
960
|
slot.pendingConfirmations = [];
|
|
929
961
|
}
|
|
930
962
|
if (slot.pendingUserInput && !adapterState.pendingUserInput) {
|
|
931
963
|
slot.pendingUserInput = null;
|
|
932
964
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
965
|
+
slot.eventForwardChain = slot.eventForwardChain
|
|
966
|
+
.then(() => forwardBridgeEvent(event, {
|
|
967
|
+
stdout: (next) => {
|
|
936
968
|
slot.lastOutputAt = Date.now();
|
|
937
|
-
if (shouldForwardBridgeEventToWechat(slot.adapter,
|
|
938
|
-
slot.outputBatcher.push(
|
|
969
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
970
|
+
slot.outputBatcher.push(next.text);
|
|
939
971
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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)}`);
|
|
943
982
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
944
|
-
await
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
},
|
|
950
|
-
sender: {
|
|
951
|
-
sendText: async (text) => {
|
|
952
|
-
const sent = await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, text), "final_reply");
|
|
953
|
-
if (sent) {
|
|
954
|
-
appendDaemonLog(`final_reply_sent: adapter=${slot.adapter} chars=${Array.from(text).length}`);
|
|
955
|
-
}
|
|
956
|
-
return sent;
|
|
957
|
-
},
|
|
958
|
-
sendImage: (imagePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(imagePath, {
|
|
959
|
-
recipientId: this.authorizedUserId,
|
|
960
|
-
})),
|
|
961
|
-
sendFile: (filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, {
|
|
962
|
-
recipientId: this.authorizedUserId,
|
|
963
|
-
})),
|
|
964
|
-
sendVoice: (voicePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(voicePath, this.authorizedUserId)),
|
|
965
|
-
sendVideo: (videoPath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(videoPath, {
|
|
966
|
-
recipientId: this.authorizedUserId,
|
|
967
|
-
})),
|
|
983
|
+
await channelPort.send({
|
|
984
|
+
target: {
|
|
985
|
+
channelId: "wechat",
|
|
986
|
+
conversationId: this.authorizedUserId,
|
|
987
|
+
recipientId: this.authorizedUserId,
|
|
968
988
|
},
|
|
989
|
+
kind: "final_reply",
|
|
990
|
+
text: next.text,
|
|
991
|
+
adapter: slot.adapter,
|
|
969
992
|
});
|
|
970
993
|
}));
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
if (
|
|
974
|
-
log(`${slot.adapter} ${
|
|
975
|
-
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}`);
|
|
976
999
|
}
|
|
977
|
-
|
|
978
|
-
|
|
1000
|
+
},
|
|
1001
|
+
notice: (next) => {
|
|
979
1002
|
slot.lastOutputAt = Date.now();
|
|
980
|
-
appendDaemonLog(`${slot.adapter}_${
|
|
981
|
-
if (shouldForwardBridgeEventToWechat(slot.adapter,
|
|
1003
|
+
appendDaemonLog(`${slot.adapter}_${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
1004
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
|
|
982
1005
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
983
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter,
|
|
1006
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice");
|
|
984
1007
|
}));
|
|
985
1008
|
}
|
|
986
|
-
|
|
987
|
-
|
|
1009
|
+
},
|
|
1010
|
+
approvalRequired: (next) => {
|
|
988
1011
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
989
|
-
const pending = toPendingApproval(
|
|
1012
|
+
const pending = toPendingApproval(next);
|
|
990
1013
|
slot.pendingConfirmations.push(pending);
|
|
991
1014
|
appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
|
|
992
1015
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
|
|
993
1016
|
}));
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
// send (which retries with backoff). Otherwise a fast user reply can
|
|
998
|
-
// race past the still-unwritten mirror and be dispatched as plain
|
|
999
|
-
// input into a CLI that is waiting for a structured form answer.
|
|
1000
|
-
const pending = toPendingUserInput(event.request);
|
|
1017
|
+
},
|
|
1018
|
+
userInputRequired: (next) => {
|
|
1019
|
+
const pending = toPendingUserInput(next.request);
|
|
1001
1020
|
slot.pendingUserInput = pending;
|
|
1002
1021
|
appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
|
|
1003
1022
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1004
1023
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
|
|
1005
1024
|
}));
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
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 })) {
|
|
1011
1029
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1012
|
-
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");
|
|
1013
1031
|
}));
|
|
1014
1032
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1033
|
+
},
|
|
1034
|
+
sessionSwitched: (next) => {
|
|
1035
|
+
if (next.source === "local")
|
|
1036
|
+
slot.resumeCoordinator.clear();
|
|
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)) {
|
|
1019
1039
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1020
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
|
|
1021
|
-
adapter: slot.adapter,
|
|
1022
|
-
sessionId: event.sessionId,
|
|
1023
|
-
source: event.source,
|
|
1024
|
-
reason: event.reason,
|
|
1025
|
-
})), "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");
|
|
1026
1041
|
}));
|
|
1027
1042
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1043
|
+
},
|
|
1044
|
+
threadSwitched: (next) => {
|
|
1045
|
+
if (next.source === "local")
|
|
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;
|
|
1055
|
+
}
|
|
1056
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1032
1057
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1033
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
|
|
1034
|
-
adapter: slot.adapter,
|
|
1035
|
-
sessionId: event.threadId,
|
|
1036
|
-
source: event.source,
|
|
1037
|
-
reason: event.reason,
|
|
1038
|
-
})), "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");
|
|
1039
1059
|
}));
|
|
1040
1060
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1061
|
+
},
|
|
1062
|
+
taskComplete: () => {
|
|
1043
1063
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
|
|
1044
1064
|
slot.pendingConfirmations = [];
|
|
1045
1065
|
slot.pendingUserInput = null;
|
|
1046
1066
|
slot.activeTask = null;
|
|
1047
1067
|
}));
|
|
1048
|
-
|
|
1049
|
-
|
|
1068
|
+
},
|
|
1069
|
+
taskFailed: (next) => {
|
|
1050
1070
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1051
1071
|
slot.pendingConfirmations = [];
|
|
1052
1072
|
slot.pendingUserInput = null;
|
|
1053
1073
|
slot.activeTask = null;
|
|
1054
|
-
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");
|
|
1055
1075
|
}));
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
logError(`${slot.adapter}: ${
|
|
1059
|
-
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}`);
|
|
1060
1080
|
slot.pendingConfirmations = [];
|
|
1061
1081
|
slot.pendingUserInput = null;
|
|
1062
1082
|
slot.activeTask = null;
|
|
1063
1083
|
this.disposeDeadSlot(slot);
|
|
1064
1084
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1065
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(
|
|
1085
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
|
|
1066
1086
|
}));
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${
|
|
1070
|
-
|
|
1071
|
-
}
|
|
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
|
+
});
|
|
1072
1096
|
}
|
|
1073
1097
|
async handleInboundMessage(message) {
|
|
1074
1098
|
if (message.senderId !== this.authorizedUserId) {
|
|
@@ -1139,21 +1163,47 @@ class WechatDaemon {
|
|
|
1139
1163
|
await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
|
|
1140
1164
|
return;
|
|
1141
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
|
+
};
|
|
1142
1196
|
const command = parseWechatControlCommand(message.text, {
|
|
1143
1197
|
adapter: slot.adapter,
|
|
1144
1198
|
hasPendingConfirmation: slot.pendingConfirmations.length > 0,
|
|
1145
1199
|
hasPendingUserInput: Boolean(slot.pendingUserInput),
|
|
1146
1200
|
});
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
await
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
if (slot.pendingUserInput) {
|
|
1156
|
-
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);
|
|
1157
1207
|
return;
|
|
1158
1208
|
}
|
|
1159
1209
|
const visibleResult = await this.ensureSlot(slot.adapter, {
|
|
@@ -1165,21 +1215,7 @@ class WechatDaemon {
|
|
|
1165
1215
|
return;
|
|
1166
1216
|
}
|
|
1167
1217
|
slot = this.getActiveSlot() ?? slot;
|
|
1168
|
-
|
|
1169
|
-
if (adapterState.status === "awaiting_input") {
|
|
1170
|
-
// Belt-and-braces with the pendingUserInput mirror above: the adapter
|
|
1171
|
-
// state is authoritative even when the mirror was cleared or the WeChat
|
|
1172
|
-
// notification is still queued.
|
|
1173
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, slot.pendingUserInput
|
|
1174
|
-
? formatPendingUserInputReminder(slot.pendingUserInput)
|
|
1175
|
-
: `${slot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
|
|
1176
|
-
return;
|
|
1177
|
-
}
|
|
1178
|
-
if (adapterState.status === "busy" || adapterState.status === "awaiting_approval") {
|
|
1179
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
await this.dispatchInboundWechatText(message, slot);
|
|
1218
|
+
await routeCurrentMessage(slot, null);
|
|
1183
1219
|
}
|
|
1184
1220
|
async handleEmojiBindingsCommand(senderId, cmd) {
|
|
1185
1221
|
switch (cmd.type) {
|
|
@@ -1207,29 +1243,33 @@ class WechatDaemon {
|
|
|
1207
1243
|
await this.queueWechatMessage(message.senderId, formatDaemonStatus(this.getStatus()));
|
|
1208
1244
|
return;
|
|
1209
1245
|
case "resume": {
|
|
1210
|
-
if (activeSlot.adapter
|
|
1246
|
+
if (!isWechatResumeEnabled(activeSlot.adapter)) {
|
|
1211
1247
|
await this.queueWechatMessage(message.senderId, `WeChat /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; WeChat will follow that local session.`);
|
|
1212
1248
|
return;
|
|
1213
1249
|
}
|
|
1214
|
-
if (command.type !== "resume") {
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
1250
|
try {
|
|
1251
|
+
let resumeSlot = activeSlot;
|
|
1252
|
+
if (activeSlot.adapter === "codex" && command.target) {
|
|
1253
|
+
const visible = await this.ensureSlot("codex", {
|
|
1254
|
+
openVisible: true,
|
|
1255
|
+
reuseExistingVisible: true,
|
|
1256
|
+
});
|
|
1257
|
+
if (!visible.activated) {
|
|
1258
|
+
throw new Error(formatDaemonSwitchResultDetail(visible));
|
|
1259
|
+
}
|
|
1260
|
+
resumeSlot = this.slots.get("codex") ?? activeSlot;
|
|
1261
|
+
}
|
|
1218
1262
|
if (command.target) {
|
|
1219
|
-
await
|
|
1220
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, `Resumed OpenCode session ${command.target}.`));
|
|
1263
|
+
await resumeSlot.outputBatcher.flushNow();
|
|
1221
1264
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
adapter: activeSlot.adapter,
|
|
1226
|
-
candidates,
|
|
1227
|
-
currentSessionId: activeSlot.runtime.getState().sharedSessionId,
|
|
1228
|
-
})));
|
|
1265
|
+
const result = await resumeSlot.resumeCoordinator.execute(command.target);
|
|
1266
|
+
if (result.kind === "resumed") {
|
|
1267
|
+
resumeSlot.activeTask = null;
|
|
1229
1268
|
}
|
|
1269
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(resumeSlot.adapter, result.message));
|
|
1230
1270
|
}
|
|
1231
1271
|
catch (error) {
|
|
1232
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter,
|
|
1272
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, error instanceof Error ? error.message : String(error)));
|
|
1233
1273
|
}
|
|
1234
1274
|
return;
|
|
1235
1275
|
}
|
|
@@ -1242,6 +1282,7 @@ class WechatDaemon {
|
|
|
1242
1282
|
activeSlot.outputBatcher.clear();
|
|
1243
1283
|
activeSlot.pendingConfirmations = [];
|
|
1244
1284
|
activeSlot.pendingUserInput = null;
|
|
1285
|
+
activeSlot.resumeCoordinator.clear();
|
|
1245
1286
|
await activeSlot.runtime.createSession();
|
|
1246
1287
|
appendDaemonLog(`new_session: adapter=${activeSlot.adapter}`);
|
|
1247
1288
|
return;
|
|
@@ -1257,6 +1298,7 @@ class WechatDaemon {
|
|
|
1257
1298
|
activeSlot.outputBatcher.clear();
|
|
1258
1299
|
activeSlot.pendingConfirmations = [];
|
|
1259
1300
|
activeSlot.pendingUserInput = null;
|
|
1301
|
+
activeSlot.resumeCoordinator.clear();
|
|
1260
1302
|
await activeSlot.runtime.reset();
|
|
1261
1303
|
appendDaemonLog(`reset: adapter=${activeSlot.adapter}`);
|
|
1262
1304
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "Worker session has been reset."));
|
|
@@ -1378,39 +1420,80 @@ class WechatDaemon {
|
|
|
1378
1420
|
this.attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
1379
1421
|
return run;
|
|
1380
1422
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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}`);
|
|
1387
1435
|
}
|
|
1388
|
-
|
|
1436
|
+
if (isWechatContextUnavailableError(error)) {
|
|
1389
1437
|
if (isWechatContextTokenStaleError(error)) {
|
|
1390
1438
|
this.transport.clearCachedContextToken(senderId);
|
|
1391
|
-
|
|
1439
|
+
}
|
|
1440
|
+
appendDaemonLog(isWechatContextTokenStaleError(error)
|
|
1441
|
+
? formatWechatContextTokenStaleLogEntry({
|
|
1442
|
+
context,
|
|
1443
|
+
recipientId: senderId,
|
|
1444
|
+
error,
|
|
1445
|
+
})
|
|
1446
|
+
: formatWechatSendFailureLogEntry({
|
|
1392
1447
|
context,
|
|
1393
1448
|
recipientId: senderId,
|
|
1394
1449
|
error,
|
|
1395
1450
|
}));
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
}
|
|
1404
|
-
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
|
|
1405
|
-
appendDaemonLog(formatWechatSendFailureLogEntry({
|
|
1406
|
-
context,
|
|
1407
|
-
recipientId: senderId,
|
|
1408
|
-
error,
|
|
1409
|
-
}));
|
|
1410
|
-
return false;
|
|
1451
|
+
return { status: "context_unavailable", error };
|
|
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;
|
|
1411
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;
|
|
1412
1496
|
}
|
|
1413
|
-
return false;
|
|
1414
1497
|
});
|
|
1415
1498
|
}
|
|
1416
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",
|