cli-wechat-bridge 1.1.5 → 1.1.7
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 +212 -232
- package/bin/_run-entry.mjs +20 -5
- package/bin/wecom-check-update.mjs +5 -0
- package/bin/wecom-claude.mjs +5 -0
- package/bin/wecom-codex.mjs +5 -0
- package/bin/wecom-daemon.mjs +5 -0
- package/bin/wecom-opencode.mjs +5 -0
- package/bin/wecom-pi.mjs +5 -0
- package/bin/wecom-setup.mjs +5 -0
- package/dist/bridge/bridge-adapters.claude.js +128 -4
- 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-process-reaper.js +35 -2
- package/dist/bridge/bridge-state.js +11 -4
- package/dist/bridge/bridge-utils.js +4 -96
- package/dist/bridge/wechat-bridge.js +476 -206
- 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 +79 -0
- package/dist/channels/wecom/bridge-final-reply.js +26 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +176 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +66 -0
- package/dist/channels/wecom/wecom-media.js +141 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +308 -0
- package/dist/companion/codex-remote-client.js +2 -1
- package/dist/companion/local-companion-start.js +49 -9
- package/dist/companion/local-companion.js +2 -1
- 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 +3 -0
- package/dist/core/direct-run.js +17 -0
- package/dist/core/inbound-message-claims.js +61 -0
- package/dist/core/text-utils.js +63 -0
- package/dist/daemon/daemon-link.js +3 -0
- package/dist/daemon/wechat-daemon.js +466 -172
- package/dist/i18n/messages-en.js +4 -2
- package/dist/i18n/messages-zh.js +4 -2
- package/dist/utils/doctor.js +30 -10
- package/dist/wechat/setup.js +113 -13
- package/package.json +117 -107
|
@@ -8,17 +8,26 @@ import { resolveDefaultAdapterCommand, } from "../bridge/bridge-adapters.js";
|
|
|
8
8
|
import { t } from "../i18n/index.js";
|
|
9
9
|
import { delay, getSharedSessionIdFromAdapterState, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
|
|
10
10
|
import { BridgeController } from "../bridge/bridge-controller.js";
|
|
11
|
-
import { forwardWechatFinalReply } from "../bridge/bridge-final-reply.js";
|
|
12
11
|
import { readBridgeLockFile, } from "../bridge/bridge-state.js";
|
|
13
12
|
import { getProcessRecordByPid, isWechatDaemonCommandLine, killProcessTreeSync, listWechatDaemonProcesses, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "../bridge/bridge-process-reaper.js";
|
|
14
13
|
import { buildOneTimeCode, buildWechatInboundPrompt, formatApprovalMessage, formatDuration, formatMirroredUserInputMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatSessionSwitchMessage, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "../bridge/bridge-utils.js";
|
|
15
14
|
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "../bridge/bridge-session-resume.js";
|
|
16
|
-
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../
|
|
15
|
+
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
|
|
17
16
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
18
17
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
19
18
|
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
|
|
20
19
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
|
|
20
|
+
import { getPendingChannelMessagesFile, 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 { isDirectModuleRun } from "../core/direct-run.js";
|
|
26
|
+
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
27
|
+
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
28
|
+
import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
|
|
29
|
+
import { WecomTransport } from "../channels/wecom/wecom-transport.js";
|
|
30
|
+
import { buildWecomInboundPrompt, formatWecomVisibleText, } from "../channels/wecom/wecom-message.js";
|
|
22
31
|
import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
23
32
|
import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
|
|
24
33
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
@@ -36,11 +45,15 @@ const DAEMON_TAKEOVER_STOP_POLL_MS = 250;
|
|
|
36
45
|
const VISIBLE_CLIENT_CONNECT_TIMEOUT_MS = 15_000;
|
|
37
46
|
const VISIBLE_CLIENT_CONNECT_POLL_MS = 250;
|
|
38
47
|
const DAEMON_ADAPTERS = ["codex", "claude", "opencode", "pi"];
|
|
48
|
+
function getCliChannelId(argv = process.argv.slice(2)) {
|
|
49
|
+
const index = argv.indexOf("--channel");
|
|
50
|
+
return index >= 0 && argv[index + 1] === "wecom" ? "wecom" : "wechat";
|
|
51
|
+
}
|
|
39
52
|
function log(message) {
|
|
40
|
-
process.stderr.write(`[
|
|
53
|
+
process.stderr.write(`[${getCliChannelId()}-daemon] ${message}\n`);
|
|
41
54
|
}
|
|
42
55
|
function logError(message) {
|
|
43
|
-
process.stderr.write(`[
|
|
56
|
+
process.stderr.write(`[${getCliChannelId()}-daemon] ERROR: ${message}\n`);
|
|
44
57
|
}
|
|
45
58
|
function appendDaemonLog(message) {
|
|
46
59
|
ensureChannelDataDir();
|
|
@@ -51,6 +64,48 @@ function computePollRetryDelayMs(consecutiveFailures) {
|
|
|
51
64
|
const exponent = Math.min(normalizedFailures - 1, 5);
|
|
52
65
|
return Math.min(POLL_RETRY_MAX_MS, POLL_RETRY_BASE_MS * 2 ** exponent);
|
|
53
66
|
}
|
|
67
|
+
function toWecomOutputKind(context) {
|
|
68
|
+
switch (context) {
|
|
69
|
+
case "notice":
|
|
70
|
+
case "thinking":
|
|
71
|
+
case "final_reply":
|
|
72
|
+
case "approval_required":
|
|
73
|
+
case "user_input_required":
|
|
74
|
+
case "task_failed":
|
|
75
|
+
case "fatal_error":
|
|
76
|
+
return context;
|
|
77
|
+
case "mirrored_user_input":
|
|
78
|
+
return "mirrored_input";
|
|
79
|
+
default:
|
|
80
|
+
return "notice";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function toWechatSendContext(kind) {
|
|
84
|
+
if (kind === "mirrored_input") {
|
|
85
|
+
return "mirrored_user_input";
|
|
86
|
+
}
|
|
87
|
+
if (kind === "status") {
|
|
88
|
+
return "message";
|
|
89
|
+
}
|
|
90
|
+
return kind;
|
|
91
|
+
}
|
|
92
|
+
function toLegacyInboundWechatMessage(message) {
|
|
93
|
+
return {
|
|
94
|
+
senderId: message.senderId,
|
|
95
|
+
sender: message.senderId,
|
|
96
|
+
sessionId: message.conversation.conversationId,
|
|
97
|
+
text: message.text,
|
|
98
|
+
attachments: message.attachments.map((attachment) => ({
|
|
99
|
+
kind: attachment.kind === "image" ? "image" : "file",
|
|
100
|
+
path: attachment.path,
|
|
101
|
+
fileName: attachment.fileName || path.basename(attachment.path),
|
|
102
|
+
sizeBytes: attachment.sizeBytes ?? 0,
|
|
103
|
+
})),
|
|
104
|
+
contextToken: message.conversation.opaqueRef,
|
|
105
|
+
createdAt: message.createdAt,
|
|
106
|
+
createdAtMs: Date.parse(message.createdAt),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
54
109
|
function isDaemonAdapterKind(value) {
|
|
55
110
|
return value === "codex" || value === "claude" || value === "opencode" || value === "pi";
|
|
56
111
|
}
|
|
@@ -72,6 +127,7 @@ export function parseDaemonCliArgs(argv) {
|
|
|
72
127
|
let profile;
|
|
73
128
|
let initialAdapter;
|
|
74
129
|
let openVisible = true;
|
|
130
|
+
let channelId = "wechat";
|
|
75
131
|
for (let i = 0; i < argv.length; i += 1) {
|
|
76
132
|
const arg = argv[i];
|
|
77
133
|
if (!arg) {
|
|
@@ -80,10 +136,10 @@ export function parseDaemonCliArgs(argv) {
|
|
|
80
136
|
const next = argv[i + 1];
|
|
81
137
|
if (arg === "--help" || arg === "-h") {
|
|
82
138
|
process.stdout.write([
|
|
83
|
-
|
|
139
|
+
`Usage: ${channelId}-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]`,
|
|
84
140
|
"",
|
|
85
|
-
|
|
86
|
-
|
|
141
|
+
`Keeps one ${channelId === "wecom" ? "WeCom" : "WeChat"} connection alive and switches between Codex, Claude Code, OpenCode, and Pi.`,
|
|
142
|
+
`Send /codex, /claude, /opencode, or /pi in ${channelId === "wecom" ? "WeCom" : "WeChat"} to switch the active terminal.`,
|
|
87
143
|
"",
|
|
88
144
|
].join("\n"));
|
|
89
145
|
process.exit(0);
|
|
@@ -96,6 +152,14 @@ export function parseDaemonCliArgs(argv) {
|
|
|
96
152
|
i += 1;
|
|
97
153
|
continue;
|
|
98
154
|
}
|
|
155
|
+
if (arg === "--channel") {
|
|
156
|
+
if (!next || (next !== "wechat" && next !== "wecom")) {
|
|
157
|
+
throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
|
|
158
|
+
}
|
|
159
|
+
channelId = next;
|
|
160
|
+
i += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
99
163
|
if (arg === "--adapter") {
|
|
100
164
|
if (!isDaemonAdapterKind(next)) {
|
|
101
165
|
throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
|
|
@@ -118,7 +182,13 @@ export function parseDaemonCliArgs(argv) {
|
|
|
118
182
|
}
|
|
119
183
|
throw new Error(`Unknown argument: ${arg}`);
|
|
120
184
|
}
|
|
121
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
cwd,
|
|
187
|
+
profile,
|
|
188
|
+
initialAdapter,
|
|
189
|
+
openVisible,
|
|
190
|
+
...(channelId === "wecom" ? { channelId } : {}),
|
|
191
|
+
};
|
|
122
192
|
}
|
|
123
193
|
export function parseDaemonSwitchCommand(text) {
|
|
124
194
|
const normalized = text.trim().toLowerCase();
|
|
@@ -217,7 +287,7 @@ export function buildVisibleClientLaunchArgs(params) {
|
|
|
217
287
|
export function buildWindowsVisibleClientLaunchCommand(params) {
|
|
218
288
|
return [
|
|
219
289
|
"start",
|
|
220
|
-
quoteWindowsCommandArg(
|
|
290
|
+
quoteWindowsCommandArg(`${params.channelId ?? "wechat"}-${params.adapter}`),
|
|
221
291
|
"/D",
|
|
222
292
|
quoteWindowsCommandArg(params.cwd),
|
|
223
293
|
quoteWindowsCommandArg(process.execPath),
|
|
@@ -265,6 +335,7 @@ function openVisibleClient(params) {
|
|
|
265
335
|
adapter: params.adapter,
|
|
266
336
|
cwd: params.cwd,
|
|
267
337
|
args,
|
|
338
|
+
channelId: params.channelId,
|
|
268
339
|
}),
|
|
269
340
|
];
|
|
270
341
|
const child = spawn(command, launchArgs, {
|
|
@@ -285,7 +356,7 @@ function openVisibleClient(params) {
|
|
|
285
356
|
pid: child.pid,
|
|
286
357
|
};
|
|
287
358
|
}
|
|
288
|
-
const title =
|
|
359
|
+
const title = `${params.channelId ?? "wechat"}-${params.adapter}`;
|
|
289
360
|
const fullArgs = [process.execPath, ...args];
|
|
290
361
|
if (process.platform === "darwin") {
|
|
291
362
|
const cmdLine = fullArgs.map(shellQuotePosix).join(" ");
|
|
@@ -482,6 +553,10 @@ class WechatDaemon {
|
|
|
482
553
|
profile;
|
|
483
554
|
authorizedUserId;
|
|
484
555
|
transport;
|
|
556
|
+
channelId;
|
|
557
|
+
wecomTransport;
|
|
558
|
+
currentInboundConversation = null;
|
|
559
|
+
handlingInboundMessage = false;
|
|
485
560
|
slots = new Map();
|
|
486
561
|
// Per-adapter serialization chains for ensureSlot (see ensureSlot comment).
|
|
487
562
|
slotEnsureChains = new Map();
|
|
@@ -493,6 +568,7 @@ class WechatDaemon {
|
|
|
493
568
|
textSendChain = Promise.resolve();
|
|
494
569
|
attachmentSendChain = Promise.resolve();
|
|
495
570
|
pendingWechatForwardTasks = new Set();
|
|
571
|
+
pendingWechatMessages;
|
|
496
572
|
shutdownPromise = null;
|
|
497
573
|
ipcServer = null;
|
|
498
574
|
endpointToken = "";
|
|
@@ -501,6 +577,19 @@ class WechatDaemon {
|
|
|
501
577
|
this.profile = params.profile;
|
|
502
578
|
this.authorizedUserId = params.authorizedUserId;
|
|
503
579
|
this.transport = params.transport;
|
|
580
|
+
this.channelId = params.channelId ?? "wechat";
|
|
581
|
+
this.wecomTransport = params.wecomTransport ?? null;
|
|
582
|
+
if (this.channelId === "wecom") {
|
|
583
|
+
this.currentInboundConversation = {
|
|
584
|
+
channelId: "wecom",
|
|
585
|
+
conversationId: params.authorizedUserId,
|
|
586
|
+
recipientId: params.authorizedUserId,
|
|
587
|
+
metadata: { chatType: "direct" },
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
this.pendingWechatMessages = new PendingWechatMessageStore(this.channelId === "wechat"
|
|
591
|
+
? getPendingWechatMessagesFile(this.cwd)
|
|
592
|
+
: getPendingChannelMessagesFile(this.cwd, this.channelId));
|
|
504
593
|
}
|
|
505
594
|
async startIpcServer() {
|
|
506
595
|
this.endpointToken = buildDaemonToken();
|
|
@@ -548,6 +637,7 @@ class WechatDaemon {
|
|
|
548
637
|
token: this.endpointToken,
|
|
549
638
|
cwd: this.cwd,
|
|
550
639
|
startedAt: this.startedAt,
|
|
640
|
+
channelId: this.channelId,
|
|
551
641
|
});
|
|
552
642
|
resolve();
|
|
553
643
|
});
|
|
@@ -556,6 +646,7 @@ class WechatDaemon {
|
|
|
556
646
|
getStatus() {
|
|
557
647
|
return {
|
|
558
648
|
cwd: this.cwd,
|
|
649
|
+
channelId: this.channelId,
|
|
559
650
|
activeAdapter: this.activeAdapter ?? undefined,
|
|
560
651
|
startedAt: this.startedAt,
|
|
561
652
|
slots: Array.from(this.slots.values()).map((slot) => {
|
|
@@ -584,10 +675,52 @@ class WechatDaemon {
|
|
|
584
675
|
}
|
|
585
676
|
async runPollLoop() {
|
|
586
677
|
let consecutivePollFailures = 0;
|
|
587
|
-
|
|
678
|
+
if (this.wecomTransport) {
|
|
679
|
+
this.wecomTransport.setHandlers({
|
|
680
|
+
onMessage: async (channelMessage) => {
|
|
681
|
+
if (this.shutdownPromise) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.currentInboundConversation = channelMessage.conversation;
|
|
685
|
+
this.handlingInboundMessage = true;
|
|
686
|
+
try {
|
|
687
|
+
if (this.pendingWechatMessages.list().length > 0) {
|
|
688
|
+
await this.flushPendingWechatMessages();
|
|
689
|
+
}
|
|
690
|
+
await this.handleInboundMessage(toLegacyInboundWechatMessage(channelMessage));
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
this.handlingInboundMessage = false;
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
onUnauthorized: async (senderId, chatType) => {
|
|
697
|
+
appendDaemonLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
|
|
698
|
+
if (chatType === "direct") {
|
|
699
|
+
await this.wecomTransport.sendText({
|
|
700
|
+
channelId: "wecom",
|
|
701
|
+
conversationId: senderId,
|
|
702
|
+
recipientId: senderId,
|
|
703
|
+
metadata: { chatType: "direct" },
|
|
704
|
+
}, "Unauthorized.", "notice");
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
onFatal: async (error) => {
|
|
708
|
+
appendDaemonLog(`wecom_fatal_error: ${error.message}`);
|
|
709
|
+
await this.shutdown();
|
|
710
|
+
},
|
|
711
|
+
onConnected: async () => {
|
|
712
|
+
if (this.pendingWechatMessages.list().length > 0) {
|
|
713
|
+
await this.flushPendingWechatMessages();
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
this.wecomTransport.start();
|
|
718
|
+
await this.wecomTransport.waitUntilConnected();
|
|
719
|
+
}
|
|
720
|
+
log(`${this.channelId === "wecom" ? "WeCom" : "WeChat"} daemon is ready.`);
|
|
588
721
|
log(`Working directory: ${this.cwd}`);
|
|
589
|
-
log(
|
|
590
|
-
appendDaemonLog(`started: cwd=${this.cwd}`);
|
|
722
|
+
log(`Switch from ${this.channelId === "wecom" ? "WeCom" : "WeChat"} with /codex, /claude, /opencode, or /pi.`);
|
|
723
|
+
appendDaemonLog(`started: channel=${this.channelId} cwd=${this.cwd}`);
|
|
591
724
|
const activeSlot = this.getActiveSlot();
|
|
592
725
|
const welcomeText = t("daemon.welcome", {
|
|
593
726
|
cwd: this.cwd,
|
|
@@ -595,6 +728,13 @@ class WechatDaemon {
|
|
|
595
728
|
bindings: formatBindingsListMessage(listBindings()),
|
|
596
729
|
});
|
|
597
730
|
await this.queueWechatMessage(this.authorizedUserId, welcomeText);
|
|
731
|
+
if (this.channelId === "wecom") {
|
|
732
|
+
while (!this.shutdownPromise) {
|
|
733
|
+
await delay(1_000);
|
|
734
|
+
}
|
|
735
|
+
await this.shutdownPromise;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
598
738
|
while (!this.shutdownPromise) {
|
|
599
739
|
let pollResult;
|
|
600
740
|
try {
|
|
@@ -624,6 +764,9 @@ class WechatDaemon {
|
|
|
624
764
|
appendDaemonLog(`poll_recovered: failures=${consecutivePollFailures}`);
|
|
625
765
|
consecutivePollFailures = 0;
|
|
626
766
|
}
|
|
767
|
+
if (pollResult.messages.length > 0 && this.pendingWechatMessages.list().length > 0) {
|
|
768
|
+
await this.flushPendingWechatMessages();
|
|
769
|
+
}
|
|
627
770
|
if (pollResult.ignoredBacklogCount > 0) {
|
|
628
771
|
appendDaemonLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
|
|
629
772
|
if (!this.backlogNoticeSent) {
|
|
@@ -696,6 +839,12 @@ class WechatDaemon {
|
|
|
696
839
|
await this.waitForPendingWechatForwardTasks();
|
|
697
840
|
await this.textSendChain.catch(() => undefined);
|
|
698
841
|
await this.attachmentSendChain.catch(() => undefined);
|
|
842
|
+
try {
|
|
843
|
+
this.wecomTransport?.stop();
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
// Best effort shutdown.
|
|
847
|
+
}
|
|
699
848
|
for (const slot of this.slots.values()) {
|
|
700
849
|
try {
|
|
701
850
|
await slot.runtime.dispose();
|
|
@@ -727,7 +876,7 @@ class WechatDaemon {
|
|
|
727
876
|
return { shuttingDown: true };
|
|
728
877
|
case "ensure_slot":
|
|
729
878
|
if (!isSameWorkspaceCwd(request.cwd, this.cwd)) {
|
|
730
|
-
throw new Error(
|
|
879
|
+
throw new Error(`${this.channelId}-daemon is bound to ${this.cwd}; requested cwd was ${request.cwd}.`);
|
|
731
880
|
}
|
|
732
881
|
return await this.ensureSlot(request.adapter, {
|
|
733
882
|
profile: request.profile,
|
|
@@ -819,6 +968,7 @@ class WechatDaemon {
|
|
|
819
968
|
cwd: this.cwd,
|
|
820
969
|
sessionStartMode,
|
|
821
970
|
cliArgs: options.cliArgs,
|
|
971
|
+
channelId: this.channelId,
|
|
822
972
|
onError: (error) => {
|
|
823
973
|
appendDaemonLog(`visible_client_open_error: adapter=${adapter} error=${truncatePreview(error.message, 400)}`);
|
|
824
974
|
},
|
|
@@ -899,6 +1049,8 @@ class WechatDaemon {
|
|
|
899
1049
|
}),
|
|
900
1050
|
activeTask: null,
|
|
901
1051
|
lastOutputAt: 0,
|
|
1052
|
+
lastFinalReplyAtMs: 0,
|
|
1053
|
+
eventForwardChain: Promise.resolve(),
|
|
902
1054
|
};
|
|
903
1055
|
runtime.setEventSink((event) => {
|
|
904
1056
|
this.handleSlotEvent(slot, event);
|
|
@@ -925,166 +1077,192 @@ class WechatDaemon {
|
|
|
925
1077
|
}
|
|
926
1078
|
slot.controller.syncLocalClientEndpoint();
|
|
927
1079
|
}
|
|
1080
|
+
createWechatChannelPort(adapter) {
|
|
1081
|
+
if (this.channelId === "wecom") {
|
|
1082
|
+
return new WecomChannelPort({
|
|
1083
|
+
transport: this.wecomTransport,
|
|
1084
|
+
sendText: (target, text, kind) => this.queueWechatMessage(this.authorizedUserId, text, toWechatSendContext(kind), target),
|
|
1085
|
+
prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
|
|
1086
|
+
onEmptyVisibleReply: (currentAdapter, rawText) => {
|
|
1087
|
+
appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
|
|
1088
|
+
},
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
return new WechatChannelPort({
|
|
1092
|
+
sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
|
|
1093
|
+
sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(filePath, { recipientId })),
|
|
1094
|
+
sendFile: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, { recipientId })),
|
|
1095
|
+
sendVoice: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(filePath, recipientId)),
|
|
1096
|
+
sendVideo: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(filePath, { recipientId })),
|
|
1097
|
+
prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
|
|
1098
|
+
onEmptyVisibleReply: (currentAdapter, rawText) => {
|
|
1099
|
+
appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
|
|
1100
|
+
},
|
|
1101
|
+
onTextSent: (currentAdapter, text) => {
|
|
1102
|
+
appendDaemonLog(`final_reply_sent: adapter=${currentAdapter ?? adapter} chars=${Array.from(text).length}`);
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
928
1106
|
handleSlotEvent(slot, event) {
|
|
929
1107
|
slot.controller.syncLocalClientEndpoint();
|
|
930
1108
|
const adapterState = slot.runtime.getState();
|
|
1109
|
+
const channelPort = this.createWechatChannelPort(slot.adapter);
|
|
931
1110
|
if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
|
|
932
1111
|
slot.pendingConfirmations = [];
|
|
933
1112
|
}
|
|
934
1113
|
if (slot.pendingUserInput && !adapterState.pendingUserInput) {
|
|
935
1114
|
slot.pendingUserInput = null;
|
|
936
1115
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1116
|
+
slot.eventForwardChain = slot.eventForwardChain
|
|
1117
|
+
.then(() => forwardBridgeEvent(event, {
|
|
1118
|
+
stdout: (next) => {
|
|
940
1119
|
slot.lastOutputAt = Date.now();
|
|
941
|
-
if (shouldForwardBridgeEventToWechat(slot.adapter,
|
|
942
|
-
slot.outputBatcher.push(
|
|
1120
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1121
|
+
slot.outputBatcher.push(next.text);
|
|
943
1122
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1123
|
+
},
|
|
1124
|
+
stderr: (next) => {
|
|
1125
|
+
slot.lastOutputAt = Date.now();
|
|
1126
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1127
|
+
slot.outputBatcher.push(next.text);
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
finalReply: (next) => {
|
|
1131
|
+
slot.lastFinalReplyAtMs = Date.now();
|
|
1132
|
+
appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
|
|
947
1133
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
948
|
-
await
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const sent = await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, text), "final_reply");
|
|
957
|
-
if (sent) {
|
|
958
|
-
appendDaemonLog(`final_reply_sent: adapter=${slot.adapter} chars=${Array.from(text).length}`);
|
|
959
|
-
}
|
|
960
|
-
return sent;
|
|
961
|
-
},
|
|
962
|
-
sendImage: (imagePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(imagePath, {
|
|
963
|
-
recipientId: this.authorizedUserId,
|
|
964
|
-
})),
|
|
965
|
-
sendFile: (filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, {
|
|
966
|
-
recipientId: this.authorizedUserId,
|
|
967
|
-
})),
|
|
968
|
-
sendVoice: (voicePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(voicePath, this.authorizedUserId)),
|
|
969
|
-
sendVideo: (videoPath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(videoPath, {
|
|
1134
|
+
await channelPort.send({
|
|
1135
|
+
target: this.channelId === "wecom"
|
|
1136
|
+
? slot.activeConversation ??
|
|
1137
|
+
slot.lastConversation ??
|
|
1138
|
+
this.currentInboundConversation
|
|
1139
|
+
: {
|
|
1140
|
+
channelId: "wechat",
|
|
1141
|
+
conversationId: this.authorizedUserId,
|
|
970
1142
|
recipientId: this.authorizedUserId,
|
|
971
|
-
}
|
|
972
|
-
|
|
1143
|
+
},
|
|
1144
|
+
kind: "final_reply",
|
|
1145
|
+
text: next.text,
|
|
1146
|
+
adapter: slot.adapter,
|
|
973
1147
|
});
|
|
974
1148
|
}));
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if (
|
|
978
|
-
log(`${slot.adapter} ${
|
|
979
|
-
appendDaemonLog(`${slot.adapter}_${
|
|
1149
|
+
},
|
|
1150
|
+
status: (next) => {
|
|
1151
|
+
if (next.message) {
|
|
1152
|
+
log(`${slot.adapter} ${next.status}: ${next.message}`);
|
|
1153
|
+
appendDaemonLog(`${slot.adapter}_${next.status}: ${next.message}`);
|
|
980
1154
|
}
|
|
981
|
-
|
|
982
|
-
|
|
1155
|
+
},
|
|
1156
|
+
notice: (next) => {
|
|
983
1157
|
slot.lastOutputAt = Date.now();
|
|
984
|
-
appendDaemonLog(`${slot.adapter}_${
|
|
985
|
-
if (shouldForwardBridgeEventToWechat(slot.adapter,
|
|
1158
|
+
appendDaemonLog(`${slot.adapter}_${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
1159
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
|
|
986
1160
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
987
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter,
|
|
1161
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice");
|
|
988
1162
|
}));
|
|
989
1163
|
}
|
|
990
|
-
|
|
991
|
-
|
|
1164
|
+
},
|
|
1165
|
+
approvalRequired: (next) => {
|
|
992
1166
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
993
|
-
const pending = toPendingApproval(
|
|
1167
|
+
const pending = toPendingApproval(next);
|
|
994
1168
|
slot.pendingConfirmations.push(pending);
|
|
995
1169
|
appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
|
|
996
1170
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
|
|
997
1171
|
}));
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
// send (which retries with backoff). Otherwise a fast user reply can
|
|
1002
|
-
// race past the still-unwritten mirror and be dispatched as plain
|
|
1003
|
-
// input into a CLI that is waiting for a structured form answer.
|
|
1004
|
-
const pending = toPendingUserInput(event.request);
|
|
1172
|
+
},
|
|
1173
|
+
userInputRequired: (next) => {
|
|
1174
|
+
const pending = toPendingUserInput(next.request);
|
|
1005
1175
|
slot.pendingUserInput = pending;
|
|
1006
1176
|
appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
|
|
1007
1177
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1008
1178
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
|
|
1009
1179
|
}));
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (shouldForwardBridgeEventToWechat(slot.adapter, event.type, { text: event.text })) {
|
|
1180
|
+
},
|
|
1181
|
+
mirroredUserInput: (next) => {
|
|
1182
|
+
appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
|
|
1183
|
+
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
|
|
1015
1184
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1016
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter,
|
|
1185
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input");
|
|
1017
1186
|
}));
|
|
1018
1187
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (
|
|
1188
|
+
},
|
|
1189
|
+
sessionSwitched: (next) => {
|
|
1190
|
+
if (next.source === "local")
|
|
1022
1191
|
slot.resumeCoordinator.clear();
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
1026
|
-
shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
|
|
1192
|
+
appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
1193
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1027
1194
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1028
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
|
|
1029
|
-
adapter: slot.adapter,
|
|
1030
|
-
sessionId: event.sessionId,
|
|
1031
|
-
source: event.source,
|
|
1032
|
-
reason: event.reason,
|
|
1033
|
-
})), "session_switched");
|
|
1195
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched");
|
|
1034
1196
|
}));
|
|
1035
1197
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (
|
|
1198
|
+
},
|
|
1199
|
+
threadSwitched: (next) => {
|
|
1200
|
+
if (next.source === "local")
|
|
1039
1201
|
slot.resumeCoordinator.clear();
|
|
1202
|
+
appendDaemonLog(`thread_switched: adapter=${slot.adapter} thread=${next.threadId} source=${next.source} reason=${next.reason}`);
|
|
1203
|
+
if (shouldSuppressCodexLocalThreadNotice({
|
|
1204
|
+
adapter: slot.adapter,
|
|
1205
|
+
source: next.source,
|
|
1206
|
+
activeTurnOrigin: slot.runtime.getState().activeTurnOrigin,
|
|
1207
|
+
lastFinalReplyAtMs: slot.lastFinalReplyAtMs,
|
|
1208
|
+
})) {
|
|
1209
|
+
return;
|
|
1040
1210
|
}
|
|
1041
|
-
|
|
1042
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
1043
|
-
shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
|
|
1211
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1044
1212
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1045
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
|
|
1046
|
-
adapter: slot.adapter,
|
|
1047
|
-
sessionId: event.threadId,
|
|
1048
|
-
source: event.source,
|
|
1049
|
-
reason: event.reason,
|
|
1050
|
-
})), "thread_switched");
|
|
1213
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched");
|
|
1051
1214
|
}));
|
|
1052
1215
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1216
|
+
},
|
|
1217
|
+
taskComplete: () => {
|
|
1055
1218
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
|
|
1056
1219
|
slot.pendingConfirmations = [];
|
|
1057
1220
|
slot.pendingUserInput = null;
|
|
1058
1221
|
slot.activeTask = null;
|
|
1222
|
+
slot.activeConversation = undefined;
|
|
1059
1223
|
}));
|
|
1060
|
-
|
|
1061
|
-
|
|
1224
|
+
},
|
|
1225
|
+
taskFailed: (next) => {
|
|
1062
1226
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1063
1227
|
slot.pendingConfirmations = [];
|
|
1064
1228
|
slot.pendingUserInput = null;
|
|
1065
1229
|
slot.activeTask = null;
|
|
1066
|
-
|
|
1230
|
+
slot.activeConversation = undefined;
|
|
1231
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed");
|
|
1067
1232
|
}));
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
logError(`${slot.adapter}: ${
|
|
1071
|
-
appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${
|
|
1233
|
+
},
|
|
1234
|
+
fatalError: (next) => {
|
|
1235
|
+
logError(`${slot.adapter}: ${next.message}`);
|
|
1236
|
+
appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${next.message}`);
|
|
1072
1237
|
slot.pendingConfirmations = [];
|
|
1073
1238
|
slot.pendingUserInput = null;
|
|
1074
1239
|
slot.activeTask = null;
|
|
1240
|
+
slot.activeConversation = undefined;
|
|
1075
1241
|
this.disposeDeadSlot(slot);
|
|
1076
1242
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1077
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(
|
|
1243
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
|
|
1078
1244
|
}));
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${
|
|
1082
|
-
|
|
1245
|
+
},
|
|
1246
|
+
shutdownRequested: (next) => {
|
|
1247
|
+
appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${next.reason}`);
|
|
1248
|
+
},
|
|
1249
|
+
}))
|
|
1250
|
+
.catch((error) => {
|
|
1251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1252
|
+
appendDaemonLog(`event_forward_failed: adapter=${slot.adapter} message=${message}`);
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
bindCurrentWecomConversation(slot) {
|
|
1256
|
+
if (this.channelId !== "wecom" || !slot || !this.currentInboundConversation) {
|
|
1257
|
+
return;
|
|
1083
1258
|
}
|
|
1259
|
+
slot.lastConversation = this.currentInboundConversation;
|
|
1084
1260
|
}
|
|
1085
1261
|
async handleInboundMessage(message) {
|
|
1086
1262
|
if (message.senderId !== this.authorizedUserId) {
|
|
1087
|
-
await this.queueWechatMessage(message.senderId,
|
|
1263
|
+
await this.queueWechatMessage(message.senderId, this.channelId === "wecom"
|
|
1264
|
+
? "Unauthorized. This daemon only accepts messages from the paired WeCom operator."
|
|
1265
|
+
: "Unauthorized. This daemon only accepts messages from the configured WeChat owner.");
|
|
1088
1266
|
return;
|
|
1089
1267
|
}
|
|
1090
1268
|
const emojiMatch = resolveEmojiCommand(message.text);
|
|
@@ -1096,6 +1274,7 @@ class WechatDaemon {
|
|
|
1096
1274
|
reuseExistingVisible: true,
|
|
1097
1275
|
});
|
|
1098
1276
|
if (result.activated) {
|
|
1277
|
+
this.bindCurrentWecomConversation(this.getActiveSlot());
|
|
1099
1278
|
message = { ...message, text: emojiMatch.remainder };
|
|
1100
1279
|
}
|
|
1101
1280
|
else {
|
|
@@ -1122,6 +1301,7 @@ class WechatDaemon {
|
|
|
1122
1301
|
await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchDirective.adapter}.\n${detail}`);
|
|
1123
1302
|
return;
|
|
1124
1303
|
}
|
|
1304
|
+
this.bindCurrentWecomConversation(this.getActiveSlot());
|
|
1125
1305
|
if (switchDirective.remainder) {
|
|
1126
1306
|
message = { ...message, text: switchDirective.remainder };
|
|
1127
1307
|
}
|
|
@@ -1131,7 +1311,7 @@ class WechatDaemon {
|
|
|
1131
1311
|
}
|
|
1132
1312
|
}
|
|
1133
1313
|
if (message.text.trim().toLowerCase() === "/daemon-stop") {
|
|
1134
|
-
await this.queueWechatMessage(message.senderId,
|
|
1314
|
+
await this.queueWechatMessage(message.senderId, `Stopping ${this.channelId}-daemon...`);
|
|
1135
1315
|
setTimeout(() => {
|
|
1136
1316
|
void this.shutdown().finally(() => process.exit(0));
|
|
1137
1317
|
}, 0);
|
|
@@ -1151,21 +1331,56 @@ class WechatDaemon {
|
|
|
1151
1331
|
await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
|
|
1152
1332
|
return;
|
|
1153
1333
|
}
|
|
1334
|
+
this.bindCurrentWecomConversation(slot);
|
|
1335
|
+
const routeCurrentMessage = async (currentSlot, command) => {
|
|
1336
|
+
const previousTask = currentSlot.activeTask;
|
|
1337
|
+
await routeBridgeMessage({
|
|
1338
|
+
message: toChannelInboundMessage(message),
|
|
1339
|
+
authorized: true,
|
|
1340
|
+
command,
|
|
1341
|
+
adapterState: currentSlot.runtime.getState(),
|
|
1342
|
+
hasPendingApproval: currentSlot.pendingConfirmations.length > 0,
|
|
1343
|
+
hasPendingUserInput: Boolean(currentSlot.pendingUserInput),
|
|
1344
|
+
onUnauthorized: async () => undefined,
|
|
1345
|
+
handleCommand: async (nextCommand) => {
|
|
1346
|
+
await this.handleSystemCommand(message, currentSlot, nextCommand);
|
|
1347
|
+
return true;
|
|
1348
|
+
},
|
|
1349
|
+
remindPendingApproval: async () => {
|
|
1350
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, formatPendingApprovalReminder(currentSlot.pendingConfirmations[0], currentSlot.runtime.getState())));
|
|
1351
|
+
},
|
|
1352
|
+
remindPendingUserInput: async () => {
|
|
1353
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, currentSlot.pendingUserInput
|
|
1354
|
+
? formatPendingUserInputReminder(currentSlot.pendingUserInput)
|
|
1355
|
+
: `${currentSlot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
|
|
1356
|
+
},
|
|
1357
|
+
remindBusy: async () => {
|
|
1358
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, `${currentSlot.adapter} is still working. Wait for the current reply or use /stop.`));
|
|
1359
|
+
},
|
|
1360
|
+
defer: async () => undefined,
|
|
1361
|
+
dispatch: async () => {
|
|
1362
|
+
await this.dispatchInboundWechatText(message, currentSlot);
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
1365
|
+
if (this.channelId === "wecom" &&
|
|
1366
|
+
this.currentInboundConversation &&
|
|
1367
|
+
currentSlot.activeTask &&
|
|
1368
|
+
currentSlot.activeTask !== previousTask) {
|
|
1369
|
+
currentSlot.activeConversation = this.currentInboundConversation;
|
|
1370
|
+
currentSlot.lastConversation = this.currentInboundConversation;
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1154
1373
|
const command = parseWechatControlCommand(message.text, {
|
|
1155
1374
|
adapter: slot.adapter,
|
|
1156
1375
|
hasPendingConfirmation: slot.pendingConfirmations.length > 0,
|
|
1157
1376
|
hasPendingUserInput: Boolean(slot.pendingUserInput),
|
|
1158
1377
|
});
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
await
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
if (slot.pendingUserInput) {
|
|
1168
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingUserInputReminder(slot.pendingUserInput)));
|
|
1378
|
+
const slotState = slot.runtime.getState();
|
|
1379
|
+
if (command ||
|
|
1380
|
+
slot.pendingConfirmations.length > 0 ||
|
|
1381
|
+
slot.pendingUserInput ||
|
|
1382
|
+
slotState.status === "awaiting_input") {
|
|
1383
|
+
await routeCurrentMessage(slot, command);
|
|
1169
1384
|
return;
|
|
1170
1385
|
}
|
|
1171
1386
|
const visibleResult = await this.ensureSlot(slot.adapter, {
|
|
@@ -1177,21 +1392,8 @@ class WechatDaemon {
|
|
|
1177
1392
|
return;
|
|
1178
1393
|
}
|
|
1179
1394
|
slot = this.getActiveSlot() ?? slot;
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
// Belt-and-braces with the pendingUserInput mirror above: the adapter
|
|
1183
|
-
// state is authoritative even when the mirror was cleared or the WeChat
|
|
1184
|
-
// notification is still queued.
|
|
1185
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, slot.pendingUserInput
|
|
1186
|
-
? formatPendingUserInputReminder(slot.pendingUserInput)
|
|
1187
|
-
: `${slot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
if (adapterState.status === "busy" || adapterState.status === "awaiting_approval") {
|
|
1191
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
await this.dispatchInboundWechatText(message, slot);
|
|
1395
|
+
this.bindCurrentWecomConversation(slot);
|
|
1396
|
+
await routeCurrentMessage(slot, null);
|
|
1195
1397
|
}
|
|
1196
1398
|
async handleEmojiBindingsCommand(senderId, cmd) {
|
|
1197
1399
|
switch (cmd.type) {
|
|
@@ -1220,7 +1422,7 @@ class WechatDaemon {
|
|
|
1220
1422
|
return;
|
|
1221
1423
|
case "resume": {
|
|
1222
1424
|
if (!isWechatResumeEnabled(activeSlot.adapter)) {
|
|
1223
|
-
await this.queueWechatMessage(message.senderId,
|
|
1425
|
+
await this.queueWechatMessage(message.senderId, `${this.channelId === "wecom" ? "WeCom" : "WeChat"} /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; the remote channel will follow that local session.`);
|
|
1224
1426
|
return;
|
|
1225
1427
|
}
|
|
1226
1428
|
try {
|
|
@@ -1384,7 +1586,9 @@ class WechatDaemon {
|
|
|
1384
1586
|
inputPreview: truncatePreview(preview, 180),
|
|
1385
1587
|
};
|
|
1386
1588
|
appendDaemonLog(`forwarded_input: adapter=${slot.adapter} text=${truncatePreview(preview)}`);
|
|
1387
|
-
await slot.runtime.sendInput(
|
|
1589
|
+
await slot.runtime.sendInput(this.channelId === "wecom"
|
|
1590
|
+
? buildWecomInboundPrompt(message.text, message.attachments)
|
|
1591
|
+
: buildWechatInboundPrompt(message.text, message.attachments));
|
|
1388
1592
|
}
|
|
1389
1593
|
queueWechatTextAction(action) {
|
|
1390
1594
|
const run = this.textSendChain.then(action);
|
|
@@ -1396,39 +1600,110 @@ class WechatDaemon {
|
|
|
1396
1600
|
this.attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
1397
1601
|
return run;
|
|
1398
1602
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1603
|
+
async sendWechatMessageNow(senderId, text, context = "message", targetOverride) {
|
|
1604
|
+
if (this.channelId === "wecom") {
|
|
1605
|
+
const activeSlot = this.getActiveSlot();
|
|
1606
|
+
const target = targetOverride ??
|
|
1607
|
+
(senderId === this.authorizedUserId
|
|
1608
|
+
? (this.handlingInboundMessage
|
|
1609
|
+
? this.currentInboundConversation
|
|
1610
|
+
: null) ??
|
|
1611
|
+
activeSlot?.activeConversation ??
|
|
1612
|
+
activeSlot?.lastConversation ??
|
|
1613
|
+
this.currentInboundConversation
|
|
1614
|
+
: {
|
|
1615
|
+
channelId: "wecom",
|
|
1616
|
+
conversationId: senderId,
|
|
1617
|
+
recipientId: senderId,
|
|
1618
|
+
metadata: { chatType: "direct" },
|
|
1619
|
+
});
|
|
1620
|
+
const startedAtMs = Date.now();
|
|
1621
|
+
appendDaemonLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
|
|
1622
|
+
try {
|
|
1623
|
+
await this.wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
|
|
1624
|
+
appendDaemonLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
1625
|
+
return { status: "sent" };
|
|
1626
|
+
}
|
|
1627
|
+
catch (error) {
|
|
1628
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1629
|
+
appendDaemonLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
|
|
1630
|
+
logError(`Failed to send WeCom ${context}: ${message}`);
|
|
1631
|
+
return { status: "context_unavailable", error, target };
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
const startedAtMs = Date.now();
|
|
1635
|
+
appendDaemonLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
1636
|
+
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
1637
|
+
try {
|
|
1638
|
+
await this.transport.sendText(senderId, text);
|
|
1639
|
+
appendDaemonLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
1640
|
+
return { status: "sent" };
|
|
1641
|
+
}
|
|
1642
|
+
catch (error) {
|
|
1643
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1644
|
+
appendDaemonLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
1405
1645
|
}
|
|
1406
|
-
|
|
1646
|
+
if (isWechatContextUnavailableError(error)) {
|
|
1407
1647
|
if (isWechatContextTokenStaleError(error)) {
|
|
1408
1648
|
this.transport.clearCachedContextToken(senderId);
|
|
1409
|
-
|
|
1649
|
+
}
|
|
1650
|
+
appendDaemonLog(isWechatContextTokenStaleError(error)
|
|
1651
|
+
? formatWechatContextTokenStaleLogEntry({
|
|
1652
|
+
context,
|
|
1653
|
+
recipientId: senderId,
|
|
1654
|
+
error,
|
|
1655
|
+
})
|
|
1656
|
+
: formatWechatSendFailureLogEntry({
|
|
1410
1657
|
context,
|
|
1411
1658
|
recipientId: senderId,
|
|
1412
1659
|
error,
|
|
1413
1660
|
}));
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
|
|
1423
|
-
appendDaemonLog(formatWechatSendFailureLogEntry({
|
|
1424
|
-
context,
|
|
1425
|
-
recipientId: senderId,
|
|
1426
|
-
error,
|
|
1427
|
-
}));
|
|
1428
|
-
return false;
|
|
1661
|
+
return { status: "context_unavailable", error };
|
|
1662
|
+
}
|
|
1663
|
+
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(error)) {
|
|
1664
|
+
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
1665
|
+
appendDaemonLog(`wechat_send_retry: context=${context} recipient=${senderId} attempt=${attempt} delay_ms=${delayMs} error=${truncatePreview(describeWechatTransportError(error), 400)}`);
|
|
1666
|
+
await delay(delayMs);
|
|
1667
|
+
continue;
|
|
1429
1668
|
}
|
|
1669
|
+
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
|
|
1670
|
+
appendDaemonLog(formatWechatSendFailureLogEntry({
|
|
1671
|
+
context,
|
|
1672
|
+
recipientId: senderId,
|
|
1673
|
+
error,
|
|
1674
|
+
}));
|
|
1675
|
+
return { status: "failed" };
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return { status: "failed" };
|
|
1679
|
+
}
|
|
1680
|
+
queueWechatMessage(senderId, text, context = "message", targetOverride) {
|
|
1681
|
+
return this.queueWechatTextAction(async () => {
|
|
1682
|
+
const result = await this.sendWechatMessageNow(senderId, text, context, targetOverride);
|
|
1683
|
+
if (result.status === "context_unavailable") {
|
|
1684
|
+
const pending = this.pendingWechatMessages.enqueue(senderId, text, context, result.target);
|
|
1685
|
+
if (pending) {
|
|
1686
|
+
appendDaemonLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${this.pendingWechatMessages.list().length}`);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return result.status === "sent";
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
flushPendingWechatMessages() {
|
|
1693
|
+
return this.queueWechatTextAction(async () => {
|
|
1694
|
+
for (const pending of this.pendingWechatMessages.list()) {
|
|
1695
|
+
const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
|
|
1696
|
+
if (result.status === "sent") {
|
|
1697
|
+
this.pendingWechatMessages.remove(pending.id);
|
|
1698
|
+
appendDaemonLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
if (result.status === "context_unavailable") {
|
|
1702
|
+
break;
|
|
1703
|
+
}
|
|
1704
|
+
appendDaemonLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
1705
|
+
break;
|
|
1430
1706
|
}
|
|
1431
|
-
return false;
|
|
1432
1707
|
});
|
|
1433
1708
|
}
|
|
1434
1709
|
trackWechatForwardTask(task) {
|
|
@@ -1728,7 +2003,9 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1728
2003
|
return { action: "stopped", lock, forced };
|
|
1729
2004
|
}
|
|
1730
2005
|
export async function runDaemon(options) {
|
|
1731
|
-
|
|
2006
|
+
if ((options.channelId ?? "wechat") === "wechat") {
|
|
2007
|
+
migrateLegacyChannelFiles((message) => log(message));
|
|
2008
|
+
}
|
|
1732
2009
|
loadEmojiBindings();
|
|
1733
2010
|
await cleanupDaemonBeforeStart({ cwd: options.cwd });
|
|
1734
2011
|
const cleanupResult = await cleanupSingleBridgeBeforeDaemon();
|
|
@@ -1744,19 +2021,36 @@ export async function runDaemon(options) {
|
|
|
1744
2021
|
if (reapedOpencodePids.length > 0) {
|
|
1745
2022
|
log(`Cleaned ${reapedOpencodePids.length} orphaned OpenCode process(es): ${reapedOpencodePids.join(", ")}`);
|
|
1746
2023
|
}
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2024
|
+
const wecomAccount = options.channelId === "wecom"
|
|
2025
|
+
? await ensureWecomAccount({ log })
|
|
2026
|
+
: null;
|
|
2027
|
+
const credentials = wecomAccount
|
|
2028
|
+
? { userId: wecomAccount.operatorUserId }
|
|
2029
|
+
: await ensureWechatCredentials({
|
|
2030
|
+
requireUserId: true,
|
|
2031
|
+
validateExisting: true,
|
|
2032
|
+
log,
|
|
2033
|
+
});
|
|
1752
2034
|
if (!credentials.userId) {
|
|
1753
|
-
throw new Error(
|
|
2035
|
+
throw new Error(options.channelId === "wecom"
|
|
2036
|
+
? "Saved WeCom credentials are missing operatorUserId."
|
|
2037
|
+
: "Saved WeChat credentials are missing userId.");
|
|
1754
2038
|
}
|
|
1755
2039
|
const daemon = new WechatDaemon({
|
|
1756
2040
|
cwd: options.cwd,
|
|
1757
2041
|
profile: options.profile,
|
|
1758
2042
|
authorizedUserId: credentials.userId,
|
|
1759
2043
|
transport: new WeChatTransport({ log, logError }),
|
|
2044
|
+
channelId: options.channelId,
|
|
2045
|
+
wecomTransport: wecomAccount
|
|
2046
|
+
? new WecomTransport({
|
|
2047
|
+
account: wecomAccount,
|
|
2048
|
+
logger: {
|
|
2049
|
+
log: (message) => log(message),
|
|
2050
|
+
error: (message) => logError(message),
|
|
2051
|
+
},
|
|
2052
|
+
})
|
|
2053
|
+
: null,
|
|
1760
2054
|
});
|
|
1761
2055
|
if (cleanupResult.action === "stopped" && isDaemonAdapterKind(cleanupResult.lock.adapter)) {
|
|
1762
2056
|
daemon.takenOverAdapter = cleanupResult.lock.adapter;
|
|
@@ -1806,7 +2100,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1806
2100
|
process.exit(1);
|
|
1807
2101
|
}
|
|
1808
2102
|
}
|
|
1809
|
-
const isDirectRun =
|
|
2103
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
1810
2104
|
if (isDirectRun) {
|
|
1811
2105
|
void main();
|
|
1812
2106
|
}
|