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 +105 -109
- 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
|
@@ -4,17 +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 {
|
|
8
|
-
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "
|
|
7
|
+
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "./bridge-session-resume.js";
|
|
8
|
+
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
|
|
9
9
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
10
10
|
import { BridgeStateStore } from "./bridge-state.js";
|
|
11
11
|
import { getProcessRecordByPid, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "./bridge-process-reaper.js";
|
|
12
12
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
13
|
-
import {
|
|
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";
|
|
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";
|
|
14
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";
|
|
15
20
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
16
21
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
17
|
-
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "../daemon/emoji-bindings.js";
|
|
18
22
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
19
23
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
20
24
|
const PARENT_PROCESS_POLL_MS = 5_000;
|
|
@@ -269,6 +273,11 @@ async function main() {
|
|
|
269
273
|
initialResumeConversationId: stateStore.getState().resumeConversationId,
|
|
270
274
|
initialTranscriptPath: stateStore.getState().transcriptPath,
|
|
271
275
|
});
|
|
276
|
+
const pendingWechatMessages = new PendingWechatMessageStore(getPendingWechatMessagesFile(options.cwd));
|
|
277
|
+
const resumeCoordinator = new ResumeSessionCoordinator({
|
|
278
|
+
adapter: options.adapter,
|
|
279
|
+
runtime: adapter,
|
|
280
|
+
});
|
|
272
281
|
const controller = new BridgeController(adapter, options.cwd);
|
|
273
282
|
controller.clearLocalClientEndpoint();
|
|
274
283
|
stateStore.appendLog(`Cleared stale companion endpoint for ${options.cwd} before adapter start.`);
|
|
@@ -290,48 +299,89 @@ async function main() {
|
|
|
290
299
|
attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
291
300
|
return run;
|
|
292
301
|
};
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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}`);
|
|
299
314
|
}
|
|
300
|
-
|
|
315
|
+
if (isWechatContextUnavailableError(err)) {
|
|
301
316
|
if (isWechatContextTokenStaleError(err)) {
|
|
302
317
|
transport.clearCachedContextToken(senderId);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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({
|
|
306
323
|
context,
|
|
307
324
|
recipientId: senderId,
|
|
308
325
|
error: err,
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
313
|
-
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
314
|
-
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
315
|
-
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
326
|
+
})
|
|
327
|
+
: formatWechatSendFailureLogEntry({
|
|
316
328
|
context,
|
|
317
329
|
recipientId: senderId,
|
|
318
|
-
attempt,
|
|
319
|
-
delayMs,
|
|
320
330
|
error: err,
|
|
321
331
|
}));
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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({
|
|
327
338
|
context,
|
|
328
339
|
recipientId: senderId,
|
|
340
|
+
attempt,
|
|
341
|
+
delayMs,
|
|
329
342
|
error: err,
|
|
330
343
|
}));
|
|
331
|
-
|
|
344
|
+
await delay(delayMs);
|
|
345
|
+
continue;
|
|
332
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;
|
|
333
384
|
}
|
|
334
|
-
return false;
|
|
335
385
|
});
|
|
336
386
|
};
|
|
337
387
|
const trackWechatForwardTask = (task) => {
|
|
@@ -533,6 +583,7 @@ async function main() {
|
|
|
533
583
|
try {
|
|
534
584
|
wireAdapterEvents({
|
|
535
585
|
adapter,
|
|
586
|
+
resumeCoordinator,
|
|
536
587
|
options,
|
|
537
588
|
transport,
|
|
538
589
|
stateStore,
|
|
@@ -577,10 +628,6 @@ async function main() {
|
|
|
577
628
|
else if (options.adapter === "pi") {
|
|
578
629
|
log("For source-mode debugging, open the visible Pi client with: npm run pi:companion");
|
|
579
630
|
}
|
|
580
|
-
loadEmojiBindings();
|
|
581
|
-
// Keep the standalone bridge's WeChat welcome brief: command and emoji
|
|
582
|
-
// reference material belongs to /status and /bindings, which users can
|
|
583
|
-
// request on demand. The daemon sends its own, fuller welcome.
|
|
584
631
|
const welcomeText = t("bridge.welcome", {
|
|
585
632
|
adapter: options.adapter,
|
|
586
633
|
cwd: options.cwd,
|
|
@@ -628,6 +675,9 @@ async function main() {
|
|
|
628
675
|
log(`WeChat long poll recovered after ${recoveredFailures} transient error(s).`);
|
|
629
676
|
stateStore.appendLog(`poll_recovered: failures=${recoveredFailures}`);
|
|
630
677
|
}
|
|
678
|
+
if (pollResult.messages.length > 0 && pendingWechatMessages.list().length > 0) {
|
|
679
|
+
await flushPendingWechatMessages();
|
|
680
|
+
}
|
|
631
681
|
if (pollResult.ignoredBacklogCount > 0) {
|
|
632
682
|
stateStore.incrementIgnoredBacklog(pollResult.ignoredBacklogCount);
|
|
633
683
|
stateStore.appendLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
|
|
@@ -651,8 +701,12 @@ async function main() {
|
|
|
651
701
|
options,
|
|
652
702
|
stateStore,
|
|
653
703
|
adapter,
|
|
704
|
+
resumeCoordinator,
|
|
654
705
|
queueWechatMessage,
|
|
655
706
|
outputBatcher,
|
|
707
|
+
clearActiveTask: () => {
|
|
708
|
+
activeTask = null;
|
|
709
|
+
},
|
|
656
710
|
deferInboundMessage: async (nextMessage) => {
|
|
657
711
|
deferredInboundMessages.push({
|
|
658
712
|
message: nextMessage,
|
|
@@ -709,7 +763,22 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
709
763
|
}
|
|
710
764
|
}
|
|
711
765
|
function wireAdapterEvents(params) {
|
|
712
|
-
const { adapter, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
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();
|
|
713
782
|
adapter.setEventSink((event) => {
|
|
714
783
|
syncSharedSessionState();
|
|
715
784
|
syncLocalClientEndpoint();
|
|
@@ -722,56 +791,52 @@ function wireAdapterEvents(params) {
|
|
|
722
791
|
stateStore.clearPendingUserInput();
|
|
723
792
|
}
|
|
724
793
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
729
|
-
outputBatcher.push(
|
|
794
|
+
eventForwardChain = eventForwardChain
|
|
795
|
+
.then(() => forwardBridgeEvent(event, {
|
|
796
|
+
stdout: (next) => {
|
|
797
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
798
|
+
outputBatcher.push(next.text);
|
|
730
799
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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)}`);
|
|
734
809
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
735
|
-
await
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
},
|
|
741
|
-
sender: {
|
|
742
|
-
sendText: async (text) => {
|
|
743
|
-
const sent = await queueWechatMessage(authorizedUserId, text, "final_reply");
|
|
744
|
-
if (sent) {
|
|
745
|
-
stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
|
|
746
|
-
}
|
|
747
|
-
return sent;
|
|
748
|
-
},
|
|
749
|
-
sendImage: (imagePath) => queueWechatAttachmentAction(() => transport.sendImage(imagePath, { recipientId: authorizedUserId })),
|
|
750
|
-
sendFile: (filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId: authorizedUserId })),
|
|
751
|
-
sendVoice: (voicePath) => queueWechatAttachmentAction(() => transport.sendVoice(voicePath, authorizedUserId)),
|
|
752
|
-
sendVideo: (videoPath) => queueWechatAttachmentAction(() => transport.sendVideo(videoPath, { recipientId: authorizedUserId })),
|
|
810
|
+
await channelPort.send({
|
|
811
|
+
target: {
|
|
812
|
+
channelId: "wechat",
|
|
813
|
+
conversationId: authorizedUserId,
|
|
814
|
+
recipientId: authorizedUserId,
|
|
753
815
|
},
|
|
816
|
+
kind: "final_reply",
|
|
817
|
+
text: next.text,
|
|
818
|
+
adapter: options.adapter,
|
|
754
819
|
});
|
|
755
820
|
}));
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
if (
|
|
759
|
-
log(`${
|
|
760
|
-
stateStore.appendLog(`${
|
|
821
|
+
},
|
|
822
|
+
status: (next) => {
|
|
823
|
+
if (next.message) {
|
|
824
|
+
log(`${next.status}: ${next.message}`);
|
|
825
|
+
stateStore.appendLog(`${next.status}: ${next.message}`);
|
|
761
826
|
}
|
|
762
827
|
void maybeDrainDeferredInboundMessages();
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
stateStore.appendLog(`${
|
|
766
|
-
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 })) {
|
|
767
832
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
768
|
-
await queueWechatMessage(authorizedUserId,
|
|
833
|
+
await queueWechatMessage(authorizedUserId, next.text, "notice");
|
|
769
834
|
}));
|
|
770
835
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (
|
|
774
|
-
const thinkingPreview = formatThinkingForWechat(
|
|
836
|
+
},
|
|
837
|
+
thinking: (next) => {
|
|
838
|
+
if (next.text) {
|
|
839
|
+
const thinkingPreview = formatThinkingForWechat(next.text, 500);
|
|
775
840
|
if (thinkingPreview) {
|
|
776
841
|
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
777
842
|
trackWechatForwardTask((async () => {
|
|
@@ -779,91 +844,98 @@ function wireAdapterEvents(params) {
|
|
|
779
844
|
})());
|
|
780
845
|
}
|
|
781
846
|
}
|
|
782
|
-
|
|
783
|
-
|
|
847
|
+
},
|
|
848
|
+
approvalRequired: (next) => {
|
|
784
849
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
785
|
-
const pending = toPendingApproval(
|
|
850
|
+
const pending = toPendingApproval(next.request);
|
|
786
851
|
stateStore.setPendingConfirmation(pending);
|
|
787
852
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
788
853
|
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
789
854
|
}));
|
|
790
|
-
|
|
791
|
-
|
|
855
|
+
},
|
|
856
|
+
userInputRequired: (next) => {
|
|
792
857
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
793
|
-
const pending = toPendingUserInput(
|
|
858
|
+
const pending = toPendingUserInput(next.request);
|
|
794
859
|
stateStore.setPendingUserInput(pending);
|
|
795
860
|
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
796
861
|
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
|
|
797
862
|
}));
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(
|
|
801
|
-
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 })) {
|
|
802
867
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
803
|
-
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter,
|
|
868
|
+
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input");
|
|
804
869
|
}));
|
|
805
870
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
871
|
+
},
|
|
872
|
+
sessionSwitched: (next) => {
|
|
873
|
+
if (next.source === "local")
|
|
874
|
+
resumeCoordinator.clear();
|
|
875
|
+
stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
876
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
810
877
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
811
|
-
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
812
|
-
adapter: options.adapter,
|
|
813
|
-
sessionId: event.sessionId,
|
|
814
|
-
source: event.source,
|
|
815
|
-
reason: event.reason,
|
|
816
|
-
}), "session_switched");
|
|
878
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched");
|
|
817
879
|
}));
|
|
818
880
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
881
|
+
},
|
|
882
|
+
threadSwitched: (next) => {
|
|
883
|
+
if (next.source === "local")
|
|
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;
|
|
893
|
+
}
|
|
894
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
823
895
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
824
|
-
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
825
|
-
adapter: options.adapter,
|
|
826
|
-
sessionId: event.threadId,
|
|
827
|
-
source: event.source,
|
|
828
|
-
reason: event.reason,
|
|
829
|
-
}), "thread_switched");
|
|
896
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched");
|
|
830
897
|
}));
|
|
831
898
|
}
|
|
832
899
|
void maybeDrainDeferredInboundMessages();
|
|
833
|
-
|
|
834
|
-
|
|
900
|
+
},
|
|
901
|
+
taskComplete: () => {
|
|
835
902
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
836
903
|
stateStore.clearPendingConfirmation();
|
|
837
904
|
stateStore.clearPendingUserInput();
|
|
838
905
|
clearActiveTask();
|
|
839
906
|
await maybeDrainDeferredInboundMessages();
|
|
840
907
|
}));
|
|
841
|
-
|
|
842
|
-
|
|
908
|
+
},
|
|
909
|
+
taskFailed: (next) => {
|
|
843
910
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
844
911
|
stateStore.clearPendingConfirmation();
|
|
845
912
|
stateStore.clearPendingUserInput();
|
|
846
913
|
clearActiveTask();
|
|
847
|
-
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter,
|
|
914
|
+
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed");
|
|
848
915
|
await maybeDrainDeferredInboundMessages();
|
|
849
916
|
}));
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
logError(
|
|
853
|
-
stateStore.appendLog(`fatal_error: ${
|
|
917
|
+
},
|
|
918
|
+
fatalError: (next) => {
|
|
919
|
+
logError(next.message);
|
|
920
|
+
stateStore.appendLog(`fatal_error: ${next.message}`);
|
|
854
921
|
stateStore.clearPendingConfirmation();
|
|
855
922
|
stateStore.clearPendingUserInput();
|
|
856
923
|
clearActiveTask();
|
|
857
924
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
858
|
-
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(
|
|
925
|
+
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error");
|
|
859
926
|
await maybeDrainDeferredInboundMessages();
|
|
860
927
|
}));
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
stateStore.appendLog(`shutdown_requested: ${
|
|
864
|
-
requestShutdown(
|
|
865
|
-
|
|
866
|
-
}
|
|
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
|
+
});
|
|
867
939
|
});
|
|
868
940
|
}
|
|
869
941
|
function formatInboundMessagePreview(message) {
|
|
@@ -878,44 +950,13 @@ function formatInboundMessagePreview(message) {
|
|
|
878
950
|
return "(empty)";
|
|
879
951
|
}
|
|
880
952
|
async function handleInboundMessage(params) {
|
|
881
|
-
|
|
882
|
-
const { options, stateStore, adapter, queueWechatMessage, outputBatcher, deferInboundMessage, } = params;
|
|
953
|
+
const { message, } = params;
|
|
954
|
+
const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, deferInboundMessage, } = params;
|
|
883
955
|
const state = stateStore.getState();
|
|
884
956
|
if (message.senderId !== state.authorizedUserId) {
|
|
885
957
|
await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
886
958
|
return null;
|
|
887
959
|
}
|
|
888
|
-
// Emoji resolution: rewrite message text if it starts with a bound emoji
|
|
889
|
-
const emojiMatch = resolveEmojiCommand(message.text);
|
|
890
|
-
if (emojiMatch) {
|
|
891
|
-
const rewritten = emojiMatch.remainder
|
|
892
|
-
? `${emojiMatch.command} ${emojiMatch.remainder}`
|
|
893
|
-
: emojiMatch.command;
|
|
894
|
-
message = { ...message, text: rewritten };
|
|
895
|
-
}
|
|
896
|
-
// Emoji binding management commands
|
|
897
|
-
const bindingsCmd = parseEmojiBindingsCommand(message.text);
|
|
898
|
-
if (bindingsCmd) {
|
|
899
|
-
switch (bindingsCmd.type) {
|
|
900
|
-
case "list":
|
|
901
|
-
await queueWechatMessage(message.senderId, formatBindingsListMessage(listBindings()));
|
|
902
|
-
break;
|
|
903
|
-
case "bind":
|
|
904
|
-
setBinding(bindingsCmd.emoji, bindingsCmd.command);
|
|
905
|
-
await queueWechatMessage(message.senderId, `Bound ${bindingsCmd.emoji} → ${bindingsCmd.command}`);
|
|
906
|
-
break;
|
|
907
|
-
case "unbind": {
|
|
908
|
-
const removed = removeBinding(bindingsCmd.emoji);
|
|
909
|
-
await queueWechatMessage(message.senderId, removed ? `Unbound ${bindingsCmd.emoji}` : `No binding found for ${bindingsCmd.emoji}`);
|
|
910
|
-
break;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
if (isBindCommandPrefix(message.text)) {
|
|
916
|
-
await queueWechatMessage(message.senderId, formatBindCommandUsage());
|
|
917
|
-
return null;
|
|
918
|
-
}
|
|
919
960
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
920
961
|
adapter: options.adapter,
|
|
921
962
|
hasPendingConfirmation: Boolean(state.pendingConfirmation),
|
|
@@ -926,35 +967,23 @@ async function handleInboundMessage(params) {
|
|
|
926
967
|
await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
|
|
927
968
|
return null;
|
|
928
969
|
case "resume": {
|
|
929
|
-
if (options.adapter
|
|
930
|
-
await queueWechatMessage(message.senderId,
|
|
970
|
+
if (!isWechatResumeEnabled(options.adapter)) {
|
|
971
|
+
await queueWechatMessage(message.senderId, `WeChat /resume is disabled in ${options.adapter} mode. Use /resume directly inside "wechat-${options.adapter}"; WeChat will follow the active local session.`);
|
|
931
972
|
return null;
|
|
932
973
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
if (options.adapter === "opencode") {
|
|
938
|
-
try {
|
|
939
|
-
if (systemCommand.target) {
|
|
940
|
-
await adapter.resumeSession(systemCommand.target);
|
|
941
|
-
await queueWechatMessage(message.senderId, `Resumed OpenCode session ${systemCommand.target}.`);
|
|
942
|
-
}
|
|
943
|
-
else {
|
|
944
|
-
const candidates = await adapter.listResumeSessions(8);
|
|
945
|
-
await queueWechatMessage(message.senderId, formatResumeSessionList({
|
|
946
|
-
adapter: options.adapter,
|
|
947
|
-
candidates,
|
|
948
|
-
currentSessionId: adapter.getState().sharedSessionId,
|
|
949
|
-
}));
|
|
950
|
-
}
|
|
974
|
+
try {
|
|
975
|
+
if (systemCommand.target) {
|
|
976
|
+
await outputBatcher.flushNow();
|
|
951
977
|
}
|
|
952
|
-
|
|
953
|
-
|
|
978
|
+
const result = await resumeCoordinator.execute(systemCommand.target);
|
|
979
|
+
if (result.kind === "resumed") {
|
|
980
|
+
clearActiveTask();
|
|
954
981
|
}
|
|
955
|
-
|
|
982
|
+
await queueWechatMessage(message.senderId, result.message);
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
await queueWechatMessage(message.senderId, error instanceof Error ? error.message : String(error));
|
|
956
986
|
}
|
|
957
|
-
await queueWechatMessage(message.senderId, `/resume is not available in ${options.adapter} mode.`);
|
|
958
987
|
return null;
|
|
959
988
|
}
|
|
960
989
|
case "new_session": {
|
|
@@ -967,6 +996,7 @@ async function handleInboundMessage(params) {
|
|
|
967
996
|
stateStore.clearPendingConfirmation();
|
|
968
997
|
stateStore.clearPendingUserInput();
|
|
969
998
|
stateStore.clearSharedSessionId();
|
|
999
|
+
resumeCoordinator.clear();
|
|
970
1000
|
await adapter.createSession();
|
|
971
1001
|
stateStore.appendLog(`New ${options.adapter} session requested by owner.`);
|
|
972
1002
|
return null;
|
|
@@ -984,6 +1014,7 @@ async function handleInboundMessage(params) {
|
|
|
984
1014
|
stateStore.clearPendingConfirmation();
|
|
985
1015
|
stateStore.clearPendingUserInput();
|
|
986
1016
|
stateStore.clearSharedSessionId();
|
|
1017
|
+
resumeCoordinator.clear();
|
|
987
1018
|
await adapter.reset();
|
|
988
1019
|
stateStore.appendLog("Worker reset by owner.");
|
|
989
1020
|
await queueWechatMessage(message.senderId, "Worker session has been reset.");
|
|
@@ -1048,40 +1079,54 @@ async function handleInboundMessage(params) {
|
|
|
1048
1079
|
};
|
|
1049
1080
|
}
|
|
1050
1081
|
}
|
|
1051
|
-
if (state.pendingConfirmation) {
|
|
1052
|
-
await queueWechatMessage(message.senderId, formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()));
|
|
1053
|
-
return null;
|
|
1054
|
-
}
|
|
1055
|
-
if (state.pendingUserInput) {
|
|
1056
|
-
await queueWechatMessage(message.senderId, formatPendingUserInputReminder(state.pendingUserInput));
|
|
1057
|
-
return null;
|
|
1058
|
-
}
|
|
1059
1082
|
const adapterState = adapter.getState();
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
+
},
|
|
1084
1128
|
});
|
|
1129
|
+
return routeResult.kind === "dispatched" ? routeResult.result : null;
|
|
1085
1130
|
}
|
|
1086
1131
|
async function dispatchInboundWechatText(params) {
|
|
1087
1132
|
const { message, options, stateStore, adapter } = params;
|