cli-wechat-bridge 1.1.6 → 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 +163 -198
- 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 +126 -3
- package/dist/bridge/bridge-process-reaper.js +35 -2
- package/dist/bridge/bridge-state.js +10 -3
- package/dist/bridge/wechat-bridge.js +277 -44
- 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 +2 -1
- package/dist/companion/local-companion-start.js +49 -9
- package/dist/companion/local-companion.js +2 -1
- package/dist/core/channel-types.js +3 -1
- 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 +265 -36
- package/dist/i18n/messages-en.js +2 -0
- package/dist/i18n/messages-zh.js +2 -0
- package/dist/utils/doctor.js +30 -10
- package/dist/wechat/setup.js +113 -13
- package/package.json +20 -10
|
@@ -17,12 +17,17 @@ 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
24
|
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
25
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
25
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";
|
|
26
31
|
import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
27
32
|
import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
|
|
28
33
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
@@ -40,11 +45,15 @@ const DAEMON_TAKEOVER_STOP_POLL_MS = 250;
|
|
|
40
45
|
const VISIBLE_CLIENT_CONNECT_TIMEOUT_MS = 15_000;
|
|
41
46
|
const VISIBLE_CLIENT_CONNECT_POLL_MS = 250;
|
|
42
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
|
+
}
|
|
43
52
|
function log(message) {
|
|
44
|
-
process.stderr.write(`[
|
|
53
|
+
process.stderr.write(`[${getCliChannelId()}-daemon] ${message}\n`);
|
|
45
54
|
}
|
|
46
55
|
function logError(message) {
|
|
47
|
-
process.stderr.write(`[
|
|
56
|
+
process.stderr.write(`[${getCliChannelId()}-daemon] ERROR: ${message}\n`);
|
|
48
57
|
}
|
|
49
58
|
function appendDaemonLog(message) {
|
|
50
59
|
ensureChannelDataDir();
|
|
@@ -55,6 +64,48 @@ function computePollRetryDelayMs(consecutiveFailures) {
|
|
|
55
64
|
const exponent = Math.min(normalizedFailures - 1, 5);
|
|
56
65
|
return Math.min(POLL_RETRY_MAX_MS, POLL_RETRY_BASE_MS * 2 ** exponent);
|
|
57
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
|
+
}
|
|
58
109
|
function isDaemonAdapterKind(value) {
|
|
59
110
|
return value === "codex" || value === "claude" || value === "opencode" || value === "pi";
|
|
60
111
|
}
|
|
@@ -76,6 +127,7 @@ export function parseDaemonCliArgs(argv) {
|
|
|
76
127
|
let profile;
|
|
77
128
|
let initialAdapter;
|
|
78
129
|
let openVisible = true;
|
|
130
|
+
let channelId = "wechat";
|
|
79
131
|
for (let i = 0; i < argv.length; i += 1) {
|
|
80
132
|
const arg = argv[i];
|
|
81
133
|
if (!arg) {
|
|
@@ -84,10 +136,10 @@ export function parseDaemonCliArgs(argv) {
|
|
|
84
136
|
const next = argv[i + 1];
|
|
85
137
|
if (arg === "--help" || arg === "-h") {
|
|
86
138
|
process.stdout.write([
|
|
87
|
-
|
|
139
|
+
`Usage: ${channelId}-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]`,
|
|
88
140
|
"",
|
|
89
|
-
|
|
90
|
-
|
|
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.`,
|
|
91
143
|
"",
|
|
92
144
|
].join("\n"));
|
|
93
145
|
process.exit(0);
|
|
@@ -100,6 +152,14 @@ export function parseDaemonCliArgs(argv) {
|
|
|
100
152
|
i += 1;
|
|
101
153
|
continue;
|
|
102
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
|
+
}
|
|
103
163
|
if (arg === "--adapter") {
|
|
104
164
|
if (!isDaemonAdapterKind(next)) {
|
|
105
165
|
throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
|
|
@@ -122,7 +182,13 @@ export function parseDaemonCliArgs(argv) {
|
|
|
122
182
|
}
|
|
123
183
|
throw new Error(`Unknown argument: ${arg}`);
|
|
124
184
|
}
|
|
125
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
cwd,
|
|
187
|
+
profile,
|
|
188
|
+
initialAdapter,
|
|
189
|
+
openVisible,
|
|
190
|
+
...(channelId === "wecom" ? { channelId } : {}),
|
|
191
|
+
};
|
|
126
192
|
}
|
|
127
193
|
export function parseDaemonSwitchCommand(text) {
|
|
128
194
|
const normalized = text.trim().toLowerCase();
|
|
@@ -221,7 +287,7 @@ export function buildVisibleClientLaunchArgs(params) {
|
|
|
221
287
|
export function buildWindowsVisibleClientLaunchCommand(params) {
|
|
222
288
|
return [
|
|
223
289
|
"start",
|
|
224
|
-
quoteWindowsCommandArg(
|
|
290
|
+
quoteWindowsCommandArg(`${params.channelId ?? "wechat"}-${params.adapter}`),
|
|
225
291
|
"/D",
|
|
226
292
|
quoteWindowsCommandArg(params.cwd),
|
|
227
293
|
quoteWindowsCommandArg(process.execPath),
|
|
@@ -269,6 +335,7 @@ function openVisibleClient(params) {
|
|
|
269
335
|
adapter: params.adapter,
|
|
270
336
|
cwd: params.cwd,
|
|
271
337
|
args,
|
|
338
|
+
channelId: params.channelId,
|
|
272
339
|
}),
|
|
273
340
|
];
|
|
274
341
|
const child = spawn(command, launchArgs, {
|
|
@@ -289,7 +356,7 @@ function openVisibleClient(params) {
|
|
|
289
356
|
pid: child.pid,
|
|
290
357
|
};
|
|
291
358
|
}
|
|
292
|
-
const title =
|
|
359
|
+
const title = `${params.channelId ?? "wechat"}-${params.adapter}`;
|
|
293
360
|
const fullArgs = [process.execPath, ...args];
|
|
294
361
|
if (process.platform === "darwin") {
|
|
295
362
|
const cmdLine = fullArgs.map(shellQuotePosix).join(" ");
|
|
@@ -486,6 +553,10 @@ class WechatDaemon {
|
|
|
486
553
|
profile;
|
|
487
554
|
authorizedUserId;
|
|
488
555
|
transport;
|
|
556
|
+
channelId;
|
|
557
|
+
wecomTransport;
|
|
558
|
+
currentInboundConversation = null;
|
|
559
|
+
handlingInboundMessage = false;
|
|
489
560
|
slots = new Map();
|
|
490
561
|
// Per-adapter serialization chains for ensureSlot (see ensureSlot comment).
|
|
491
562
|
slotEnsureChains = new Map();
|
|
@@ -506,7 +577,19 @@ class WechatDaemon {
|
|
|
506
577
|
this.profile = params.profile;
|
|
507
578
|
this.authorizedUserId = params.authorizedUserId;
|
|
508
579
|
this.transport = params.transport;
|
|
509
|
-
this.
|
|
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));
|
|
510
593
|
}
|
|
511
594
|
async startIpcServer() {
|
|
512
595
|
this.endpointToken = buildDaemonToken();
|
|
@@ -554,6 +637,7 @@ class WechatDaemon {
|
|
|
554
637
|
token: this.endpointToken,
|
|
555
638
|
cwd: this.cwd,
|
|
556
639
|
startedAt: this.startedAt,
|
|
640
|
+
channelId: this.channelId,
|
|
557
641
|
});
|
|
558
642
|
resolve();
|
|
559
643
|
});
|
|
@@ -562,6 +646,7 @@ class WechatDaemon {
|
|
|
562
646
|
getStatus() {
|
|
563
647
|
return {
|
|
564
648
|
cwd: this.cwd,
|
|
649
|
+
channelId: this.channelId,
|
|
565
650
|
activeAdapter: this.activeAdapter ?? undefined,
|
|
566
651
|
startedAt: this.startedAt,
|
|
567
652
|
slots: Array.from(this.slots.values()).map((slot) => {
|
|
@@ -590,10 +675,52 @@ class WechatDaemon {
|
|
|
590
675
|
}
|
|
591
676
|
async runPollLoop() {
|
|
592
677
|
let consecutivePollFailures = 0;
|
|
593
|
-
|
|
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.`);
|
|
594
721
|
log(`Working directory: ${this.cwd}`);
|
|
595
|
-
log(
|
|
596
|
-
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}`);
|
|
597
724
|
const activeSlot = this.getActiveSlot();
|
|
598
725
|
const welcomeText = t("daemon.welcome", {
|
|
599
726
|
cwd: this.cwd,
|
|
@@ -601,6 +728,13 @@ class WechatDaemon {
|
|
|
601
728
|
bindings: formatBindingsListMessage(listBindings()),
|
|
602
729
|
});
|
|
603
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
|
+
}
|
|
604
738
|
while (!this.shutdownPromise) {
|
|
605
739
|
let pollResult;
|
|
606
740
|
try {
|
|
@@ -705,6 +839,12 @@ class WechatDaemon {
|
|
|
705
839
|
await this.waitForPendingWechatForwardTasks();
|
|
706
840
|
await this.textSendChain.catch(() => undefined);
|
|
707
841
|
await this.attachmentSendChain.catch(() => undefined);
|
|
842
|
+
try {
|
|
843
|
+
this.wecomTransport?.stop();
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
// Best effort shutdown.
|
|
847
|
+
}
|
|
708
848
|
for (const slot of this.slots.values()) {
|
|
709
849
|
try {
|
|
710
850
|
await slot.runtime.dispose();
|
|
@@ -736,7 +876,7 @@ class WechatDaemon {
|
|
|
736
876
|
return { shuttingDown: true };
|
|
737
877
|
case "ensure_slot":
|
|
738
878
|
if (!isSameWorkspaceCwd(request.cwd, this.cwd)) {
|
|
739
|
-
throw new Error(
|
|
879
|
+
throw new Error(`${this.channelId}-daemon is bound to ${this.cwd}; requested cwd was ${request.cwd}.`);
|
|
740
880
|
}
|
|
741
881
|
return await this.ensureSlot(request.adapter, {
|
|
742
882
|
profile: request.profile,
|
|
@@ -828,6 +968,7 @@ class WechatDaemon {
|
|
|
828
968
|
cwd: this.cwd,
|
|
829
969
|
sessionStartMode,
|
|
830
970
|
cliArgs: options.cliArgs,
|
|
971
|
+
channelId: this.channelId,
|
|
831
972
|
onError: (error) => {
|
|
832
973
|
appendDaemonLog(`visible_client_open_error: adapter=${adapter} error=${truncatePreview(error.message, 400)}`);
|
|
833
974
|
},
|
|
@@ -937,6 +1078,16 @@ class WechatDaemon {
|
|
|
937
1078
|
slot.controller.syncLocalClientEndpoint();
|
|
938
1079
|
}
|
|
939
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
|
+
}
|
|
940
1091
|
return new WechatChannelPort({
|
|
941
1092
|
sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
|
|
942
1093
|
sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(filePath, { recipientId })),
|
|
@@ -981,11 +1132,15 @@ class WechatDaemon {
|
|
|
981
1132
|
appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
|
|
982
1133
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
983
1134
|
await channelPort.send({
|
|
984
|
-
target:
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1135
|
+
target: this.channelId === "wecom"
|
|
1136
|
+
? slot.activeConversation ??
|
|
1137
|
+
slot.lastConversation ??
|
|
1138
|
+
this.currentInboundConversation
|
|
1139
|
+
: {
|
|
1140
|
+
channelId: "wechat",
|
|
1141
|
+
conversationId: this.authorizedUserId,
|
|
1142
|
+
recipientId: this.authorizedUserId,
|
|
1143
|
+
},
|
|
989
1144
|
kind: "final_reply",
|
|
990
1145
|
text: next.text,
|
|
991
1146
|
adapter: slot.adapter,
|
|
@@ -1064,6 +1219,7 @@ class WechatDaemon {
|
|
|
1064
1219
|
slot.pendingConfirmations = [];
|
|
1065
1220
|
slot.pendingUserInput = null;
|
|
1066
1221
|
slot.activeTask = null;
|
|
1222
|
+
slot.activeConversation = undefined;
|
|
1067
1223
|
}));
|
|
1068
1224
|
},
|
|
1069
1225
|
taskFailed: (next) => {
|
|
@@ -1071,6 +1227,7 @@ class WechatDaemon {
|
|
|
1071
1227
|
slot.pendingConfirmations = [];
|
|
1072
1228
|
slot.pendingUserInput = null;
|
|
1073
1229
|
slot.activeTask = null;
|
|
1230
|
+
slot.activeConversation = undefined;
|
|
1074
1231
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed");
|
|
1075
1232
|
}));
|
|
1076
1233
|
},
|
|
@@ -1080,6 +1237,7 @@ class WechatDaemon {
|
|
|
1080
1237
|
slot.pendingConfirmations = [];
|
|
1081
1238
|
slot.pendingUserInput = null;
|
|
1082
1239
|
slot.activeTask = null;
|
|
1240
|
+
slot.activeConversation = undefined;
|
|
1083
1241
|
this.disposeDeadSlot(slot);
|
|
1084
1242
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1085
1243
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
|
|
@@ -1094,9 +1252,17 @@ class WechatDaemon {
|
|
|
1094
1252
|
appendDaemonLog(`event_forward_failed: adapter=${slot.adapter} message=${message}`);
|
|
1095
1253
|
});
|
|
1096
1254
|
}
|
|
1255
|
+
bindCurrentWecomConversation(slot) {
|
|
1256
|
+
if (this.channelId !== "wecom" || !slot || !this.currentInboundConversation) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
slot.lastConversation = this.currentInboundConversation;
|
|
1260
|
+
}
|
|
1097
1261
|
async handleInboundMessage(message) {
|
|
1098
1262
|
if (message.senderId !== this.authorizedUserId) {
|
|
1099
|
-
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.");
|
|
1100
1266
|
return;
|
|
1101
1267
|
}
|
|
1102
1268
|
const emojiMatch = resolveEmojiCommand(message.text);
|
|
@@ -1108,6 +1274,7 @@ class WechatDaemon {
|
|
|
1108
1274
|
reuseExistingVisible: true,
|
|
1109
1275
|
});
|
|
1110
1276
|
if (result.activated) {
|
|
1277
|
+
this.bindCurrentWecomConversation(this.getActiveSlot());
|
|
1111
1278
|
message = { ...message, text: emojiMatch.remainder };
|
|
1112
1279
|
}
|
|
1113
1280
|
else {
|
|
@@ -1134,6 +1301,7 @@ class WechatDaemon {
|
|
|
1134
1301
|
await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchDirective.adapter}.\n${detail}`);
|
|
1135
1302
|
return;
|
|
1136
1303
|
}
|
|
1304
|
+
this.bindCurrentWecomConversation(this.getActiveSlot());
|
|
1137
1305
|
if (switchDirective.remainder) {
|
|
1138
1306
|
message = { ...message, text: switchDirective.remainder };
|
|
1139
1307
|
}
|
|
@@ -1143,7 +1311,7 @@ class WechatDaemon {
|
|
|
1143
1311
|
}
|
|
1144
1312
|
}
|
|
1145
1313
|
if (message.text.trim().toLowerCase() === "/daemon-stop") {
|
|
1146
|
-
await this.queueWechatMessage(message.senderId,
|
|
1314
|
+
await this.queueWechatMessage(message.senderId, `Stopping ${this.channelId}-daemon...`);
|
|
1147
1315
|
setTimeout(() => {
|
|
1148
1316
|
void this.shutdown().finally(() => process.exit(0));
|
|
1149
1317
|
}, 0);
|
|
@@ -1163,7 +1331,9 @@ class WechatDaemon {
|
|
|
1163
1331
|
await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
|
|
1164
1332
|
return;
|
|
1165
1333
|
}
|
|
1334
|
+
this.bindCurrentWecomConversation(slot);
|
|
1166
1335
|
const routeCurrentMessage = async (currentSlot, command) => {
|
|
1336
|
+
const previousTask = currentSlot.activeTask;
|
|
1167
1337
|
await routeBridgeMessage({
|
|
1168
1338
|
message: toChannelInboundMessage(message),
|
|
1169
1339
|
authorized: true,
|
|
@@ -1192,6 +1362,13 @@ class WechatDaemon {
|
|
|
1192
1362
|
await this.dispatchInboundWechatText(message, currentSlot);
|
|
1193
1363
|
},
|
|
1194
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
|
+
}
|
|
1195
1372
|
};
|
|
1196
1373
|
const command = parseWechatControlCommand(message.text, {
|
|
1197
1374
|
adapter: slot.adapter,
|
|
@@ -1215,6 +1392,7 @@ class WechatDaemon {
|
|
|
1215
1392
|
return;
|
|
1216
1393
|
}
|
|
1217
1394
|
slot = this.getActiveSlot() ?? slot;
|
|
1395
|
+
this.bindCurrentWecomConversation(slot);
|
|
1218
1396
|
await routeCurrentMessage(slot, null);
|
|
1219
1397
|
}
|
|
1220
1398
|
async handleEmojiBindingsCommand(senderId, cmd) {
|
|
@@ -1244,7 +1422,7 @@ class WechatDaemon {
|
|
|
1244
1422
|
return;
|
|
1245
1423
|
case "resume": {
|
|
1246
1424
|
if (!isWechatResumeEnabled(activeSlot.adapter)) {
|
|
1247
|
-
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.`);
|
|
1248
1426
|
return;
|
|
1249
1427
|
}
|
|
1250
1428
|
try {
|
|
@@ -1408,7 +1586,9 @@ class WechatDaemon {
|
|
|
1408
1586
|
inputPreview: truncatePreview(preview, 180),
|
|
1409
1587
|
};
|
|
1410
1588
|
appendDaemonLog(`forwarded_input: adapter=${slot.adapter} text=${truncatePreview(preview)}`);
|
|
1411
|
-
await slot.runtime.sendInput(
|
|
1589
|
+
await slot.runtime.sendInput(this.channelId === "wecom"
|
|
1590
|
+
? buildWecomInboundPrompt(message.text, message.attachments)
|
|
1591
|
+
: buildWechatInboundPrompt(message.text, message.attachments));
|
|
1412
1592
|
}
|
|
1413
1593
|
queueWechatTextAction(action) {
|
|
1414
1594
|
const run = this.textSendChain.then(action);
|
|
@@ -1420,7 +1600,37 @@ class WechatDaemon {
|
|
|
1420
1600
|
this.attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
1421
1601
|
return run;
|
|
1422
1602
|
}
|
|
1423
|
-
async sendWechatMessageNow(senderId, text, context = "message") {
|
|
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
|
+
}
|
|
1424
1634
|
const startedAtMs = Date.now();
|
|
1425
1635
|
appendDaemonLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
1426
1636
|
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
@@ -1467,11 +1677,11 @@ class WechatDaemon {
|
|
|
1467
1677
|
}
|
|
1468
1678
|
return { status: "failed" };
|
|
1469
1679
|
}
|
|
1470
|
-
queueWechatMessage(senderId, text, context = "message") {
|
|
1680
|
+
queueWechatMessage(senderId, text, context = "message", targetOverride) {
|
|
1471
1681
|
return this.queueWechatTextAction(async () => {
|
|
1472
|
-
const result = await this.sendWechatMessageNow(senderId, text, context);
|
|
1682
|
+
const result = await this.sendWechatMessageNow(senderId, text, context, targetOverride);
|
|
1473
1683
|
if (result.status === "context_unavailable") {
|
|
1474
|
-
const pending = this.pendingWechatMessages.enqueue(senderId, text, context);
|
|
1684
|
+
const pending = this.pendingWechatMessages.enqueue(senderId, text, context, result.target);
|
|
1475
1685
|
if (pending) {
|
|
1476
1686
|
appendDaemonLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${this.pendingWechatMessages.list().length}`);
|
|
1477
1687
|
}
|
|
@@ -1482,7 +1692,7 @@ class WechatDaemon {
|
|
|
1482
1692
|
flushPendingWechatMessages() {
|
|
1483
1693
|
return this.queueWechatTextAction(async () => {
|
|
1484
1694
|
for (const pending of this.pendingWechatMessages.list()) {
|
|
1485
|
-
const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
|
|
1695
|
+
const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
|
|
1486
1696
|
if (result.status === "sent") {
|
|
1487
1697
|
this.pendingWechatMessages.remove(pending.id);
|
|
1488
1698
|
appendDaemonLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
@@ -1793,7 +2003,9 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1793
2003
|
return { action: "stopped", lock, forced };
|
|
1794
2004
|
}
|
|
1795
2005
|
export async function runDaemon(options) {
|
|
1796
|
-
|
|
2006
|
+
if ((options.channelId ?? "wechat") === "wechat") {
|
|
2007
|
+
migrateLegacyChannelFiles((message) => log(message));
|
|
2008
|
+
}
|
|
1797
2009
|
loadEmojiBindings();
|
|
1798
2010
|
await cleanupDaemonBeforeStart({ cwd: options.cwd });
|
|
1799
2011
|
const cleanupResult = await cleanupSingleBridgeBeforeDaemon();
|
|
@@ -1809,19 +2021,36 @@ export async function runDaemon(options) {
|
|
|
1809
2021
|
if (reapedOpencodePids.length > 0) {
|
|
1810
2022
|
log(`Cleaned ${reapedOpencodePids.length} orphaned OpenCode process(es): ${reapedOpencodePids.join(", ")}`);
|
|
1811
2023
|
}
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
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
|
+
});
|
|
1817
2034
|
if (!credentials.userId) {
|
|
1818
|
-
throw new Error(
|
|
2035
|
+
throw new Error(options.channelId === "wecom"
|
|
2036
|
+
? "Saved WeCom credentials are missing operatorUserId."
|
|
2037
|
+
: "Saved WeChat credentials are missing userId.");
|
|
1819
2038
|
}
|
|
1820
2039
|
const daemon = new WechatDaemon({
|
|
1821
2040
|
cwd: options.cwd,
|
|
1822
2041
|
profile: options.profile,
|
|
1823
2042
|
authorizedUserId: credentials.userId,
|
|
1824
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,
|
|
1825
2054
|
});
|
|
1826
2055
|
if (cleanupResult.action === "stopped" && isDaemonAdapterKind(cleanupResult.lock.adapter)) {
|
|
1827
2056
|
daemon.takenOverAdapter = cleanupResult.lock.adapter;
|
|
@@ -1871,7 +2100,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1871
2100
|
process.exit(1);
|
|
1872
2101
|
}
|
|
1873
2102
|
}
|
|
1874
|
-
const isDirectRun =
|
|
2103
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
1875
2104
|
if (isDirectRun) {
|
|
1876
2105
|
void main();
|
|
1877
2106
|
}
|
package/dist/i18n/messages-en.js
CHANGED
|
@@ -59,6 +59,7 @@ export const messages = {
|
|
|
59
59
|
"doctor.label.codePage": "Console code page",
|
|
60
60
|
"doctor.codePage.nonAsciiWarning": "{codePage} (not UTF-8 and the data directory path contains non-ASCII characters; hook scripts from older versions break in this environment — upgrade to the latest release)",
|
|
61
61
|
"doctor.label.connectivity": "WeChat connectivity",
|
|
62
|
+
"doctor.label.wecomConnectivity": "WeCom connectivity",
|
|
62
63
|
"doctor.connectivity.ok": "reachable ({baseUrl})",
|
|
63
64
|
"doctor.connectivity.unreachable": "cannot reach {baseUrl} (check network and firewall)",
|
|
64
65
|
"doctor.connectivity.proxyHint": "cannot reach {baseUrl}. Proxy {proxy} detected, but Node fetch ignores proxy environment variables by default; if your network requires the proxy, set NODE_USE_ENV_PROXY=1 and retry.",
|
|
@@ -82,6 +83,7 @@ export const messages = {
|
|
|
82
83
|
"doctor.dataDirMissing": "(will be created on first run)",
|
|
83
84
|
"doctor.credentialsFound": "found",
|
|
84
85
|
"doctor.credentialsMissing": "not found (run wechat-setup first)",
|
|
86
|
+
"doctor.credentialsMissingWecom": "not found (run wecom-setup first)",
|
|
85
87
|
"doctor.mode.bridge": "standalone bridge",
|
|
86
88
|
"doctor.mode.generic": "generic",
|
|
87
89
|
"doctor.notSpecified": "(not specified)",
|
package/dist/i18n/messages-zh.js
CHANGED
|
@@ -59,6 +59,7 @@ export const messages = {
|
|
|
59
59
|
"doctor.label.codePage": "控制台代码页",
|
|
60
60
|
"doctor.codePage.nonAsciiWarning": "{codePage}(非 UTF-8,且数据目录路径含非 ASCII 字符;旧版本的 hook 脚本在此环境下会乱码失效,请升级到最新版本)",
|
|
61
61
|
"doctor.label.connectivity": "微信服务可达性",
|
|
62
|
+
"doctor.label.wecomConnectivity": "企业微信服务可达性",
|
|
62
63
|
"doctor.connectivity.ok": "可达 ({baseUrl})",
|
|
63
64
|
"doctor.connectivity.unreachable": "无法访问 {baseUrl}(检查网络与防火墙)",
|
|
64
65
|
"doctor.connectivity.proxyHint": "无法访问 {baseUrl}。检测到代理 {proxy},但 Node fetch 默认不使用代理环境变量;若必须经代理出网,请设置 NODE_USE_ENV_PROXY=1 后重试。",
|
|
@@ -82,6 +83,7 @@ export const messages = {
|
|
|
82
83
|
"doctor.dataDirMissing": "(首次运行会创建)",
|
|
83
84
|
"doctor.credentialsFound": "已找到",
|
|
84
85
|
"doctor.credentialsMissing": "未找到 (先运行 wechat-setup)",
|
|
86
|
+
"doctor.credentialsMissingWecom": "未找到 (先运行 wecom-setup)",
|
|
85
87
|
"doctor.mode.bridge": "独立桥接",
|
|
86
88
|
"doctor.mode.generic": "通用",
|
|
87
89
|
"doctor.notSpecified": "(未指定)",
|