cli-wechat-bridge 1.1.6 → 1.1.8
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/LICENSE.txt +661 -661
- package/README.md +454 -475
- package/bin/_run-entry.mjs +23 -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/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +155 -4
- package/dist/bridge/bridge-adapters.codex.js +112 -7
- package/dist/bridge/bridge-adapters.core.js +19 -1
- package/dist/bridge/bridge-adapters.opencode.js +150 -1
- package/dist/bridge/bridge-adapters.pi.js +39 -0
- package/dist/bridge/bridge-process-reaper.js +35 -2
- package/dist/bridge/bridge-state.js +10 -3
- package/dist/bridge/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +253 -0
- package/dist/bridge/wechat-bridge.js +389 -87
- package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
- 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 +24 -3
- package/dist/companion/codex-visible-client-link.js +101 -0
- package/dist/companion/local-companion-start.js +56 -11
- package/dist/companion/local-companion.js +19 -1
- package/dist/companion/pi-tui-bridge-extension.js +56 -3
- package/dist/core/bridge-control.js +8 -0
- package/dist/core/channel-types.js +3 -1
- package/dist/core/conversation-routing.js +48 -0
- package/dist/core/direct-run.js +17 -0
- package/dist/core/inbound-message-claims.js +61 -0
- package/dist/daemon/daemon-link.js +3 -0
- package/dist/daemon/wechat-daemon.js +335 -60
- package/dist/i18n/messages-en.js +3 -1
- package/dist/i18n/messages-zh.js +3 -1
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/doctor.js +30 -10
- package/dist/utils/version-checker.js +11 -11
- package/dist/wechat/setup.js +113 -13
- package/dist/wechat/wechat-transport.js +96 -58
- package/package.json +116 -104
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -17,12 +17,19 @@ import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataD
|
|
|
17
17
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
18
18
|
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
|
|
19
19
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
|
|
20
|
-
import { getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
20
|
+
import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
21
21
|
import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
|
|
22
22
|
import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
|
|
23
23
|
import { routeBridgeMessage } from "../core/bridge-message-router.js";
|
|
24
|
+
import { clearTurn, InboundConversationContext, resolveConversationTarget, rollbackTurn, tryBeginTurn, } from "../core/conversation-routing.js";
|
|
25
|
+
import { handleAdapterControl, invalidateModelSnapshot } from "../bridge/adapter-control.js";
|
|
24
26
|
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
27
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
25
28
|
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
29
|
+
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
30
|
+
import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
|
|
31
|
+
import { WecomTransport } from "../channels/wecom/wecom-transport.js";
|
|
32
|
+
import { buildWecomInboundPrompt, formatWecomVisibleText, } from "../channels/wecom/wecom-message.js";
|
|
26
33
|
import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
27
34
|
import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
|
|
28
35
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
@@ -40,11 +47,15 @@ const DAEMON_TAKEOVER_STOP_POLL_MS = 250;
|
|
|
40
47
|
const VISIBLE_CLIENT_CONNECT_TIMEOUT_MS = 15_000;
|
|
41
48
|
const VISIBLE_CLIENT_CONNECT_POLL_MS = 250;
|
|
42
49
|
const DAEMON_ADAPTERS = ["codex", "claude", "opencode", "pi"];
|
|
50
|
+
function getCliChannelId(argv = process.argv.slice(2)) {
|
|
51
|
+
const index = argv.indexOf("--channel");
|
|
52
|
+
return index >= 0 && argv[index + 1] === "wecom" ? "wecom" : "wechat";
|
|
53
|
+
}
|
|
43
54
|
function log(message) {
|
|
44
|
-
process.stderr.write(`[
|
|
55
|
+
process.stderr.write(`[${getCliChannelId()}-daemon] ${message}\n`);
|
|
45
56
|
}
|
|
46
57
|
function logError(message) {
|
|
47
|
-
process.stderr.write(`[
|
|
58
|
+
process.stderr.write(`[${getCliChannelId()}-daemon] ERROR: ${message}\n`);
|
|
48
59
|
}
|
|
49
60
|
function appendDaemonLog(message) {
|
|
50
61
|
ensureChannelDataDir();
|
|
@@ -55,6 +66,48 @@ function computePollRetryDelayMs(consecutiveFailures) {
|
|
|
55
66
|
const exponent = Math.min(normalizedFailures - 1, 5);
|
|
56
67
|
return Math.min(POLL_RETRY_MAX_MS, POLL_RETRY_BASE_MS * 2 ** exponent);
|
|
57
68
|
}
|
|
69
|
+
function toWecomOutputKind(context) {
|
|
70
|
+
switch (context) {
|
|
71
|
+
case "notice":
|
|
72
|
+
case "thinking":
|
|
73
|
+
case "final_reply":
|
|
74
|
+
case "approval_required":
|
|
75
|
+
case "user_input_required":
|
|
76
|
+
case "task_failed":
|
|
77
|
+
case "fatal_error":
|
|
78
|
+
return context;
|
|
79
|
+
case "mirrored_user_input":
|
|
80
|
+
return "mirrored_input";
|
|
81
|
+
default:
|
|
82
|
+
return "notice";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function toWechatSendContext(kind) {
|
|
86
|
+
if (kind === "mirrored_input") {
|
|
87
|
+
return "mirrored_user_input";
|
|
88
|
+
}
|
|
89
|
+
if (kind === "status") {
|
|
90
|
+
return "message";
|
|
91
|
+
}
|
|
92
|
+
return kind;
|
|
93
|
+
}
|
|
94
|
+
function toLegacyInboundWechatMessage(message) {
|
|
95
|
+
return {
|
|
96
|
+
senderId: message.senderId,
|
|
97
|
+
sender: message.senderId,
|
|
98
|
+
sessionId: message.conversation.conversationId,
|
|
99
|
+
text: message.text,
|
|
100
|
+
attachments: message.attachments.map((attachment) => ({
|
|
101
|
+
kind: attachment.kind === "image" ? "image" : "file",
|
|
102
|
+
path: attachment.path,
|
|
103
|
+
fileName: attachment.fileName || path.basename(attachment.path),
|
|
104
|
+
sizeBytes: attachment.sizeBytes ?? 0,
|
|
105
|
+
})),
|
|
106
|
+
contextToken: message.conversation.opaqueRef,
|
|
107
|
+
createdAt: message.createdAt,
|
|
108
|
+
createdAtMs: Date.parse(message.createdAt),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
58
111
|
function isDaemonAdapterKind(value) {
|
|
59
112
|
return value === "codex" || value === "claude" || value === "opencode" || value === "pi";
|
|
60
113
|
}
|
|
@@ -76,6 +129,7 @@ export function parseDaemonCliArgs(argv) {
|
|
|
76
129
|
let profile;
|
|
77
130
|
let initialAdapter;
|
|
78
131
|
let openVisible = true;
|
|
132
|
+
let channelId = "wechat";
|
|
79
133
|
for (let i = 0; i < argv.length; i += 1) {
|
|
80
134
|
const arg = argv[i];
|
|
81
135
|
if (!arg) {
|
|
@@ -84,10 +138,10 @@ export function parseDaemonCliArgs(argv) {
|
|
|
84
138
|
const next = argv[i + 1];
|
|
85
139
|
if (arg === "--help" || arg === "-h") {
|
|
86
140
|
process.stdout.write([
|
|
87
|
-
|
|
141
|
+
`Usage: ${channelId}-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]`,
|
|
88
142
|
"",
|
|
89
|
-
|
|
90
|
-
|
|
143
|
+
`Keeps one ${channelId === "wecom" ? "WeCom" : "WeChat"} connection alive and switches between Codex, Claude Code, OpenCode, and Pi.`,
|
|
144
|
+
`Send /codex, /claude, /opencode, or /pi in ${channelId === "wecom" ? "WeCom" : "WeChat"} to switch the active terminal.`,
|
|
91
145
|
"",
|
|
92
146
|
].join("\n"));
|
|
93
147
|
process.exit(0);
|
|
@@ -100,6 +154,14 @@ export function parseDaemonCliArgs(argv) {
|
|
|
100
154
|
i += 1;
|
|
101
155
|
continue;
|
|
102
156
|
}
|
|
157
|
+
if (arg === "--channel") {
|
|
158
|
+
if (!next || (next !== "wechat" && next !== "wecom")) {
|
|
159
|
+
throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
|
|
160
|
+
}
|
|
161
|
+
channelId = next;
|
|
162
|
+
i += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
103
165
|
if (arg === "--adapter") {
|
|
104
166
|
if (!isDaemonAdapterKind(next)) {
|
|
105
167
|
throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
|
|
@@ -122,7 +184,13 @@ export function parseDaemonCliArgs(argv) {
|
|
|
122
184
|
}
|
|
123
185
|
throw new Error(`Unknown argument: ${arg}`);
|
|
124
186
|
}
|
|
125
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
cwd,
|
|
189
|
+
profile,
|
|
190
|
+
initialAdapter,
|
|
191
|
+
openVisible,
|
|
192
|
+
...(channelId === "wecom" ? { channelId } : {}),
|
|
193
|
+
};
|
|
126
194
|
}
|
|
127
195
|
export function parseDaemonSwitchCommand(text) {
|
|
128
196
|
const normalized = text.trim().toLowerCase();
|
|
@@ -151,8 +219,13 @@ export function parseDaemonSwitchDirective(text) {
|
|
|
151
219
|
remainder: match[2]?.trim() ?? "",
|
|
152
220
|
};
|
|
153
221
|
}
|
|
154
|
-
export function defaultDaemonSessionStartMode(
|
|
155
|
-
|
|
222
|
+
export function defaultDaemonSessionStartMode(_adapter) {
|
|
223
|
+
// Codex 0.155+ persists app-server threads lazily, so resuming a freshly
|
|
224
|
+
// prepared (not yet persisted) thread crashes the visible TUI bootstrap
|
|
225
|
+
// with "no rollout found". Every adapter therefore opens a fresh session
|
|
226
|
+
// by default; explicit --session-start-mode restore and WeChat /resume
|
|
227
|
+
// remain available for deliberate restores of persisted threads.
|
|
228
|
+
return "new";
|
|
156
229
|
}
|
|
157
230
|
export function resolveDaemonSessionStartMode(params) {
|
|
158
231
|
// An explicit --session-start-mode wins over visible-client reuse: the user
|
|
@@ -165,9 +238,6 @@ export function resolveDaemonSessionStartMode(params) {
|
|
|
165
238
|
if (params.reuseExistingVisible && params.visibleConnected) {
|
|
166
239
|
return "restore";
|
|
167
240
|
}
|
|
168
|
-
if (params.adapter === "codex") {
|
|
169
|
-
return "restore";
|
|
170
|
-
}
|
|
171
241
|
if (params.slotCreated) {
|
|
172
242
|
return "new";
|
|
173
243
|
}
|
|
@@ -221,7 +291,7 @@ export function buildVisibleClientLaunchArgs(params) {
|
|
|
221
291
|
export function buildWindowsVisibleClientLaunchCommand(params) {
|
|
222
292
|
return [
|
|
223
293
|
"start",
|
|
224
|
-
quoteWindowsCommandArg(
|
|
294
|
+
quoteWindowsCommandArg(`${params.channelId ?? "wechat"}-${params.adapter}`),
|
|
225
295
|
"/D",
|
|
226
296
|
quoteWindowsCommandArg(params.cwd),
|
|
227
297
|
quoteWindowsCommandArg(process.execPath),
|
|
@@ -269,6 +339,7 @@ function openVisibleClient(params) {
|
|
|
269
339
|
adapter: params.adapter,
|
|
270
340
|
cwd: params.cwd,
|
|
271
341
|
args,
|
|
342
|
+
channelId: params.channelId,
|
|
272
343
|
}),
|
|
273
344
|
];
|
|
274
345
|
const child = spawn(command, launchArgs, {
|
|
@@ -289,13 +360,13 @@ function openVisibleClient(params) {
|
|
|
289
360
|
pid: child.pid,
|
|
290
361
|
};
|
|
291
362
|
}
|
|
292
|
-
const title =
|
|
363
|
+
const title = `${params.channelId ?? "wechat"}-${params.adapter}`;
|
|
293
364
|
const fullArgs = [process.execPath, ...args];
|
|
294
365
|
if (process.platform === "darwin") {
|
|
295
366
|
const cmdLine = fullArgs.map(shellQuotePosix).join(" ");
|
|
296
|
-
const script = `tell application "Terminal"
|
|
297
|
-
activate
|
|
298
|
-
do script "cd ${shellQuotePosix(params.cwd)} && exec ${cmdLine}"
|
|
367
|
+
const script = `tell application "Terminal"
|
|
368
|
+
activate
|
|
369
|
+
do script "cd ${shellQuotePosix(params.cwd)} && exec ${cmdLine}"
|
|
299
370
|
end tell`;
|
|
300
371
|
const child = spawn("osascript", ["-e", script], {
|
|
301
372
|
cwd: params.cwd,
|
|
@@ -486,6 +557,10 @@ class WechatDaemon {
|
|
|
486
557
|
profile;
|
|
487
558
|
authorizedUserId;
|
|
488
559
|
transport;
|
|
560
|
+
channelId;
|
|
561
|
+
wecomTransport;
|
|
562
|
+
inboundConversationContext = new InboundConversationContext();
|
|
563
|
+
fallbackConversation;
|
|
489
564
|
slots = new Map();
|
|
490
565
|
// Per-adapter serialization chains for ensureSlot (see ensureSlot comment).
|
|
491
566
|
slotEnsureChains = new Map();
|
|
@@ -506,7 +581,17 @@ class WechatDaemon {
|
|
|
506
581
|
this.profile = params.profile;
|
|
507
582
|
this.authorizedUserId = params.authorizedUserId;
|
|
508
583
|
this.transport = params.transport;
|
|
509
|
-
this.
|
|
584
|
+
this.channelId = params.channelId ?? "wechat";
|
|
585
|
+
this.wecomTransport = params.wecomTransport ?? null;
|
|
586
|
+
this.fallbackConversation = {
|
|
587
|
+
channelId: this.channelId,
|
|
588
|
+
conversationId: params.authorizedUserId,
|
|
589
|
+
recipientId: params.authorizedUserId,
|
|
590
|
+
metadata: this.channelId === "wecom" ? { chatType: "direct" } : undefined,
|
|
591
|
+
};
|
|
592
|
+
this.pendingWechatMessages = new PendingWechatMessageStore(this.channelId === "wechat"
|
|
593
|
+
? getPendingWechatMessagesFile(this.cwd)
|
|
594
|
+
: getPendingChannelMessagesFile(this.cwd, this.channelId));
|
|
510
595
|
}
|
|
511
596
|
async startIpcServer() {
|
|
512
597
|
this.endpointToken = buildDaemonToken();
|
|
@@ -554,6 +639,7 @@ class WechatDaemon {
|
|
|
554
639
|
token: this.endpointToken,
|
|
555
640
|
cwd: this.cwd,
|
|
556
641
|
startedAt: this.startedAt,
|
|
642
|
+
channelId: this.channelId,
|
|
557
643
|
});
|
|
558
644
|
resolve();
|
|
559
645
|
});
|
|
@@ -562,6 +648,7 @@ class WechatDaemon {
|
|
|
562
648
|
getStatus() {
|
|
563
649
|
return {
|
|
564
650
|
cwd: this.cwd,
|
|
651
|
+
channelId: this.channelId,
|
|
565
652
|
activeAdapter: this.activeAdapter ?? undefined,
|
|
566
653
|
startedAt: this.startedAt,
|
|
567
654
|
slots: Array.from(this.slots.values()).map((slot) => {
|
|
@@ -590,10 +677,47 @@ class WechatDaemon {
|
|
|
590
677
|
}
|
|
591
678
|
async runPollLoop() {
|
|
592
679
|
let consecutivePollFailures = 0;
|
|
593
|
-
|
|
680
|
+
if (this.wecomTransport) {
|
|
681
|
+
this.wecomTransport.setHandlers({
|
|
682
|
+
onMessage: async (channelMessage) => {
|
|
683
|
+
if (this.shutdownPromise) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
await this.inboundConversationContext.run(channelMessage.conversation, async () => {
|
|
687
|
+
if (this.pendingWechatMessages.list().length > 0) {
|
|
688
|
+
await this.flushPendingWechatMessages();
|
|
689
|
+
}
|
|
690
|
+
await this.handleInboundMessage(toLegacyInboundWechatMessage(channelMessage));
|
|
691
|
+
});
|
|
692
|
+
},
|
|
693
|
+
onUnauthorized: async (senderId, chatType) => {
|
|
694
|
+
appendDaemonLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
|
|
695
|
+
if (chatType === "direct") {
|
|
696
|
+
await this.wecomTransport.sendText({
|
|
697
|
+
channelId: "wecom",
|
|
698
|
+
conversationId: senderId,
|
|
699
|
+
recipientId: senderId,
|
|
700
|
+
metadata: { chatType: "direct" },
|
|
701
|
+
}, "Unauthorized.", "notice");
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
onFatal: async (error) => {
|
|
705
|
+
appendDaemonLog(`wecom_fatal_error: ${error.message}`);
|
|
706
|
+
await this.shutdown();
|
|
707
|
+
},
|
|
708
|
+
onConnected: async () => {
|
|
709
|
+
if (this.pendingWechatMessages.list().length > 0) {
|
|
710
|
+
await this.flushPendingWechatMessages();
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
this.wecomTransport.start();
|
|
715
|
+
await this.wecomTransport.waitUntilConnected();
|
|
716
|
+
}
|
|
717
|
+
log(`${this.channelId === "wecom" ? "WeCom" : "WeChat"} daemon is ready.`);
|
|
594
718
|
log(`Working directory: ${this.cwd}`);
|
|
595
|
-
log(
|
|
596
|
-
appendDaemonLog(`started: cwd=${this.cwd}`);
|
|
719
|
+
log(`Switch from ${this.channelId === "wecom" ? "WeCom" : "WeChat"} with /codex, /claude, /opencode, or /pi.`);
|
|
720
|
+
appendDaemonLog(`started: channel=${this.channelId} cwd=${this.cwd}`);
|
|
597
721
|
const activeSlot = this.getActiveSlot();
|
|
598
722
|
const welcomeText = t("daemon.welcome", {
|
|
599
723
|
cwd: this.cwd,
|
|
@@ -601,6 +725,13 @@ class WechatDaemon {
|
|
|
601
725
|
bindings: formatBindingsListMessage(listBindings()),
|
|
602
726
|
});
|
|
603
727
|
await this.queueWechatMessage(this.authorizedUserId, welcomeText);
|
|
728
|
+
if (this.channelId === "wecom") {
|
|
729
|
+
while (!this.shutdownPromise) {
|
|
730
|
+
await delay(1_000);
|
|
731
|
+
}
|
|
732
|
+
await this.shutdownPromise;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
604
735
|
while (!this.shutdownPromise) {
|
|
605
736
|
let pollResult;
|
|
606
737
|
try {
|
|
@@ -694,6 +825,23 @@ class WechatDaemon {
|
|
|
694
825
|
}
|
|
695
826
|
async cleanup() {
|
|
696
827
|
appendDaemonLog("shutdown_started");
|
|
828
|
+
// Stop accepting new channel work and abort an in-flight long poll before
|
|
829
|
+
// waiting for queued sends or adapter disposal. Otherwise Ctrl+C can leave
|
|
830
|
+
// the process alive until the 35-second poll timeout and retain the daemon
|
|
831
|
+
// endpoint for the next startup to clean up.
|
|
832
|
+
try {
|
|
833
|
+
this.transport.stop();
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
// Best effort shutdown.
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
this.wecomTransport?.stop();
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
// Best effort shutdown.
|
|
843
|
+
}
|
|
844
|
+
clearDaemonEndpoint();
|
|
697
845
|
for (const slot of this.slots.values()) {
|
|
698
846
|
try {
|
|
699
847
|
await slot.outputBatcher.flushNow();
|
|
@@ -736,7 +884,7 @@ class WechatDaemon {
|
|
|
736
884
|
return { shuttingDown: true };
|
|
737
885
|
case "ensure_slot":
|
|
738
886
|
if (!isSameWorkspaceCwd(request.cwd, this.cwd)) {
|
|
739
|
-
throw new Error(
|
|
887
|
+
throw new Error(`${this.channelId}-daemon is bound to ${this.cwd}; requested cwd was ${request.cwd}.`);
|
|
740
888
|
}
|
|
741
889
|
return await this.ensureSlot(request.adapter, {
|
|
742
890
|
profile: request.profile,
|
|
@@ -826,8 +974,12 @@ class WechatDaemon {
|
|
|
826
974
|
const launch = openVisibleClient({
|
|
827
975
|
adapter,
|
|
828
976
|
cwd: this.cwd,
|
|
977
|
+
// A freshly prepared blank thread is semantically a new session, but
|
|
978
|
+
// the visible client must resume its id from the endpoint instead of
|
|
979
|
+
// creating a second thread of its own.
|
|
829
980
|
sessionStartMode,
|
|
830
981
|
cliArgs: options.cliArgs,
|
|
982
|
+
channelId: this.channelId,
|
|
831
983
|
onError: (error) => {
|
|
832
984
|
appendDaemonLog(`visible_client_open_error: adapter=${adapter} error=${truncatePreview(error.message, 400)}`);
|
|
833
985
|
},
|
|
@@ -868,6 +1020,9 @@ class WechatDaemon {
|
|
|
868
1020
|
}
|
|
869
1021
|
const activated = options.openVisible === false || (visibleConnected && visibleReady);
|
|
870
1022
|
if (activated) {
|
|
1023
|
+
const previousSlot = this.getActiveSlot();
|
|
1024
|
+
if (previousSlot && this.activeAdapter !== adapter)
|
|
1025
|
+
invalidateModelSnapshot(previousSlot.runtime);
|
|
871
1026
|
this.activeAdapter = adapter;
|
|
872
1027
|
}
|
|
873
1028
|
appendDaemonLog(`switch_adapter: adapter=${adapter} created=${created} opened_visible=${openedVisible} visible_connected=${visibleConnected} visible_ready=${visibleReady} activated=${activated} previous_active=${previousActiveAdapter ?? "(none)"} session_start_mode=${sessionStartMode}`);
|
|
@@ -898,7 +1053,7 @@ class WechatDaemon {
|
|
|
898
1053
|
runtime,
|
|
899
1054
|
controller,
|
|
900
1055
|
outputBatcher: new OutputBatcher(async (text) => {
|
|
901
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(adapter, text));
|
|
1056
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(adapter, text), "message", this.resolveSlotOutputTarget(slot));
|
|
902
1057
|
}),
|
|
903
1058
|
pendingConfirmations: [],
|
|
904
1059
|
pendingUserInput: null,
|
|
@@ -924,7 +1079,7 @@ class WechatDaemon {
|
|
|
924
1079
|
slot.outputBatcher.clear();
|
|
925
1080
|
slot.pendingConfirmations = [];
|
|
926
1081
|
slot.pendingUserInput = null;
|
|
927
|
-
slot
|
|
1082
|
+
clearTurn(slot);
|
|
928
1083
|
if (slot.adapter === "codex" || slot.adapter === "claude") {
|
|
929
1084
|
await slot.runtime.reset();
|
|
930
1085
|
}
|
|
@@ -937,6 +1092,16 @@ class WechatDaemon {
|
|
|
937
1092
|
slot.controller.syncLocalClientEndpoint();
|
|
938
1093
|
}
|
|
939
1094
|
createWechatChannelPort(adapter) {
|
|
1095
|
+
if (this.channelId === "wecom") {
|
|
1096
|
+
return new WecomChannelPort({
|
|
1097
|
+
transport: this.wecomTransport,
|
|
1098
|
+
sendText: (target, text, kind) => this.queueWechatMessage(this.authorizedUserId, text, toWechatSendContext(kind), target),
|
|
1099
|
+
prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
|
|
1100
|
+
onEmptyVisibleReply: (currentAdapter, rawText) => {
|
|
1101
|
+
appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
|
|
1102
|
+
},
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
940
1105
|
return new WechatChannelPort({
|
|
941
1106
|
sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
|
|
942
1107
|
sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(filePath, { recipientId })),
|
|
@@ -956,6 +1121,10 @@ class WechatDaemon {
|
|
|
956
1121
|
slot.controller.syncLocalClientEndpoint();
|
|
957
1122
|
const adapterState = slot.runtime.getState();
|
|
958
1123
|
const channelPort = this.createWechatChannelPort(slot.adapter);
|
|
1124
|
+
const eventTarget = this.channelId === "wecom"
|
|
1125
|
+
? this.resolveSlotOutputTarget(slot)
|
|
1126
|
+
: this.fallbackConversation;
|
|
1127
|
+
const eventTask = slot.activeTask;
|
|
959
1128
|
if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
|
|
960
1129
|
slot.pendingConfirmations = [];
|
|
961
1130
|
}
|
|
@@ -981,11 +1150,7 @@ class WechatDaemon {
|
|
|
981
1150
|
appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
|
|
982
1151
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
983
1152
|
await channelPort.send({
|
|
984
|
-
target:
|
|
985
|
-
channelId: "wechat",
|
|
986
|
-
conversationId: this.authorizedUserId,
|
|
987
|
-
recipientId: this.authorizedUserId,
|
|
988
|
-
},
|
|
1153
|
+
target: eventTarget,
|
|
989
1154
|
kind: "final_reply",
|
|
990
1155
|
text: next.text,
|
|
991
1156
|
adapter: slot.adapter,
|
|
@@ -1003,7 +1168,7 @@ class WechatDaemon {
|
|
|
1003
1168
|
appendDaemonLog(`${slot.adapter}_${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
1004
1169
|
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
|
|
1005
1170
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1006
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice");
|
|
1171
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice", eventTarget);
|
|
1007
1172
|
}));
|
|
1008
1173
|
}
|
|
1009
1174
|
},
|
|
@@ -1012,7 +1177,7 @@ class WechatDaemon {
|
|
|
1012
1177
|
const pending = toPendingApproval(next);
|
|
1013
1178
|
slot.pendingConfirmations.push(pending);
|
|
1014
1179
|
appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
|
|
1015
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
|
|
1180
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required", eventTarget);
|
|
1016
1181
|
}));
|
|
1017
1182
|
},
|
|
1018
1183
|
userInputRequired: (next) => {
|
|
@@ -1020,14 +1185,14 @@ class WechatDaemon {
|
|
|
1020
1185
|
slot.pendingUserInput = pending;
|
|
1021
1186
|
appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
|
|
1022
1187
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1023
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
|
|
1188
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required", eventTarget);
|
|
1024
1189
|
}));
|
|
1025
1190
|
},
|
|
1026
1191
|
mirroredUserInput: (next) => {
|
|
1027
1192
|
appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
|
|
1028
1193
|
if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
|
|
1029
1194
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1030
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input");
|
|
1195
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input", eventTarget);
|
|
1031
1196
|
}));
|
|
1032
1197
|
}
|
|
1033
1198
|
},
|
|
@@ -1037,7 +1202,7 @@ class WechatDaemon {
|
|
|
1037
1202
|
appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
1038
1203
|
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1039
1204
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1040
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched");
|
|
1205
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched", eventTarget);
|
|
1041
1206
|
}));
|
|
1042
1207
|
}
|
|
1043
1208
|
},
|
|
@@ -1055,7 +1220,7 @@ class WechatDaemon {
|
|
|
1055
1220
|
}
|
|
1056
1221
|
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
|
|
1057
1222
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1058
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched");
|
|
1223
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched", eventTarget);
|
|
1059
1224
|
}));
|
|
1060
1225
|
}
|
|
1061
1226
|
},
|
|
@@ -1063,15 +1228,19 @@ class WechatDaemon {
|
|
|
1063
1228
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
|
|
1064
1229
|
slot.pendingConfirmations = [];
|
|
1065
1230
|
slot.pendingUserInput = null;
|
|
1066
|
-
|
|
1231
|
+
if (eventTask) {
|
|
1232
|
+
clearTurn(slot, eventTask);
|
|
1233
|
+
}
|
|
1067
1234
|
}));
|
|
1068
1235
|
},
|
|
1069
1236
|
taskFailed: (next) => {
|
|
1070
1237
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1071
1238
|
slot.pendingConfirmations = [];
|
|
1072
1239
|
slot.pendingUserInput = null;
|
|
1073
|
-
|
|
1074
|
-
|
|
1240
|
+
if (eventTask) {
|
|
1241
|
+
clearTurn(slot, eventTask);
|
|
1242
|
+
}
|
|
1243
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed", eventTarget);
|
|
1075
1244
|
}));
|
|
1076
1245
|
},
|
|
1077
1246
|
fatalError: (next) => {
|
|
@@ -1079,10 +1248,12 @@ class WechatDaemon {
|
|
|
1079
1248
|
appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${next.message}`);
|
|
1080
1249
|
slot.pendingConfirmations = [];
|
|
1081
1250
|
slot.pendingUserInput = null;
|
|
1082
|
-
|
|
1251
|
+
if (eventTask) {
|
|
1252
|
+
clearTurn(slot, eventTask);
|
|
1253
|
+
}
|
|
1083
1254
|
this.disposeDeadSlot(slot);
|
|
1084
1255
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1085
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
|
|
1256
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error", eventTarget);
|
|
1086
1257
|
}));
|
|
1087
1258
|
},
|
|
1088
1259
|
shutdownRequested: (next) => {
|
|
@@ -1094,9 +1265,25 @@ class WechatDaemon {
|
|
|
1094
1265
|
appendDaemonLog(`event_forward_failed: adapter=${slot.adapter} message=${message}`);
|
|
1095
1266
|
});
|
|
1096
1267
|
}
|
|
1268
|
+
resolveSlotOutputTarget(slot) {
|
|
1269
|
+
return resolveConversationTarget({
|
|
1270
|
+
active: slot.activeConversation,
|
|
1271
|
+
last: slot.lastConversation,
|
|
1272
|
+
fallback: this.fallbackConversation,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
bindCurrentWecomConversation(slot) {
|
|
1276
|
+
const conversation = this.inboundConversationContext.get();
|
|
1277
|
+
if (this.channelId !== "wecom" || !slot || !conversation) {
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
slot.lastConversation = conversation;
|
|
1281
|
+
}
|
|
1097
1282
|
async handleInboundMessage(message) {
|
|
1098
1283
|
if (message.senderId !== this.authorizedUserId) {
|
|
1099
|
-
await this.queueWechatMessage(message.senderId,
|
|
1284
|
+
await this.queueWechatMessage(message.senderId, this.channelId === "wecom"
|
|
1285
|
+
? "Unauthorized. This daemon only accepts messages from the paired WeCom operator."
|
|
1286
|
+
: "Unauthorized. This daemon only accepts messages from the configured WeChat owner.");
|
|
1100
1287
|
return;
|
|
1101
1288
|
}
|
|
1102
1289
|
const emojiMatch = resolveEmojiCommand(message.text);
|
|
@@ -1108,6 +1295,7 @@ class WechatDaemon {
|
|
|
1108
1295
|
reuseExistingVisible: true,
|
|
1109
1296
|
});
|
|
1110
1297
|
if (result.activated) {
|
|
1298
|
+
this.bindCurrentWecomConversation(this.getActiveSlot());
|
|
1111
1299
|
message = { ...message, text: emojiMatch.remainder };
|
|
1112
1300
|
}
|
|
1113
1301
|
else {
|
|
@@ -1134,6 +1322,7 @@ class WechatDaemon {
|
|
|
1134
1322
|
await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchDirective.adapter}.\n${detail}`);
|
|
1135
1323
|
return;
|
|
1136
1324
|
}
|
|
1325
|
+
this.bindCurrentWecomConversation(this.getActiveSlot());
|
|
1137
1326
|
if (switchDirective.remainder) {
|
|
1138
1327
|
message = { ...message, text: switchDirective.remainder };
|
|
1139
1328
|
}
|
|
@@ -1143,7 +1332,7 @@ class WechatDaemon {
|
|
|
1143
1332
|
}
|
|
1144
1333
|
}
|
|
1145
1334
|
if (message.text.trim().toLowerCase() === "/daemon-stop") {
|
|
1146
|
-
await this.queueWechatMessage(message.senderId,
|
|
1335
|
+
await this.queueWechatMessage(message.senderId, `Stopping ${this.channelId}-daemon...`);
|
|
1147
1336
|
setTimeout(() => {
|
|
1148
1337
|
void this.shutdown().finally(() => process.exit(0));
|
|
1149
1338
|
}, 0);
|
|
@@ -1163,7 +1352,9 @@ class WechatDaemon {
|
|
|
1163
1352
|
await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
|
|
1164
1353
|
return;
|
|
1165
1354
|
}
|
|
1355
|
+
this.bindCurrentWecomConversation(slot);
|
|
1166
1356
|
const routeCurrentMessage = async (currentSlot, command) => {
|
|
1357
|
+
const previousTask = currentSlot.activeTask;
|
|
1167
1358
|
await routeBridgeMessage({
|
|
1168
1359
|
message: toChannelInboundMessage(message),
|
|
1169
1360
|
authorized: true,
|
|
@@ -1192,6 +1383,14 @@ class WechatDaemon {
|
|
|
1192
1383
|
await this.dispatchInboundWechatText(message, currentSlot);
|
|
1193
1384
|
},
|
|
1194
1385
|
});
|
|
1386
|
+
if (this.channelId === "wecom" &&
|
|
1387
|
+
this.inboundConversationContext.get() &&
|
|
1388
|
+
currentSlot.activeTask &&
|
|
1389
|
+
currentSlot.activeTask !== previousTask) {
|
|
1390
|
+
const conversation = this.inboundConversationContext.get();
|
|
1391
|
+
currentSlot.activeConversation = conversation;
|
|
1392
|
+
currentSlot.lastConversation = conversation;
|
|
1393
|
+
}
|
|
1195
1394
|
};
|
|
1196
1395
|
const command = parseWechatControlCommand(message.text, {
|
|
1197
1396
|
adapter: slot.adapter,
|
|
@@ -1215,6 +1414,7 @@ class WechatDaemon {
|
|
|
1215
1414
|
return;
|
|
1216
1415
|
}
|
|
1217
1416
|
slot = this.getActiveSlot() ?? slot;
|
|
1417
|
+
this.bindCurrentWecomConversation(slot);
|
|
1218
1418
|
await routeCurrentMessage(slot, null);
|
|
1219
1419
|
}
|
|
1220
1420
|
async handleEmojiBindingsCommand(senderId, cmd) {
|
|
@@ -1239,12 +1439,16 @@ class WechatDaemon {
|
|
|
1239
1439
|
}
|
|
1240
1440
|
async handleSystemCommand(message, activeSlot, command) {
|
|
1241
1441
|
switch (command.type) {
|
|
1442
|
+
case "model":
|
|
1443
|
+
case "plan":
|
|
1444
|
+
await this.queueWechatMessage(message.senderId, await handleAdapterControl(activeSlot.runtime, message.senderId, command));
|
|
1445
|
+
return;
|
|
1242
1446
|
case "status":
|
|
1243
1447
|
await this.queueWechatMessage(message.senderId, formatDaemonStatus(this.getStatus()));
|
|
1244
1448
|
return;
|
|
1245
1449
|
case "resume": {
|
|
1246
1450
|
if (!isWechatResumeEnabled(activeSlot.adapter)) {
|
|
1247
|
-
await this.queueWechatMessage(message.senderId,
|
|
1451
|
+
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.`);
|
|
1248
1452
|
return;
|
|
1249
1453
|
}
|
|
1250
1454
|
try {
|
|
@@ -1262,9 +1466,10 @@ class WechatDaemon {
|
|
|
1262
1466
|
if (command.target) {
|
|
1263
1467
|
await resumeSlot.outputBatcher.flushNow();
|
|
1264
1468
|
}
|
|
1469
|
+
const taskBeforeResume = resumeSlot.activeTask;
|
|
1265
1470
|
const result = await resumeSlot.resumeCoordinator.execute(command.target);
|
|
1266
|
-
if (result.kind === "resumed") {
|
|
1267
|
-
resumeSlot
|
|
1471
|
+
if (result.kind === "resumed" && taskBeforeResume) {
|
|
1472
|
+
clearTurn(resumeSlot, taskBeforeResume);
|
|
1268
1473
|
}
|
|
1269
1474
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(resumeSlot.adapter, result.message));
|
|
1270
1475
|
}
|
|
@@ -1403,12 +1608,28 @@ class WechatDaemon {
|
|
|
1403
1608
|
}
|
|
1404
1609
|
async dispatchInboundWechatText(message, slot) {
|
|
1405
1610
|
const preview = formatInboundMessagePreview(message);
|
|
1406
|
-
|
|
1611
|
+
const nextTask = {
|
|
1407
1612
|
startedAt: Date.now(),
|
|
1408
1613
|
inputPreview: truncatePreview(preview, 180),
|
|
1409
1614
|
};
|
|
1615
|
+
const inboundConversation = this.channelId === "wecom"
|
|
1616
|
+
? this.inboundConversationContext.get()
|
|
1617
|
+
: undefined;
|
|
1618
|
+
const lease = tryBeginTurn(slot, nextTask, inboundConversation);
|
|
1619
|
+
if (!lease) {
|
|
1620
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1410
1623
|
appendDaemonLog(`forwarded_input: adapter=${slot.adapter} text=${truncatePreview(preview)}`);
|
|
1411
|
-
|
|
1624
|
+
try {
|
|
1625
|
+
await slot.runtime.sendInput(this.channelId === "wecom"
|
|
1626
|
+
? buildWecomInboundPrompt(message.text, message.attachments)
|
|
1627
|
+
: buildWechatInboundPrompt(message.text, message.attachments));
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
rollbackTurn(slot, lease);
|
|
1631
|
+
throw error;
|
|
1632
|
+
}
|
|
1412
1633
|
}
|
|
1413
1634
|
queueWechatTextAction(action) {
|
|
1414
1635
|
const run = this.textSendChain.then(action);
|
|
@@ -1420,7 +1641,33 @@ class WechatDaemon {
|
|
|
1420
1641
|
this.attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
1421
1642
|
return run;
|
|
1422
1643
|
}
|
|
1423
|
-
async sendWechatMessageNow(senderId, text, context = "message") {
|
|
1644
|
+
async sendWechatMessageNow(senderId, text, context = "message", targetOverride) {
|
|
1645
|
+
if (this.channelId === "wecom") {
|
|
1646
|
+
const activeSlot = this.getActiveSlot();
|
|
1647
|
+
const target = targetOverride ??
|
|
1648
|
+
(senderId === this.authorizedUserId
|
|
1649
|
+
? this.inboundConversationContext.get() ??
|
|
1650
|
+
(activeSlot ? this.resolveSlotOutputTarget(activeSlot) : this.fallbackConversation)
|
|
1651
|
+
: {
|
|
1652
|
+
channelId: "wecom",
|
|
1653
|
+
conversationId: senderId,
|
|
1654
|
+
recipientId: senderId,
|
|
1655
|
+
metadata: { chatType: "direct" },
|
|
1656
|
+
});
|
|
1657
|
+
const startedAtMs = Date.now();
|
|
1658
|
+
appendDaemonLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
|
|
1659
|
+
try {
|
|
1660
|
+
await this.wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
|
|
1661
|
+
appendDaemonLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
1662
|
+
return { status: "sent" };
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1666
|
+
appendDaemonLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
|
|
1667
|
+
logError(`Failed to send WeCom ${context}: ${message}`);
|
|
1668
|
+
return { status: "context_unavailable", error, target };
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1424
1671
|
const startedAtMs = Date.now();
|
|
1425
1672
|
appendDaemonLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
1426
1673
|
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
@@ -1467,11 +1714,17 @@ class WechatDaemon {
|
|
|
1467
1714
|
}
|
|
1468
1715
|
return { status: "failed" };
|
|
1469
1716
|
}
|
|
1470
|
-
queueWechatMessage(senderId, text, context = "message") {
|
|
1717
|
+
queueWechatMessage(senderId, text, context = "message", targetOverride) {
|
|
1718
|
+
const activeSlot = this.getActiveSlot();
|
|
1719
|
+
const queuedTarget = this.channelId === "wecom" && senderId === this.authorizedUserId
|
|
1720
|
+
? targetOverride ??
|
|
1721
|
+
this.inboundConversationContext.get() ??
|
|
1722
|
+
(activeSlot ? this.resolveSlotOutputTarget(activeSlot) : this.fallbackConversation)
|
|
1723
|
+
: targetOverride;
|
|
1471
1724
|
return this.queueWechatTextAction(async () => {
|
|
1472
|
-
const result = await this.sendWechatMessageNow(senderId, text, context);
|
|
1725
|
+
const result = await this.sendWechatMessageNow(senderId, text, context, queuedTarget);
|
|
1473
1726
|
if (result.status === "context_unavailable") {
|
|
1474
|
-
const pending = this.pendingWechatMessages.enqueue(senderId, text, context);
|
|
1727
|
+
const pending = this.pendingWechatMessages.enqueue(senderId, text, context, result.target);
|
|
1475
1728
|
if (pending) {
|
|
1476
1729
|
appendDaemonLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${this.pendingWechatMessages.list().length}`);
|
|
1477
1730
|
}
|
|
@@ -1482,7 +1735,7 @@ class WechatDaemon {
|
|
|
1482
1735
|
flushPendingWechatMessages() {
|
|
1483
1736
|
return this.queueWechatTextAction(async () => {
|
|
1484
1737
|
for (const pending of this.pendingWechatMessages.list()) {
|
|
1485
|
-
const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
|
|
1738
|
+
const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
|
|
1486
1739
|
if (result.status === "sent") {
|
|
1487
1740
|
this.pendingWechatMessages.remove(pending.id);
|
|
1488
1741
|
appendDaemonLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
@@ -1793,7 +2046,9 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1793
2046
|
return { action: "stopped", lock, forced };
|
|
1794
2047
|
}
|
|
1795
2048
|
export async function runDaemon(options) {
|
|
1796
|
-
|
|
2049
|
+
if ((options.channelId ?? "wechat") === "wechat") {
|
|
2050
|
+
migrateLegacyChannelFiles((message) => log(message));
|
|
2051
|
+
}
|
|
1797
2052
|
loadEmojiBindings();
|
|
1798
2053
|
await cleanupDaemonBeforeStart({ cwd: options.cwd });
|
|
1799
2054
|
const cleanupResult = await cleanupSingleBridgeBeforeDaemon();
|
|
@@ -1809,19 +2064,36 @@ export async function runDaemon(options) {
|
|
|
1809
2064
|
if (reapedOpencodePids.length > 0) {
|
|
1810
2065
|
log(`Cleaned ${reapedOpencodePids.length} orphaned OpenCode process(es): ${reapedOpencodePids.join(", ")}`);
|
|
1811
2066
|
}
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2067
|
+
const wecomAccount = options.channelId === "wecom"
|
|
2068
|
+
? await ensureWecomAccount({ log })
|
|
2069
|
+
: null;
|
|
2070
|
+
const credentials = wecomAccount
|
|
2071
|
+
? { userId: wecomAccount.operatorUserId }
|
|
2072
|
+
: await ensureWechatCredentials({
|
|
2073
|
+
requireUserId: true,
|
|
2074
|
+
validateExisting: true,
|
|
2075
|
+
log,
|
|
2076
|
+
});
|
|
1817
2077
|
if (!credentials.userId) {
|
|
1818
|
-
throw new Error(
|
|
2078
|
+
throw new Error(options.channelId === "wecom"
|
|
2079
|
+
? "Saved WeCom credentials are missing operatorUserId."
|
|
2080
|
+
: "Saved WeChat credentials are missing userId.");
|
|
1819
2081
|
}
|
|
1820
2082
|
const daemon = new WechatDaemon({
|
|
1821
2083
|
cwd: options.cwd,
|
|
1822
2084
|
profile: options.profile,
|
|
1823
2085
|
authorizedUserId: credentials.userId,
|
|
1824
2086
|
transport: new WeChatTransport({ log, logError }),
|
|
2087
|
+
channelId: options.channelId,
|
|
2088
|
+
wecomTransport: wecomAccount
|
|
2089
|
+
? new WecomTransport({
|
|
2090
|
+
account: wecomAccount,
|
|
2091
|
+
logger: {
|
|
2092
|
+
log: (message) => log(message),
|
|
2093
|
+
error: (message) => logError(message),
|
|
2094
|
+
},
|
|
2095
|
+
})
|
|
2096
|
+
: null,
|
|
1825
2097
|
});
|
|
1826
2098
|
if (cleanupResult.action === "stopped" && isDaemonAdapterKind(cleanupResult.lock.adapter)) {
|
|
1827
2099
|
daemon.takenOverAdapter = cleanupResult.lock.adapter;
|
|
@@ -1837,6 +2109,9 @@ export async function runDaemon(options) {
|
|
|
1837
2109
|
}
|
|
1838
2110
|
shutdownInProgress = true;
|
|
1839
2111
|
log(`Received ${signal}. Stopping daemon.`);
|
|
2112
|
+
// Remove this process's endpoint synchronously so a new terminal can be
|
|
2113
|
+
// started immediately even if asynchronous adapter cleanup takes time.
|
|
2114
|
+
clearDaemonEndpoint();
|
|
1840
2115
|
void daemon.shutdown().finally(() => process.exit(0));
|
|
1841
2116
|
};
|
|
1842
2117
|
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
@@ -1871,7 +2146,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1871
2146
|
process.exit(1);
|
|
1872
2147
|
}
|
|
1873
2148
|
}
|
|
1874
|
-
const isDirectRun =
|
|
2149
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
1875
2150
|
if (isDirectRun) {
|
|
1876
2151
|
void main();
|
|
1877
2152
|
}
|