@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
|
@@ -4,18 +4,21 @@ import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
|
|
|
4
4
|
import { delay } from "./bridge-adapters.shared.js";
|
|
5
5
|
import { t } from "../i18n/index.js";
|
|
6
6
|
import { BridgeController } from "./bridge-controller.js";
|
|
7
|
-
import { forwardWechatFinalReply } from "./bridge-final-reply.js";
|
|
8
7
|
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "./bridge-session-resume.js";
|
|
9
|
-
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "
|
|
8
|
+
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
|
|
10
9
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
11
10
|
import { BridgeStateStore } from "./bridge-state.js";
|
|
12
11
|
import { getProcessRecordByPid, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "./bridge-process-reaper.js";
|
|
13
12
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
13
|
+
import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
|
|
14
|
+
import { routeBridgeMessage } from "../core/bridge-message-router.js";
|
|
15
|
+
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
16
|
+
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
14
17
|
import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, formatThinkingForWechat, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
|
|
15
18
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
|
|
19
|
+
import { getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
16
20
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
17
21
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
18
|
-
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "../daemon/emoji-bindings.js";
|
|
19
22
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
20
23
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
21
24
|
const PARENT_PROCESS_POLL_MS = 5_000;
|
|
@@ -270,6 +273,7 @@ async function main() {
|
|
|
270
273
|
initialResumeConversationId: stateStore.getState().resumeConversationId,
|
|
271
274
|
initialTranscriptPath: stateStore.getState().transcriptPath,
|
|
272
275
|
});
|
|
276
|
+
const pendingWechatMessages = new PendingWechatMessageStore(getPendingWechatMessagesFile(options.cwd));
|
|
273
277
|
const resumeCoordinator = new ResumeSessionCoordinator({
|
|
274
278
|
adapter: options.adapter,
|
|
275
279
|
runtime: adapter,
|
|
@@ -295,48 +299,89 @@ async function main() {
|
|
|
295
299
|
attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
296
300
|
return run;
|
|
297
301
|
};
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
const sendWechatMessageNow = async (senderId, text, context = "message") => {
|
|
303
|
+
const startedAtMs = Date.now();
|
|
304
|
+
stateStore.appendLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
305
|
+
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
306
|
+
try {
|
|
307
|
+
await transport.sendText(senderId, text);
|
|
308
|
+
stateStore.appendLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
309
|
+
return { status: "sent" };
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
313
|
+
stateStore.appendLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
304
314
|
}
|
|
305
|
-
|
|
315
|
+
if (isWechatContextUnavailableError(err)) {
|
|
306
316
|
if (isWechatContextTokenStaleError(err)) {
|
|
307
317
|
transport.clearCachedContextToken(senderId);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
318
|
+
}
|
|
319
|
+
const hint = "WeChat conversation context is stale or unavailable. Ask the WeChat owner to send any message first, then local terminal replies can sync back to WeChat.";
|
|
320
|
+
logError(`Failed to send WeChat ${context}: ${hint}`);
|
|
321
|
+
stateStore.appendLog(isWechatContextTokenStaleError(err)
|
|
322
|
+
? formatWechatContextTokenStaleLogEntry({
|
|
311
323
|
context,
|
|
312
324
|
recipientId: senderId,
|
|
313
325
|
error: err,
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
318
|
-
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
319
|
-
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
320
|
-
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
326
|
+
})
|
|
327
|
+
: formatWechatSendFailureLogEntry({
|
|
321
328
|
context,
|
|
322
329
|
recipientId: senderId,
|
|
323
|
-
attempt,
|
|
324
|
-
delayMs,
|
|
325
330
|
error: err,
|
|
326
331
|
}));
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
return { status: "context_unavailable", error: err };
|
|
333
|
+
}
|
|
334
|
+
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
335
|
+
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
336
|
+
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
337
|
+
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
332
338
|
context,
|
|
333
339
|
recipientId: senderId,
|
|
340
|
+
attempt,
|
|
341
|
+
delayMs,
|
|
334
342
|
error: err,
|
|
335
343
|
}));
|
|
336
|
-
|
|
344
|
+
await delay(delayMs);
|
|
345
|
+
continue;
|
|
337
346
|
}
|
|
347
|
+
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
|
|
348
|
+
stateStore.appendLog(formatWechatSendFailureLogEntry({
|
|
349
|
+
context,
|
|
350
|
+
recipientId: senderId,
|
|
351
|
+
error: err,
|
|
352
|
+
}));
|
|
353
|
+
return { status: "failed" };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return { status: "failed" };
|
|
357
|
+
};
|
|
358
|
+
const queueWechatMessage = (senderId, text, context = "message") => {
|
|
359
|
+
return queueWechatTextAction(async () => {
|
|
360
|
+
const result = await sendWechatMessageNow(senderId, text, context);
|
|
361
|
+
if (result.status === "context_unavailable") {
|
|
362
|
+
const pending = pendingWechatMessages.enqueue(senderId, text, context);
|
|
363
|
+
if (pending) {
|
|
364
|
+
stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return result.status === "sent";
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
const flushPendingWechatMessages = () => {
|
|
371
|
+
return queueWechatTextAction(async () => {
|
|
372
|
+
for (const pending of pendingWechatMessages.list()) {
|
|
373
|
+
const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
|
|
374
|
+
if (result.status === "sent") {
|
|
375
|
+
pendingWechatMessages.remove(pending.id);
|
|
376
|
+
stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (result.status === "context_unavailable") {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
383
|
+
break;
|
|
338
384
|
}
|
|
339
|
-
return false;
|
|
340
385
|
});
|
|
341
386
|
};
|
|
342
387
|
const trackWechatForwardTask = (task) => {
|
|
@@ -583,10 +628,6 @@ async function main() {
|
|
|
583
628
|
else if (options.adapter === "pi") {
|
|
584
629
|
log("For source-mode debugging, open the visible Pi client with: npm run pi:companion");
|
|
585
630
|
}
|
|
586
|
-
loadEmojiBindings();
|
|
587
|
-
// Keep the standalone bridge's WeChat welcome brief: command and emoji
|
|
588
|
-
// reference material belongs to /status and /bindings, which users can
|
|
589
|
-
// request on demand. The daemon sends its own, fuller welcome.
|
|
590
631
|
const welcomeText = t("bridge.welcome", {
|
|
591
632
|
adapter: options.adapter,
|
|
592
633
|
cwd: options.cwd,
|
|
@@ -634,6 +675,9 @@ async function main() {
|
|
|
634
675
|
log(`WeChat long poll recovered after ${recoveredFailures} transient error(s).`);
|
|
635
676
|
stateStore.appendLog(`poll_recovered: failures=${recoveredFailures}`);
|
|
636
677
|
}
|
|
678
|
+
if (pollResult.messages.length > 0 && pendingWechatMessages.list().length > 0) {
|
|
679
|
+
await flushPendingWechatMessages();
|
|
680
|
+
}
|
|
637
681
|
if (pollResult.ignoredBacklogCount > 0) {
|
|
638
682
|
stateStore.incrementIgnoredBacklog(pollResult.ignoredBacklogCount);
|
|
639
683
|
stateStore.appendLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
|
|
@@ -720,6 +764,21 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
720
764
|
}
|
|
721
765
|
function wireAdapterEvents(params) {
|
|
722
766
|
const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
767
|
+
const channelPort = new WechatChannelPort({
|
|
768
|
+
sendText: (recipientId, text, context) => queueWechatMessage(recipientId, text, context),
|
|
769
|
+
sendImage: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
|
|
770
|
+
sendFile: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
|
|
771
|
+
sendVoice: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
|
|
772
|
+
sendVideo: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVideo(filePath, { recipientId })),
|
|
773
|
+
onEmptyVisibleReply: (adapter, rawText) => {
|
|
774
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
|
|
775
|
+
},
|
|
776
|
+
onTextSent: (_adapter, text) => {
|
|
777
|
+
stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
let lastFinalReplyAtMs = 0;
|
|
781
|
+
let eventForwardChain = Promise.resolve();
|
|
723
782
|
adapter.setEventSink((event) => {
|
|
724
783
|
syncSharedSessionState();
|
|
725
784
|
syncLocalClientEndpoint();
|
|
@@ -732,56 +791,52 @@ function wireAdapterEvents(params) {
|
|
|
732
791
|
stateStore.clearPendingUserInput();
|
|
733
792
|
}
|
|
734
793
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
739
|
-
outputBatcher.push(
|
|
794
|
+
eventForwardChain = eventForwardChain
|
|
795
|
+
.then(() => forwardBridgeEvent(event, {
|
|
796
|
+
stdout: (next) => {
|
|
797
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
798
|
+
outputBatcher.push(next.text);
|
|
740
799
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
800
|
+
},
|
|
801
|
+
stderr: (next) => {
|
|
802
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
803
|
+
outputBatcher.push(next.text);
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
finalReply: (next) => {
|
|
807
|
+
lastFinalReplyAtMs = Date.now();
|
|
808
|
+
stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
|
|
744
809
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
745
|
-
await
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
},
|
|
751
|
-
sender: {
|
|
752
|
-
sendText: async (text) => {
|
|
753
|
-
const sent = await queueWechatMessage(authorizedUserId, text, "final_reply");
|
|
754
|
-
if (sent) {
|
|
755
|
-
stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
|
|
756
|
-
}
|
|
757
|
-
return sent;
|
|
758
|
-
},
|
|
759
|
-
sendImage: (imagePath) => queueWechatAttachmentAction(() => transport.sendImage(imagePath, { recipientId: authorizedUserId })),
|
|
760
|
-
sendFile: (filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId: authorizedUserId })),
|
|
761
|
-
sendVoice: (voicePath) => queueWechatAttachmentAction(() => transport.sendVoice(voicePath, authorizedUserId)),
|
|
762
|
-
sendVideo: (videoPath) => queueWechatAttachmentAction(() => transport.sendVideo(videoPath, { recipientId: authorizedUserId })),
|
|
810
|
+
await channelPort.send({
|
|
811
|
+
target: {
|
|
812
|
+
channelId: "wechat",
|
|
813
|
+
conversationId: authorizedUserId,
|
|
814
|
+
recipientId: authorizedUserId,
|
|
763
815
|
},
|
|
816
|
+
kind: "final_reply",
|
|
817
|
+
text: next.text,
|
|
818
|
+
adapter: options.adapter,
|
|
764
819
|
});
|
|
765
820
|
}));
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (
|
|
769
|
-
log(`${
|
|
770
|
-
stateStore.appendLog(`${
|
|
821
|
+
},
|
|
822
|
+
status: (next) => {
|
|
823
|
+
if (next.message) {
|
|
824
|
+
log(`${next.status}: ${next.message}`);
|
|
825
|
+
stateStore.appendLog(`${next.status}: ${next.message}`);
|
|
771
826
|
}
|
|
772
827
|
void maybeDrainDeferredInboundMessages();
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
stateStore.appendLog(`${
|
|
776
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
828
|
+
},
|
|
829
|
+
notice: (next) => {
|
|
830
|
+
stateStore.appendLog(`${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
831
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
777
832
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
778
|
-
await queueWechatMessage(authorizedUserId,
|
|
833
|
+
await queueWechatMessage(authorizedUserId, next.text, "notice");
|
|
779
834
|
}));
|
|
780
835
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (
|
|
784
|
-
const thinkingPreview = formatThinkingForWechat(
|
|
836
|
+
},
|
|
837
|
+
thinking: (next) => {
|
|
838
|
+
if (next.text) {
|
|
839
|
+
const thinkingPreview = formatThinkingForWechat(next.text, 500);
|
|
785
840
|
if (thinkingPreview) {
|
|
786
841
|
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
787
842
|
trackWechatForwardTask((async () => {
|
|
@@ -789,99 +844,98 @@ function wireAdapterEvents(params) {
|
|
|
789
844
|
})());
|
|
790
845
|
}
|
|
791
846
|
}
|
|
792
|
-
|
|
793
|
-
|
|
847
|
+
},
|
|
848
|
+
approvalRequired: (next) => {
|
|
794
849
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
795
|
-
const pending = toPendingApproval(
|
|
850
|
+
const pending = toPendingApproval(next.request);
|
|
796
851
|
stateStore.setPendingConfirmation(pending);
|
|
797
852
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
798
853
|
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
799
854
|
}));
|
|
800
|
-
|
|
801
|
-
|
|
855
|
+
},
|
|
856
|
+
userInputRequired: (next) => {
|
|
802
857
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
803
|
-
const pending = toPendingUserInput(
|
|
858
|
+
const pending = toPendingUserInput(next.request);
|
|
804
859
|
stateStore.setPendingUserInput(pending);
|
|
805
860
|
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
806
861
|
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
|
|
807
862
|
}));
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(
|
|
811
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
863
|
+
},
|
|
864
|
+
mirroredUserInput: (next) => {
|
|
865
|
+
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(next.text)}`);
|
|
866
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
812
867
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
813
|
-
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter,
|
|
868
|
+
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input");
|
|
814
869
|
}));
|
|
815
870
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (
|
|
871
|
+
},
|
|
872
|
+
sessionSwitched: (next) => {
|
|
873
|
+
if (next.source === "local")
|
|
819
874
|
resumeCoordinator.clear();
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
823
|
-
shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
875
|
+
stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
876
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
824
877
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
825
|
-
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
826
|
-
adapter: options.adapter,
|
|
827
|
-
sessionId: event.sessionId,
|
|
828
|
-
source: event.source,
|
|
829
|
-
reason: event.reason,
|
|
830
|
-
}), "session_switched");
|
|
878
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched");
|
|
831
879
|
}));
|
|
832
880
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if (
|
|
881
|
+
},
|
|
882
|
+
threadSwitched: (next) => {
|
|
883
|
+
if (next.source === "local")
|
|
836
884
|
resumeCoordinator.clear();
|
|
885
|
+
stateStore.appendLog(`thread_switched: ${next.threadId} source=${next.source} reason=${next.reason}`);
|
|
886
|
+
if (shouldSuppressCodexLocalThreadNotice({
|
|
887
|
+
adapter: options.adapter,
|
|
888
|
+
source: next.source,
|
|
889
|
+
activeTurnOrigin: adapter.getState().activeTurnOrigin,
|
|
890
|
+
lastFinalReplyAtMs,
|
|
891
|
+
})) {
|
|
892
|
+
return;
|
|
837
893
|
}
|
|
838
|
-
|
|
839
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
840
|
-
shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
894
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
841
895
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
842
|
-
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
843
|
-
adapter: options.adapter,
|
|
844
|
-
sessionId: event.threadId,
|
|
845
|
-
source: event.source,
|
|
846
|
-
reason: event.reason,
|
|
847
|
-
}), "thread_switched");
|
|
896
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched");
|
|
848
897
|
}));
|
|
849
898
|
}
|
|
850
899
|
void maybeDrainDeferredInboundMessages();
|
|
851
|
-
|
|
852
|
-
|
|
900
|
+
},
|
|
901
|
+
taskComplete: () => {
|
|
853
902
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
854
903
|
stateStore.clearPendingConfirmation();
|
|
855
904
|
stateStore.clearPendingUserInput();
|
|
856
905
|
clearActiveTask();
|
|
857
906
|
await maybeDrainDeferredInboundMessages();
|
|
858
907
|
}));
|
|
859
|
-
|
|
860
|
-
|
|
908
|
+
},
|
|
909
|
+
taskFailed: (next) => {
|
|
861
910
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
862
911
|
stateStore.clearPendingConfirmation();
|
|
863
912
|
stateStore.clearPendingUserInput();
|
|
864
913
|
clearActiveTask();
|
|
865
|
-
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter,
|
|
914
|
+
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed");
|
|
866
915
|
await maybeDrainDeferredInboundMessages();
|
|
867
916
|
}));
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
logError(
|
|
871
|
-
stateStore.appendLog(`fatal_error: ${
|
|
917
|
+
},
|
|
918
|
+
fatalError: (next) => {
|
|
919
|
+
logError(next.message);
|
|
920
|
+
stateStore.appendLog(`fatal_error: ${next.message}`);
|
|
872
921
|
stateStore.clearPendingConfirmation();
|
|
873
922
|
stateStore.clearPendingUserInput();
|
|
874
923
|
clearActiveTask();
|
|
875
924
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
876
|
-
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(
|
|
925
|
+
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error");
|
|
877
926
|
await maybeDrainDeferredInboundMessages();
|
|
878
927
|
}));
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
stateStore.appendLog(`shutdown_requested: ${
|
|
882
|
-
requestShutdown(
|
|
883
|
-
|
|
884
|
-
}
|
|
928
|
+
},
|
|
929
|
+
shutdownRequested: (next) => {
|
|
930
|
+
stateStore.appendLog(`shutdown_requested: ${next.reason}`);
|
|
931
|
+
requestShutdown(next.message, next.exitCode ?? 0);
|
|
932
|
+
},
|
|
933
|
+
}))
|
|
934
|
+
.catch((error) => {
|
|
935
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
936
|
+
logError(`Bridge event handling failed: ${message}`);
|
|
937
|
+
stateStore.appendLog(`event_forward_failed: ${message}`);
|
|
938
|
+
});
|
|
885
939
|
});
|
|
886
940
|
}
|
|
887
941
|
function formatInboundMessagePreview(message) {
|
|
@@ -896,44 +950,13 @@ function formatInboundMessagePreview(message) {
|
|
|
896
950
|
return "(empty)";
|
|
897
951
|
}
|
|
898
952
|
async function handleInboundMessage(params) {
|
|
899
|
-
|
|
953
|
+
const { message, } = params;
|
|
900
954
|
const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, deferInboundMessage, } = params;
|
|
901
955
|
const state = stateStore.getState();
|
|
902
956
|
if (message.senderId !== state.authorizedUserId) {
|
|
903
957
|
await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
904
958
|
return null;
|
|
905
959
|
}
|
|
906
|
-
// Emoji resolution: rewrite message text if it starts with a bound emoji
|
|
907
|
-
const emojiMatch = resolveEmojiCommand(message.text);
|
|
908
|
-
if (emojiMatch) {
|
|
909
|
-
const rewritten = emojiMatch.remainder
|
|
910
|
-
? `${emojiMatch.command} ${emojiMatch.remainder}`
|
|
911
|
-
: emojiMatch.command;
|
|
912
|
-
message = { ...message, text: rewritten };
|
|
913
|
-
}
|
|
914
|
-
// Emoji binding management commands
|
|
915
|
-
const bindingsCmd = parseEmojiBindingsCommand(message.text);
|
|
916
|
-
if (bindingsCmd) {
|
|
917
|
-
switch (bindingsCmd.type) {
|
|
918
|
-
case "list":
|
|
919
|
-
await queueWechatMessage(message.senderId, formatBindingsListMessage(listBindings()));
|
|
920
|
-
break;
|
|
921
|
-
case "bind":
|
|
922
|
-
setBinding(bindingsCmd.emoji, bindingsCmd.command);
|
|
923
|
-
await queueWechatMessage(message.senderId, `Bound ${bindingsCmd.emoji} → ${bindingsCmd.command}`);
|
|
924
|
-
break;
|
|
925
|
-
case "unbind": {
|
|
926
|
-
const removed = removeBinding(bindingsCmd.emoji);
|
|
927
|
-
await queueWechatMessage(message.senderId, removed ? `Unbound ${bindingsCmd.emoji}` : `No binding found for ${bindingsCmd.emoji}`);
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
return null;
|
|
932
|
-
}
|
|
933
|
-
if (isBindCommandPrefix(message.text)) {
|
|
934
|
-
await queueWechatMessage(message.senderId, formatBindCommandUsage());
|
|
935
|
-
return null;
|
|
936
|
-
}
|
|
937
960
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
938
961
|
adapter: options.adapter,
|
|
939
962
|
hasPendingConfirmation: Boolean(state.pendingConfirmation),
|
|
@@ -1056,40 +1079,54 @@ async function handleInboundMessage(params) {
|
|
|
1056
1079
|
};
|
|
1057
1080
|
}
|
|
1058
1081
|
}
|
|
1059
|
-
if (state.pendingConfirmation) {
|
|
1060
|
-
await queueWechatMessage(message.senderId, formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()));
|
|
1061
|
-
return null;
|
|
1062
|
-
}
|
|
1063
|
-
if (state.pendingUserInput) {
|
|
1064
|
-
await queueWechatMessage(message.senderId, formatPendingUserInputReminder(state.pendingUserInput));
|
|
1065
|
-
return null;
|
|
1066
|
-
}
|
|
1067
1082
|
const adapterState = adapter.getState();
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1083
|
+
const routeResult = await routeBridgeMessage({
|
|
1084
|
+
message: toChannelInboundMessage(message),
|
|
1085
|
+
authorized: true,
|
|
1086
|
+
command: null,
|
|
1087
|
+
adapterState,
|
|
1088
|
+
hasPendingApproval: Boolean(state.pendingConfirmation),
|
|
1089
|
+
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
1090
|
+
shouldDefer: shouldDeferCodexInboundMessage({
|
|
1091
|
+
adapter: options.adapter,
|
|
1092
|
+
status: adapterState.status,
|
|
1093
|
+
activeTurnOrigin: adapterState.activeTurnOrigin,
|
|
1094
|
+
hasPendingConfirmation: Boolean(state.pendingConfirmation),
|
|
1095
|
+
hasSystemCommand: Boolean(systemCommand),
|
|
1096
|
+
}),
|
|
1097
|
+
onUnauthorized: async () => undefined,
|
|
1098
|
+
handleCommand: async () => false,
|
|
1099
|
+
remindPendingApproval: async () => {
|
|
1100
|
+
await queueWechatMessage(message.senderId, formatPendingApprovalReminder(stateStore.getState().pendingConfirmation, adapter.getState()));
|
|
1101
|
+
},
|
|
1102
|
+
remindPendingUserInput: async () => {
|
|
1103
|
+
const pendingUserInput = stateStore.getState().pendingUserInput;
|
|
1104
|
+
await queueWechatMessage(message.senderId, pendingUserInput
|
|
1105
|
+
? formatPendingUserInputReminder(pendingUserInput)
|
|
1106
|
+
: `${options.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`);
|
|
1107
|
+
},
|
|
1108
|
+
remindBusy: async () => {
|
|
1109
|
+
const currentState = adapter.getState();
|
|
1110
|
+
if ((options.adapter === "codex" || options.adapter === "opencode" || options.adapter === "pi") &&
|
|
1111
|
+
currentState.activeTurnOrigin === "local") {
|
|
1112
|
+
await queueWechatMessage(message.senderId, `${options.adapter === "opencode" ? "OpenCode" : options.adapter === "pi" ? "Pi" : "codex"} is currently busy with a local terminal turn. Wait for it to finish or use /stop.`);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
|
|
1116
|
+
},
|
|
1117
|
+
defer: async () => {
|
|
1118
|
+
await deferInboundMessage(message);
|
|
1119
|
+
},
|
|
1120
|
+
dispatch: async () => {
|
|
1121
|
+
return await dispatchInboundWechatText({
|
|
1122
|
+
message,
|
|
1123
|
+
options,
|
|
1124
|
+
stateStore,
|
|
1125
|
+
adapter,
|
|
1126
|
+
});
|
|
1127
|
+
},
|
|
1092
1128
|
});
|
|
1129
|
+
return routeResult.kind === "dispatched" ? routeResult.result : null;
|
|
1093
1130
|
}
|
|
1094
1131
|
async function dispatchInboundWechatText(params) {
|
|
1095
1132
|
const { message, options, stateStore, adapter } = params;
|
|
@@ -1,58 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const WECHAT_SEND_MAX_ATTEMPTS = 3;
|
|
4
|
-
const WECHAT_SEND_RETRY_BASE_MS = 750;
|
|
5
|
-
export function formatUserFacingBridgeFatalError(message) {
|
|
6
|
-
return `Bridge error: ${message.replace(/\s+Recent app-server log:.*$/s, "").trim()}`;
|
|
7
|
-
}
|
|
8
|
-
export function shouldForwardBridgeEventToWechat(adapter, eventType, options = {}) {
|
|
9
|
-
if (adapter !== "opencode" && adapter !== "pi") {
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
switch (eventType) {
|
|
13
|
-
case "stdout":
|
|
14
|
-
case "stderr":
|
|
15
|
-
case "thread_switched":
|
|
16
|
-
return false;
|
|
17
|
-
case "notice":
|
|
18
|
-
return adapter === "pi" || /^OpenCode local draft:\s*/i.test(options.text ?? "");
|
|
19
|
-
case "mirrored_user_input":
|
|
20
|
-
return true;
|
|
21
|
-
default:
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export function formatUserFacingInboundError(params) {
|
|
26
|
-
const { adapter, cwd, errorText } = params;
|
|
27
|
-
if (adapter === "opencode" &&
|
|
28
|
-
/opencode companion is not connected/i.test(errorText)) {
|
|
29
|
-
return cwd
|
|
30
|
-
? `OpenCode is not connected for workspace:\n${cwd}\nRun "wechat-opencode" in that directory to recreate or reconnect the visible terminal.`
|
|
31
|
-
: 'OpenCode is not connected. Run "wechat-opencode" in this directory, then retry.';
|
|
32
|
-
}
|
|
33
|
-
return `Bridge error: ${errorText}`;
|
|
34
|
-
}
|
|
35
|
-
export function formatWechatSendFailureLogEntry(params) {
|
|
36
|
-
return `wechat_send_failed: context=${params.context} recipient=${params.recipientId} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
37
|
-
}
|
|
38
|
-
export function formatWechatContextTokenStaleLogEntry(params) {
|
|
39
|
-
return `wechat_context_token_stale: context=${params.context} recipient=${params.recipientId} action=wechat_message_required error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
40
|
-
}
|
|
41
|
-
export function formatWechatSendRetryLogEntry(params) {
|
|
42
|
-
return `wechat_send_retry: context=${params.context} recipient=${params.recipientId} attempt=${params.attempt} delay_ms=${params.delayMs} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
43
|
-
}
|
|
44
|
-
export function isRetryableWechatSendError(error) {
|
|
45
|
-
if (isWechatContextTokenStaleError(error)) {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
const classification = classifyWechatTransportError(error);
|
|
49
|
-
if (classification.retryable) {
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
const details = describeWechatTransportError(error);
|
|
53
|
-
return /^(?:Error|WechatApiResponseError): sendmessage failed:/i.test(details) &&
|
|
54
|
-
!/errcode=-14\b.*session timeout/i.test(details);
|
|
55
|
-
}
|
|
56
|
-
export function computeWechatSendRetryDelayMs(attempt) {
|
|
57
|
-
return WECHAT_SEND_RETRY_BASE_MS * attempt;
|
|
58
|
-
}
|
|
1
|
+
/** @deprecated Import from ../channels/wechat/wechat-forwarding.ts instead. */
|
|
2
|
+
export * from "../channels/wechat/wechat-forwarding.js";
|