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
|
@@ -13,10 +13,15 @@ import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
|
13
13
|
import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
|
|
14
14
|
import { routeBridgeMessage } from "../core/bridge-message-router.js";
|
|
15
15
|
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
16
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
16
17
|
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
18
|
+
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
19
|
+
import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
|
|
20
|
+
import { WecomTransport } from "../channels/wecom/wecom-transport.js";
|
|
21
|
+
import { buildWecomInboundPrompt, formatWecomVisibleText, } from "../channels/wecom/wecom-message.js";
|
|
17
22
|
import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, formatThinkingForWechat, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
|
|
18
23
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
|
|
19
|
-
import { getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
24
|
+
import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
20
25
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
21
26
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
22
27
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
@@ -105,6 +110,7 @@ export function parseCliArgs(argv) {
|
|
|
105
110
|
let profile;
|
|
106
111
|
let lifecycle = "persistent";
|
|
107
112
|
let sessionStartMode = "restore";
|
|
113
|
+
let channelId = "wechat";
|
|
108
114
|
for (let i = 0; i < argv.length; i += 1) {
|
|
109
115
|
const arg = argv[i];
|
|
110
116
|
const next = argv[i + 1];
|
|
@@ -151,6 +157,13 @@ export function parseCliArgs(argv) {
|
|
|
151
157
|
sessionStartMode = next;
|
|
152
158
|
i += 1;
|
|
153
159
|
break;
|
|
160
|
+
case "--channel":
|
|
161
|
+
if (!next || (next !== "wechat" && next !== "wecom")) {
|
|
162
|
+
throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
|
|
163
|
+
}
|
|
164
|
+
channelId = next;
|
|
165
|
+
i += 1;
|
|
166
|
+
break;
|
|
154
167
|
case "--shutdown-on-parent-exit":
|
|
155
168
|
lifecycle = "companion_bound";
|
|
156
169
|
break;
|
|
@@ -173,6 +186,51 @@ export function parseCliArgs(argv) {
|
|
|
173
186
|
profile,
|
|
174
187
|
lifecycle,
|
|
175
188
|
sessionStartMode,
|
|
189
|
+
channelId,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function toWecomOutputKind(context) {
|
|
193
|
+
const known = new Set([
|
|
194
|
+
"status",
|
|
195
|
+
"notice",
|
|
196
|
+
"thinking",
|
|
197
|
+
"final_reply",
|
|
198
|
+
"approval_required",
|
|
199
|
+
"user_input_required",
|
|
200
|
+
"mirrored_input",
|
|
201
|
+
"task_failed",
|
|
202
|
+
"fatal_error",
|
|
203
|
+
]);
|
|
204
|
+
return known.has(context)
|
|
205
|
+
? context
|
|
206
|
+
: context === "mirrored_user_input"
|
|
207
|
+
? "mirrored_input"
|
|
208
|
+
: "notice";
|
|
209
|
+
}
|
|
210
|
+
function toWechatSendContext(kind) {
|
|
211
|
+
if (kind === "mirrored_input") {
|
|
212
|
+
return "mirrored_user_input";
|
|
213
|
+
}
|
|
214
|
+
if (kind === "status") {
|
|
215
|
+
return "message";
|
|
216
|
+
}
|
|
217
|
+
return kind;
|
|
218
|
+
}
|
|
219
|
+
function toLegacyInboundWechatMessage(message) {
|
|
220
|
+
return {
|
|
221
|
+
senderId: message.senderId,
|
|
222
|
+
sender: message.senderId,
|
|
223
|
+
sessionId: message.conversation.conversationId,
|
|
224
|
+
text: message.text,
|
|
225
|
+
attachments: message.attachments.map((attachment) => ({
|
|
226
|
+
kind: attachment.kind === "image" ? "image" : "file",
|
|
227
|
+
path: attachment.path,
|
|
228
|
+
fileName: attachment.fileName || path.basename(attachment.path),
|
|
229
|
+
sizeBytes: attachment.sizeBytes ?? 0,
|
|
230
|
+
})),
|
|
231
|
+
contextToken: message.conversation.opaqueRef,
|
|
232
|
+
createdAt: message.createdAt,
|
|
233
|
+
createdAtMs: Date.parse(message.createdAt),
|
|
176
234
|
};
|
|
177
235
|
}
|
|
178
236
|
function printUsageAndExit() {
|
|
@@ -180,7 +238,7 @@ function printUsageAndExit() {
|
|
|
180
238
|
"Internal bridge runtime usage:",
|
|
181
239
|
" npm run bridge -- --adapter <codex|claude|opencode|pi> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>] [--session-start-mode <restore|new>]",
|
|
182
240
|
"",
|
|
183
|
-
"This entry is internal. Users should run
|
|
241
|
+
"This entry is internal. Users should run a wechat-* or wecom-* launcher.",
|
|
184
242
|
"",
|
|
185
243
|
].join("\n"));
|
|
186
244
|
process.exit(0);
|
|
@@ -194,21 +252,37 @@ async function main() {
|
|
|
194
252
|
const options = parseCliArgs(process.argv.slice(2));
|
|
195
253
|
const daemonEndpoint = readDaemonEndpoint();
|
|
196
254
|
if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
|
|
197
|
-
throw new Error(
|
|
255
|
+
throw new Error(`${daemonEndpoint.channelId ?? "wechat"}-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
|
|
198
256
|
}
|
|
199
257
|
if (daemonEndpoint) {
|
|
200
258
|
clearDaemonEndpoint(daemonEndpoint.pid);
|
|
201
259
|
log(`Cleared stale wechat-daemon endpoint for pid=${daemonEndpoint.pid}.`);
|
|
202
260
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
261
|
+
const wecomAccount = options.channelId === "wecom"
|
|
262
|
+
? await ensureWecomAccount({ log })
|
|
263
|
+
: null;
|
|
264
|
+
const credentials = wecomAccount
|
|
265
|
+
? { userId: wecomAccount.operatorUserId }
|
|
266
|
+
: await ensureWechatCredentials({
|
|
267
|
+
requireUserId: true,
|
|
268
|
+
validateExisting: true,
|
|
269
|
+
log,
|
|
270
|
+
});
|
|
208
271
|
if (!credentials.userId) {
|
|
209
|
-
throw new Error(
|
|
272
|
+
throw new Error(options.channelId === "wecom"
|
|
273
|
+
? "Saved WeCom credentials are missing operatorUserId."
|
|
274
|
+
: "Saved WeChat credentials are missing userId.");
|
|
210
275
|
}
|
|
211
276
|
const transport = new WeChatTransport({ log, logError });
|
|
277
|
+
const wecomTransport = wecomAccount
|
|
278
|
+
? new WecomTransport({
|
|
279
|
+
account: wecomAccount,
|
|
280
|
+
logger: {
|
|
281
|
+
log: (message) => log(message),
|
|
282
|
+
error: (message) => logError(message),
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
: null;
|
|
212
286
|
// 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
|
|
213
287
|
// unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
|
|
214
288
|
// 否则会与强制退出 teardown 竞态。
|
|
@@ -227,6 +301,7 @@ async function main() {
|
|
|
227
301
|
const stateStore = new BridgeStateStore({
|
|
228
302
|
...options,
|
|
229
303
|
authorizedUserId: credentials.userId,
|
|
304
|
+
channelId: options.channelId,
|
|
230
305
|
});
|
|
231
306
|
const reapedPeerPids = await reapPeerBridgeProcesses({
|
|
232
307
|
logger: (message) => stateStore.appendLog(message),
|
|
@@ -273,7 +348,9 @@ async function main() {
|
|
|
273
348
|
initialResumeConversationId: stateStore.getState().resumeConversationId,
|
|
274
349
|
initialTranscriptPath: stateStore.getState().transcriptPath,
|
|
275
350
|
});
|
|
276
|
-
const pendingWechatMessages = new PendingWechatMessageStore(
|
|
351
|
+
const pendingWechatMessages = new PendingWechatMessageStore(options.channelId === "wechat"
|
|
352
|
+
? getPendingWechatMessagesFile(options.cwd)
|
|
353
|
+
: getPendingChannelMessagesFile(options.cwd, options.channelId));
|
|
277
354
|
const resumeCoordinator = new ResumeSessionCoordinator({
|
|
278
355
|
adapter: options.adapter,
|
|
279
356
|
runtime: adapter,
|
|
@@ -285,6 +362,15 @@ async function main() {
|
|
|
285
362
|
let attachmentSendChain = Promise.resolve();
|
|
286
363
|
const pendingWechatForwardTasks = new Set();
|
|
287
364
|
let activeTask = null;
|
|
365
|
+
let currentWecomConversation = null;
|
|
366
|
+
let activeWecomConversation = null;
|
|
367
|
+
let lastWecomConversation = {
|
|
368
|
+
channelId: "wecom",
|
|
369
|
+
accountId: wecomAccount?.botId,
|
|
370
|
+
conversationId: credentials.userId,
|
|
371
|
+
recipientId: credentials.userId,
|
|
372
|
+
metadata: { chatType: "direct" },
|
|
373
|
+
};
|
|
288
374
|
const deferredInboundMessages = [];
|
|
289
375
|
let drainingDeferredInboundMessages = false;
|
|
290
376
|
let consecutivePollFailures = 0;
|
|
@@ -299,7 +385,34 @@ async function main() {
|
|
|
299
385
|
attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
300
386
|
return run;
|
|
301
387
|
};
|
|
302
|
-
const sendWechatMessageNow = async (senderId, text, context = "message") => {
|
|
388
|
+
const sendWechatMessageNow = async (senderId, text, context = "message", targetOverride) => {
|
|
389
|
+
if (options.channelId === "wecom") {
|
|
390
|
+
const target = targetOverride ??
|
|
391
|
+
(senderId === credentials.userId
|
|
392
|
+
? currentWecomConversation ??
|
|
393
|
+
activeWecomConversation ??
|
|
394
|
+
lastWecomConversation
|
|
395
|
+
: {
|
|
396
|
+
channelId: "wecom",
|
|
397
|
+
accountId: wecomAccount?.botId,
|
|
398
|
+
conversationId: senderId,
|
|
399
|
+
recipientId: senderId,
|
|
400
|
+
metadata: { chatType: "direct" },
|
|
401
|
+
});
|
|
402
|
+
const startedAtMs = Date.now();
|
|
403
|
+
stateStore.appendLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
|
|
404
|
+
try {
|
|
405
|
+
await wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
|
|
406
|
+
stateStore.appendLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
407
|
+
return { status: "sent" };
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
411
|
+
stateStore.appendLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
|
|
412
|
+
logError(`Failed to send WeCom ${context}: ${message}`);
|
|
413
|
+
return { status: "context_unavailable", error, target };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
303
416
|
const startedAtMs = Date.now();
|
|
304
417
|
stateStore.appendLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
305
418
|
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
@@ -355,11 +468,11 @@ async function main() {
|
|
|
355
468
|
}
|
|
356
469
|
return { status: "failed" };
|
|
357
470
|
};
|
|
358
|
-
const queueWechatMessage = (senderId, text, context = "message") => {
|
|
471
|
+
const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
|
|
359
472
|
return queueWechatTextAction(async () => {
|
|
360
|
-
const result = await sendWechatMessageNow(senderId, text, context);
|
|
473
|
+
const result = await sendWechatMessageNow(senderId, text, context, targetOverride);
|
|
361
474
|
if (result.status === "context_unavailable") {
|
|
362
|
-
const pending = pendingWechatMessages.enqueue(senderId, text, context);
|
|
475
|
+
const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
|
|
363
476
|
if (pending) {
|
|
364
477
|
stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
|
|
365
478
|
}
|
|
@@ -370,7 +483,7 @@ async function main() {
|
|
|
370
483
|
const flushPendingWechatMessages = () => {
|
|
371
484
|
return queueWechatTextAction(async () => {
|
|
372
485
|
for (const pending of pendingWechatMessages.list()) {
|
|
373
|
-
const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
|
|
486
|
+
const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
|
|
374
487
|
if (result.status === "sent") {
|
|
375
488
|
pendingWechatMessages.remove(pending.id);
|
|
376
489
|
stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
@@ -427,6 +540,10 @@ async function main() {
|
|
|
427
540
|
drainingDeferredInboundMessages = true;
|
|
428
541
|
try {
|
|
429
542
|
stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
|
|
543
|
+
if (nextDeferred.channelMessage) {
|
|
544
|
+
activeWecomConversation = nextDeferred.channelMessage.conversation;
|
|
545
|
+
lastWecomConversation = nextDeferred.channelMessage.conversation;
|
|
546
|
+
}
|
|
430
547
|
const nextTask = await dispatchInboundWechatText({
|
|
431
548
|
message: nextDeferred.message,
|
|
432
549
|
options,
|
|
@@ -515,6 +632,12 @@ async function main() {
|
|
|
515
632
|
catch {
|
|
516
633
|
// Best effort shutdown.
|
|
517
634
|
}
|
|
635
|
+
try {
|
|
636
|
+
wecomTransport?.stop();
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// Best effort shutdown.
|
|
640
|
+
}
|
|
518
641
|
controller.clearLocalClientEndpoint();
|
|
519
642
|
stateStore.releaseLock();
|
|
520
643
|
};
|
|
@@ -594,6 +717,7 @@ async function main() {
|
|
|
594
717
|
maybeDrainDeferredInboundMessages,
|
|
595
718
|
clearActiveTask: () => {
|
|
596
719
|
activeTask = null;
|
|
720
|
+
activeWecomConversation = null;
|
|
597
721
|
},
|
|
598
722
|
syncSharedSessionState: () => {
|
|
599
723
|
syncSharedSessionState(stateStore, adapter);
|
|
@@ -602,6 +726,8 @@ async function main() {
|
|
|
602
726
|
controller.syncLocalClientEndpoint();
|
|
603
727
|
},
|
|
604
728
|
requestShutdown,
|
|
729
|
+
wecomTransport,
|
|
730
|
+
getWecomTarget: () => activeWecomConversation ?? lastWecomConversation,
|
|
605
731
|
});
|
|
606
732
|
await adapter.start();
|
|
607
733
|
if (!ensureRuntimeOwnership()) {
|
|
@@ -609,13 +735,98 @@ async function main() {
|
|
|
609
735
|
}
|
|
610
736
|
syncSharedSessionState(stateStore, adapter);
|
|
611
737
|
controller.syncLocalClientEndpoint();
|
|
612
|
-
stateStore.appendLog(`Bridge started with adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
613
|
-
|
|
738
|
+
stateStore.appendLog(`Bridge started with channel=${options.channelId} adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
739
|
+
if (wecomTransport) {
|
|
740
|
+
wecomTransport.setHandlers({
|
|
741
|
+
onMessage: async (channelMessage) => {
|
|
742
|
+
if (shutdownPromise || !ensureRuntimeOwnership()) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
currentWecomConversation = channelMessage.conversation;
|
|
746
|
+
lastWecomConversation = channelMessage.conversation;
|
|
747
|
+
try {
|
|
748
|
+
if (pendingWechatMessages.list().length > 0) {
|
|
749
|
+
await flushPendingWechatMessages();
|
|
750
|
+
}
|
|
751
|
+
const message = toLegacyInboundWechatMessage(channelMessage);
|
|
752
|
+
stateStore.touchActivity(message.createdAt);
|
|
753
|
+
let nextTask = null;
|
|
754
|
+
try {
|
|
755
|
+
nextTask = await handleInboundMessage({
|
|
756
|
+
message,
|
|
757
|
+
channelMessage,
|
|
758
|
+
options,
|
|
759
|
+
stateStore,
|
|
760
|
+
adapter,
|
|
761
|
+
resumeCoordinator,
|
|
762
|
+
queueWechatMessage,
|
|
763
|
+
outputBatcher,
|
|
764
|
+
clearActiveTask: () => {
|
|
765
|
+
activeTask = null;
|
|
766
|
+
activeWecomConversation = null;
|
|
767
|
+
},
|
|
768
|
+
deferInboundMessage: async (nextMessage) => {
|
|
769
|
+
deferredInboundMessages.push({
|
|
770
|
+
message: nextMessage,
|
|
771
|
+
channelMessage,
|
|
772
|
+
});
|
|
773
|
+
stateStore.appendLog(`deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`);
|
|
774
|
+
await queueWechatMessage(nextMessage.senderId, formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length));
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
780
|
+
logError(errorText);
|
|
781
|
+
stateStore.appendLog(`inbound_error: ${errorText}`);
|
|
782
|
+
await queueWechatMessage(message.senderId, formatUserFacingInboundError({
|
|
783
|
+
adapter: options.adapter,
|
|
784
|
+
cwd: options.cwd,
|
|
785
|
+
errorText,
|
|
786
|
+
}), "inbound_error");
|
|
787
|
+
}
|
|
788
|
+
if (nextTask) {
|
|
789
|
+
activeTask = nextTask;
|
|
790
|
+
activeWecomConversation = channelMessage.conversation;
|
|
791
|
+
}
|
|
792
|
+
syncSharedSessionState(stateStore, adapter);
|
|
793
|
+
await maybeDrainDeferredInboundMessages();
|
|
794
|
+
}
|
|
795
|
+
finally {
|
|
796
|
+
currentWecomConversation = null;
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
onUnauthorized: async (senderId, chatType) => {
|
|
800
|
+
stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
|
|
801
|
+
if (chatType === "direct") {
|
|
802
|
+
await wecomTransport.sendText({
|
|
803
|
+
channelId: "wecom",
|
|
804
|
+
accountId: wecomAccount.botId,
|
|
805
|
+
conversationId: senderId,
|
|
806
|
+
recipientId: senderId,
|
|
807
|
+
metadata: { chatType: "direct" },
|
|
808
|
+
}, "Unauthorized.", "notice");
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
onFatal: async (error) => {
|
|
812
|
+
stateStore.appendLog(`wecom_fatal_error: ${error.message}`);
|
|
813
|
+
requestShutdown(error.message, 1);
|
|
814
|
+
},
|
|
815
|
+
onConnected: async () => {
|
|
816
|
+
if (pendingWechatMessages.list().length > 0) {
|
|
817
|
+
await flushPendingWechatMessages();
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
wecomTransport.start();
|
|
822
|
+
await wecomTransport.waitUntilConnected();
|
|
823
|
+
}
|
|
824
|
+
log(`${options.channelId === "wecom" ? "WeCom" : "WeChat"} bridge is ready for adapter "${options.adapter}".`);
|
|
614
825
|
log(`Working directory: ${options.cwd}`);
|
|
615
826
|
if (options.profile) {
|
|
616
827
|
log(`Profile: ${options.profile}`);
|
|
617
828
|
}
|
|
618
|
-
log(`Authorized WeChat user: ${credentials.userId}`);
|
|
829
|
+
log(`Authorized ${options.channelId === "wecom" ? "WeCom" : "WeChat"} user: ${credentials.userId}`);
|
|
619
830
|
if (options.adapter === "codex") {
|
|
620
831
|
log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
|
|
621
832
|
}
|
|
@@ -633,6 +844,12 @@ async function main() {
|
|
|
633
844
|
cwd: options.cwd,
|
|
634
845
|
});
|
|
635
846
|
await queueWechatMessage(credentials.userId, welcomeText);
|
|
847
|
+
if (options.channelId === "wecom") {
|
|
848
|
+
while (!shutdownPromise && ensureRuntimeOwnership()) {
|
|
849
|
+
await delay(1_000);
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
636
853
|
while (true) {
|
|
637
854
|
if (shutdownPromise) {
|
|
638
855
|
// Shutdown (e.g. SIGINT) was requested while we were awaiting the
|
|
@@ -763,20 +980,28 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
763
980
|
}
|
|
764
981
|
}
|
|
765
982
|
function wireAdapterEvents(params) {
|
|
766
|
-
const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
767
|
-
const channelPort =
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
983
|
+
const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
|
|
984
|
+
const channelPort = options.channelId === "wecom"
|
|
985
|
+
? new WecomChannelPort({
|
|
986
|
+
transport: wecomTransport,
|
|
987
|
+
sendText: (target, text, kind) => queueWechatMessage(stateStore.getState().authorizedUserId, text, toWechatSendContext(kind), target),
|
|
988
|
+
onEmptyVisibleReply: (adapter, rawText) => {
|
|
989
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
|
|
990
|
+
},
|
|
991
|
+
})
|
|
992
|
+
: new WechatChannelPort({
|
|
993
|
+
sendText: (recipientId, text, context) => queueWechatMessage(recipientId, text, context),
|
|
994
|
+
sendImage: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
|
|
995
|
+
sendFile: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
|
|
996
|
+
sendVoice: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
|
|
997
|
+
sendVideo: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVideo(filePath, { recipientId })),
|
|
998
|
+
onEmptyVisibleReply: (adapter, rawText) => {
|
|
999
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
|
|
1000
|
+
},
|
|
1001
|
+
onTextSent: (_adapter, text) => {
|
|
1002
|
+
stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
780
1005
|
let lastFinalReplyAtMs = 0;
|
|
781
1006
|
let eventForwardChain = Promise.resolve();
|
|
782
1007
|
adapter.setEventSink((event) => {
|
|
@@ -808,11 +1033,13 @@ function wireAdapterEvents(params) {
|
|
|
808
1033
|
stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
|
|
809
1034
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
810
1035
|
await channelPort.send({
|
|
811
|
-
target:
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1036
|
+
target: options.channelId === "wecom"
|
|
1037
|
+
? getWecomTarget()
|
|
1038
|
+
: {
|
|
1039
|
+
channelId: "wechat",
|
|
1040
|
+
conversationId: authorizedUserId,
|
|
1041
|
+
recipientId: authorizedUserId,
|
|
1042
|
+
},
|
|
816
1043
|
kind: "final_reply",
|
|
817
1044
|
text: next.text,
|
|
818
1045
|
adapter: options.adapter,
|
|
@@ -840,7 +1067,9 @@ function wireAdapterEvents(params) {
|
|
|
840
1067
|
if (thinkingPreview) {
|
|
841
1068
|
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
842
1069
|
trackWechatForwardTask((async () => {
|
|
843
|
-
await queueWechatMessage(authorizedUserId,
|
|
1070
|
+
await queueWechatMessage(authorizedUserId, options.channelId === "wecom"
|
|
1071
|
+
? `Processing: ${thinkingPreview}`
|
|
1072
|
+
: `思考: ${thinkingPreview}`, "thinking");
|
|
844
1073
|
})());
|
|
845
1074
|
}
|
|
846
1075
|
}
|
|
@@ -950,11 +1179,13 @@ function formatInboundMessagePreview(message) {
|
|
|
950
1179
|
return "(empty)";
|
|
951
1180
|
}
|
|
952
1181
|
async function handleInboundMessage(params) {
|
|
953
|
-
const { message, } = params;
|
|
1182
|
+
const { message, channelMessage, } = params;
|
|
954
1183
|
const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, deferInboundMessage, } = params;
|
|
955
1184
|
const state = stateStore.getState();
|
|
956
1185
|
if (message.senderId !== state.authorizedUserId) {
|
|
957
|
-
await queueWechatMessage(message.senderId,
|
|
1186
|
+
await queueWechatMessage(message.senderId, options.channelId === "wecom"
|
|
1187
|
+
? "Unauthorized. This bridge only accepts messages from the paired WeCom operator."
|
|
1188
|
+
: "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
958
1189
|
return null;
|
|
959
1190
|
}
|
|
960
1191
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
@@ -968,7 +1199,7 @@ async function handleInboundMessage(params) {
|
|
|
968
1199
|
return null;
|
|
969
1200
|
case "resume": {
|
|
970
1201
|
if (!isWechatResumeEnabled(options.adapter)) {
|
|
971
|
-
await queueWechatMessage(message.senderId,
|
|
1202
|
+
await queueWechatMessage(message.senderId, `${options.channelId === "wecom" ? "WeCom" : "WeChat"} /resume is disabled in ${options.adapter} mode. Use /resume directly inside "${options.channelId}-${options.adapter}"; the remote channel will follow the active local session.`);
|
|
972
1203
|
return null;
|
|
973
1204
|
}
|
|
974
1205
|
try {
|
|
@@ -1081,7 +1312,7 @@ async function handleInboundMessage(params) {
|
|
|
1081
1312
|
}
|
|
1082
1313
|
const adapterState = adapter.getState();
|
|
1083
1314
|
const routeResult = await routeBridgeMessage({
|
|
1084
|
-
message: toChannelInboundMessage(message),
|
|
1315
|
+
message: channelMessage ?? toChannelInboundMessage(message),
|
|
1085
1316
|
authorized: true,
|
|
1086
1317
|
command: null,
|
|
1087
1318
|
adapterState,
|
|
@@ -1136,10 +1367,12 @@ async function dispatchInboundWechatText(params) {
|
|
|
1136
1367
|
inputPreview: truncatePreview(preview, 180),
|
|
1137
1368
|
};
|
|
1138
1369
|
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
|
|
1139
|
-
await adapter.sendInput(
|
|
1370
|
+
await adapter.sendInput(options.channelId === "wecom"
|
|
1371
|
+
? buildWecomInboundPrompt(message.text, message.attachments)
|
|
1372
|
+
: buildWechatInboundPrompt(message.text, message.attachments));
|
|
1140
1373
|
return activeTask;
|
|
1141
1374
|
}
|
|
1142
|
-
const isDirectRun =
|
|
1375
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
1143
1376
|
if (isDirectRun) {
|
|
1144
1377
|
main().catch((err) => {
|
|
1145
1378
|
logError(describeWechatTransportError(err));
|
|
@@ -4,7 +4,10 @@ import path from "node:path";
|
|
|
4
4
|
import { getWorkspaceChannelPaths } from "../../wechat/channel-config.js";
|
|
5
5
|
import { writeJsonFileAtomic } from "../../utils/atomic-file.js";
|
|
6
6
|
export function getPendingWechatMessagesFile(cwd) {
|
|
7
|
-
return
|
|
7
|
+
return getPendingChannelMessagesFile(cwd, "wechat");
|
|
8
|
+
}
|
|
9
|
+
export function getPendingChannelMessagesFile(cwd, channelId) {
|
|
10
|
+
return path.join(getWorkspaceChannelPaths(cwd).workspaceDir, `pending-${channelId}-messages.json`);
|
|
8
11
|
}
|
|
9
12
|
function readPendingMessages(filePath) {
|
|
10
13
|
try {
|
|
@@ -38,9 +41,12 @@ export class PendingWechatMessageStore {
|
|
|
38
41
|
this.messages = readPendingMessages(filePath);
|
|
39
42
|
}
|
|
40
43
|
list() {
|
|
41
|
-
return this.messages.map((message) => ({
|
|
44
|
+
return this.messages.map((message) => ({
|
|
45
|
+
...message,
|
|
46
|
+
target: message.target ? { ...message.target } : undefined,
|
|
47
|
+
}));
|
|
42
48
|
}
|
|
43
|
-
enqueue(recipientId, text, context) {
|
|
49
|
+
enqueue(recipientId, text, context, target) {
|
|
44
50
|
const normalizedRecipientId = recipientId.trim();
|
|
45
51
|
const normalizedText = text.trim();
|
|
46
52
|
if (!normalizedRecipientId || !normalizedText) {
|
|
@@ -52,6 +58,7 @@ export class PendingWechatMessageStore {
|
|
|
52
58
|
text: normalizedText,
|
|
53
59
|
context,
|
|
54
60
|
queuedAt: new Date().toISOString(),
|
|
61
|
+
target: target ? { ...target } : undefined,
|
|
55
62
|
};
|
|
56
63
|
this.messages.push(message);
|
|
57
64
|
this.persist();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { formatFinalReplyMessage, parseWechatFinalReply, sanitizeWechatFinalReplyText, } from "../../bridge/bridge-utils.js";
|
|
3
|
+
export async function forwardWecomFinalReply(params) {
|
|
4
|
+
const parsed = parseWechatFinalReply(params.rawText);
|
|
5
|
+
const sanitized = sanitizeWechatFinalReplyText(params.adapter, parsed.visibleText);
|
|
6
|
+
const visibleText = formatFinalReplyMessage(params.adapter, sanitized).trim();
|
|
7
|
+
if (visibleText) {
|
|
8
|
+
await params.sender.sendText(visibleText);
|
|
9
|
+
}
|
|
10
|
+
else if (params.adapter === "opencode" && parsed.visibleText.trim()) {
|
|
11
|
+
params.onEmptyVisibleReply?.(parsed.visibleText);
|
|
12
|
+
await params.sender.sendText("OpenCode did not produce a visible reply. Check the local terminal output or retry the message.");
|
|
13
|
+
}
|
|
14
|
+
for (const attachment of parsed.attachments) {
|
|
15
|
+
try {
|
|
16
|
+
await params.sender.sendAttachment({
|
|
17
|
+
kind: attachment.kind,
|
|
18
|
+
path: attachment.path,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
23
|
+
await params.sender.sendText(`Failed to send ${attachment.kind} attachment ${path.basename(attachment.path)}: ${errorText}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|