cli-wechat-bridge 1.1.8 → 1.2.0
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 +17 -4
- package/bin/_run-entry.mjs +6 -1
- package/bin/wechat-check-update.mjs +6 -1
- package/bin/wechat-claude.mjs +6 -1
- package/bin/wechat-codex.mjs +6 -1
- package/bin/wechat-daemon.mjs +6 -1
- package/bin/wechat-opencode.mjs +6 -1
- package/bin/wechat-pi.mjs +6 -1
- package/bin/wechat-setup.mjs +6 -1
- package/bin/wecom-check-update.mjs +6 -1
- package/bin/wecom-claude.mjs +6 -1
- package/bin/wecom-codex.mjs +6 -1
- package/bin/wecom-daemon.mjs +6 -1
- package/bin/wecom-opencode.mjs +6 -1
- package/bin/wecom-pi.mjs +6 -1
- package/bin/wecom-setup.mjs +6 -1
- package/dist/bridge/bridge-adapters.claude.js +6 -0
- package/dist/bridge/bridge-adapters.codex.js +6 -0
- package/dist/bridge/bridge-adapters.core.js +6 -0
- package/dist/bridge/bridge-adapters.opencode.js +6 -0
- package/dist/bridge/bridge-adapters.pi.js +6 -0
- package/dist/bridge/bridge-adapters.shared.js +6 -0
- package/dist/bridge/bridge-controller.js +6 -0
- package/dist/bridge/bridge-final-reply.js +6 -0
- package/dist/bridge/bridge-process-reaper.js +6 -0
- package/dist/bridge/bridge-state.js +6 -0
- package/dist/bridge/bridge-utils.js +6 -31
- package/dist/bridge/claude-hook.js +6 -0
- package/dist/bridge/native-terminal-control.js +6 -0
- package/dist/bridge/wechat-bridge.js +148 -241
- package/dist/bridge/wechat-forwarding.js +6 -0
- package/dist/bridge/wechat-outbound-queue.js +6 -0
- package/dist/channels/wechat/bridge-final-reply.js +13 -22
- package/dist/channels/wechat/index.js +6 -0
- package/dist/channels/wechat/wechat-driver.js +191 -0
- package/dist/channels/wechat/wechat-forwarding.js +18 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +39 -0
- package/dist/channels/wecom/bridge-final-reply.js +6 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +6 -0
- package/dist/channels/wecom/wecom-config.js +6 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +6 -0
- package/dist/channels/wecom/wecom-transport.js +6 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +6 -0
- package/dist/companion/codex-visible-client-link.js +6 -0
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion-start.js +6 -0
- package/dist/companion/local-companion.js +6 -0
- package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
- package/dist/companion/pi-tui-bridge-extension.js +6 -0
- package/dist/core/bridge-control.js +2 -0
- package/dist/core/bridge-defer.js +25 -0
- package/dist/core/channel-driver.js +1 -0
- package/dist/core/conversation-routing.js +11 -1
- package/dist/core/direct-run.js +6 -0
- package/dist/core/inbound-message-claims.js +6 -0
- package/dist/core/outbound-target.js +32 -0
- package/dist/core/text-utils.js +6 -0
- package/dist/core/turn-coordinator.js +85 -0
- package/dist/daemon/daemon-link.js +6 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +451 -173
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +7 -1
- package/dist/i18n/messages-zh.js +7 -1
- package/dist/runtime/create-runtime-host.js +6 -0
- package/dist/utils/atomic-file.js +6 -0
- package/dist/utils/doctor.js +6 -0
- package/dist/utils/version-checker.js +6 -0
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +6 -0
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +73 -0
- package/package.json +1 -1
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
4
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
5
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
6
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
7
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
2
8
|
import path from "node:path";
|
|
3
9
|
import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
|
|
4
10
|
import { delay } from "./bridge-adapters.shared.js";
|
|
5
11
|
import { t } from "../i18n/index.js";
|
|
6
12
|
import { BridgeController } from "./bridge-controller.js";
|
|
7
13
|
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "./bridge-session-resume.js";
|
|
8
|
-
import {
|
|
14
|
+
import { formatUserFacingBridgeFatalError, formatUserFacingInboundError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
|
|
9
15
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
10
16
|
import { BridgeStateStore } from "./bridge-state.js";
|
|
11
17
|
import { getProcessRecordByPid, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "./bridge-process-reaper.js";
|
|
12
18
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
13
19
|
import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
|
|
14
20
|
import { routeBridgeMessage } from "../core/bridge-message-router.js";
|
|
15
|
-
import {
|
|
21
|
+
import { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessage, isRetryableDeferredCodexDrainError, shouldDeferCodexInboundMessage, } from "../core/bridge-defer.js";
|
|
22
|
+
import { TurnCoordinator } from "../core/turn-coordinator.js";
|
|
23
|
+
import { resolveOutboundConversationTarget } from "../core/outbound-target.js";
|
|
16
24
|
import { handleAdapterControl } from "./adapter-control.js";
|
|
17
25
|
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
18
26
|
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
@@ -20,9 +28,10 @@ import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
|
20
28
|
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
21
29
|
import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
|
|
22
30
|
import { WecomTransport } from "../channels/wecom/wecom-transport.js";
|
|
23
|
-
import {
|
|
31
|
+
import { WecomChannelDriver } from "../channels/wecom/wecom-driver.js";
|
|
32
|
+
import { WechatChannelDriver } from "../channels/wechat/wechat-driver.js";
|
|
24
33
|
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";
|
|
25
|
-
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError,
|
|
34
|
+
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, } from "../wechat/wechat-transport.js";
|
|
26
35
|
import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
27
36
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
28
37
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
@@ -80,31 +89,7 @@ function toPendingUserInput(request) {
|
|
|
80
89
|
createdAt: nowIso(),
|
|
81
90
|
};
|
|
82
91
|
}
|
|
83
|
-
export
|
|
84
|
-
return (params.adapter === "codex" &&
|
|
85
|
-
!params.hasPendingConfirmation &&
|
|
86
|
-
!params.hasSystemCommand &&
|
|
87
|
-
params.activeTurnOrigin === "local" &&
|
|
88
|
-
(params.status === "busy" || params.status === "awaiting_approval"));
|
|
89
|
-
}
|
|
90
|
-
export function canDrainDeferredCodexInboundQueue(params) {
|
|
91
|
-
return (params.adapter === "codex" &&
|
|
92
|
-
params.deferredCount > 0 &&
|
|
93
|
-
!params.hasPendingConfirmation &&
|
|
94
|
-
!params.hasPendingUserInput &&
|
|
95
|
-
!params.hasPendingApproval &&
|
|
96
|
-
!params.hasActiveTask &&
|
|
97
|
-
!params.activeTurnId &&
|
|
98
|
-
params.status !== "busy" &&
|
|
99
|
-
params.status !== "awaiting_approval" &&
|
|
100
|
-
params.status !== "awaiting_input");
|
|
101
|
-
}
|
|
102
|
-
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
103
|
-
return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
|
|
104
|
-
}
|
|
105
|
-
export function isRetryableDeferredCodexDrainError(errorText) {
|
|
106
|
-
return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
|
|
107
|
-
}
|
|
92
|
+
export { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessage, isRetryableDeferredCodexDrainError, shouldDeferCodexInboundMessage, } from "../core/bridge-defer.js";
|
|
108
93
|
export function parseCliArgs(argv) {
|
|
109
94
|
let adapter = null;
|
|
110
95
|
let commandOverride;
|
|
@@ -191,24 +176,6 @@ export function parseCliArgs(argv) {
|
|
|
191
176
|
channelId,
|
|
192
177
|
};
|
|
193
178
|
}
|
|
194
|
-
function toWecomOutputKind(context) {
|
|
195
|
-
const known = new Set([
|
|
196
|
-
"status",
|
|
197
|
-
"notice",
|
|
198
|
-
"thinking",
|
|
199
|
-
"final_reply",
|
|
200
|
-
"approval_required",
|
|
201
|
-
"user_input_required",
|
|
202
|
-
"mirrored_input",
|
|
203
|
-
"task_failed",
|
|
204
|
-
"fatal_error",
|
|
205
|
-
]);
|
|
206
|
-
return known.has(context)
|
|
207
|
-
? context
|
|
208
|
-
: context === "mirrored_user_input"
|
|
209
|
-
? "mirrored_input"
|
|
210
|
-
: "notice";
|
|
211
|
-
}
|
|
212
179
|
function toWechatSendContext(kind) {
|
|
213
180
|
if (kind === "mirrored_input") {
|
|
214
181
|
return "mirrored_user_input";
|
|
@@ -285,6 +252,18 @@ async function main() {
|
|
|
285
252
|
},
|
|
286
253
|
})
|
|
287
254
|
: null;
|
|
255
|
+
const channelDriver = wecomTransport
|
|
256
|
+
? new WecomChannelDriver({
|
|
257
|
+
transport: wecomTransport,
|
|
258
|
+
accountId: wecomAccount?.botId,
|
|
259
|
+
operatorId: credentials.userId,
|
|
260
|
+
logError,
|
|
261
|
+
})
|
|
262
|
+
: new WechatChannelDriver({
|
|
263
|
+
transport,
|
|
264
|
+
logError,
|
|
265
|
+
buildInboundPrompt: (text, attachments) => buildWechatInboundPrompt(text, attachments.filter((attachment) => attachment.kind === "image" || attachment.kind === "file")),
|
|
266
|
+
});
|
|
288
267
|
// 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
|
|
289
268
|
// unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
|
|
290
269
|
// 否则会与强制退出 teardown 竞态。
|
|
@@ -363,17 +342,13 @@ async function main() {
|
|
|
363
342
|
let textSendChain = Promise.resolve();
|
|
364
343
|
let attachmentSendChain = Promise.resolve();
|
|
365
344
|
const pendingWechatForwardTasks = new Set();
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
};
|
|
373
|
-
const turnState = {
|
|
374
|
-
activeTask: null,
|
|
375
|
-
lastConversation: defaultWecomConversation,
|
|
376
|
-
};
|
|
345
|
+
const defaultConversation = channelDriver.defaultConversation();
|
|
346
|
+
const operatorId = credentials.userId;
|
|
347
|
+
const operatorConversation = defaultConversation
|
|
348
|
+
?? channelDriver.directConversation(operatorId);
|
|
349
|
+
const turns = new TurnCoordinator({
|
|
350
|
+
initialLastConversation: defaultConversation,
|
|
351
|
+
});
|
|
377
352
|
const deferredInboundMessages = [];
|
|
378
353
|
let drainingDeferredInboundMessages = false;
|
|
379
354
|
let consecutivePollFailures = 0;
|
|
@@ -389,101 +364,28 @@ async function main() {
|
|
|
389
364
|
return run;
|
|
390
365
|
};
|
|
391
366
|
const sendWechatMessageNow = async (senderId, text, context = "message", targetOverride) => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
407
|
-
const startedAtMs = Date.now();
|
|
408
|
-
stateStore.appendLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
|
|
409
|
-
try {
|
|
410
|
-
await wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
|
|
411
|
-
stateStore.appendLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
412
|
-
return { status: "sent" };
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
416
|
-
stateStore.appendLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
|
|
417
|
-
logError(`Failed to send WeCom ${context}: ${message}`);
|
|
418
|
-
return { status: "context_unavailable", error, target };
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
const startedAtMs = Date.now();
|
|
422
|
-
stateStore.appendLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
423
|
-
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
424
|
-
try {
|
|
425
|
-
await transport.sendText(senderId, text);
|
|
426
|
-
stateStore.appendLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
427
|
-
return { status: "sent" };
|
|
428
|
-
}
|
|
429
|
-
catch (err) {
|
|
430
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
431
|
-
stateStore.appendLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
432
|
-
}
|
|
433
|
-
if (isWechatContextUnavailableError(err)) {
|
|
434
|
-
if (isWechatContextTokenStaleError(err)) {
|
|
435
|
-
transport.clearCachedContextToken(senderId);
|
|
436
|
-
}
|
|
437
|
-
const hint = "WeChat conversation context is stale or unavailable. Ask the WeChat owner to send any message first, then local terminal replies can sync back to WeChat.";
|
|
438
|
-
logError(`Failed to send WeChat ${context}: ${hint}`);
|
|
439
|
-
stateStore.appendLog(isWechatContextTokenStaleError(err)
|
|
440
|
-
? formatWechatContextTokenStaleLogEntry({
|
|
441
|
-
context,
|
|
442
|
-
recipientId: senderId,
|
|
443
|
-
error: err,
|
|
444
|
-
})
|
|
445
|
-
: formatWechatSendFailureLogEntry({
|
|
446
|
-
context,
|
|
447
|
-
recipientId: senderId,
|
|
448
|
-
error: err,
|
|
449
|
-
}));
|
|
450
|
-
return { status: "context_unavailable", error: err };
|
|
451
|
-
}
|
|
452
|
-
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
453
|
-
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
454
|
-
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
455
|
-
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
456
|
-
context,
|
|
457
|
-
recipientId: senderId,
|
|
458
|
-
attempt,
|
|
459
|
-
delayMs,
|
|
460
|
-
error: err,
|
|
461
|
-
}));
|
|
462
|
-
await delay(delayMs);
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
|
|
466
|
-
stateStore.appendLog(formatWechatSendFailureLogEntry({
|
|
467
|
-
context,
|
|
468
|
-
recipientId: senderId,
|
|
469
|
-
error: err,
|
|
470
|
-
}));
|
|
471
|
-
return { status: "failed" };
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
return { status: "failed" };
|
|
367
|
+
const target = resolveOutboundConversationTarget({
|
|
368
|
+
senderId,
|
|
369
|
+
operatorId,
|
|
370
|
+
override: targetOverride,
|
|
371
|
+
multiConversation: channelDriver.capabilities.multiConversation,
|
|
372
|
+
operatorTarget: turns.resolveTarget(operatorConversation),
|
|
373
|
+
directConversation: (id) => channelDriver.directConversation(id),
|
|
374
|
+
});
|
|
375
|
+
return channelDriver.sendText({
|
|
376
|
+
target,
|
|
377
|
+
text,
|
|
378
|
+
context,
|
|
379
|
+
log: (entry) => stateStore.appendLog(entry),
|
|
380
|
+
});
|
|
475
381
|
};
|
|
476
382
|
const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
|
|
477
|
-
const queuedTarget =
|
|
478
|
-
? targetOverride ??
|
|
479
|
-
active: turnState.activeConversation,
|
|
480
|
-
last: turnState.lastConversation,
|
|
481
|
-
fallback: defaultWecomConversation,
|
|
482
|
-
})
|
|
383
|
+
const queuedTarget = senderId === operatorId && channelDriver.capabilities.multiConversation
|
|
384
|
+
? targetOverride ?? turns.resolveTarget(operatorConversation)
|
|
483
385
|
: targetOverride;
|
|
484
386
|
return queueWechatTextAction(async () => {
|
|
485
387
|
const result = await sendWechatMessageNow(senderId, text, context, queuedTarget);
|
|
486
|
-
if (result.status === "
|
|
388
|
+
if (result.status === "target_stale") {
|
|
487
389
|
const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
|
|
488
390
|
if (pending) {
|
|
489
391
|
stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
|
|
@@ -501,7 +403,7 @@ async function main() {
|
|
|
501
403
|
stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
502
404
|
continue;
|
|
503
405
|
}
|
|
504
|
-
if (result.status === "
|
|
406
|
+
if (result.status === "target_stale") {
|
|
505
407
|
break;
|
|
506
408
|
}
|
|
507
409
|
stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
@@ -526,12 +428,8 @@ async function main() {
|
|
|
526
428
|
}
|
|
527
429
|
};
|
|
528
430
|
const outputBatcher = new OutputBatcher(async (text) => {
|
|
529
|
-
await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message",
|
|
530
|
-
?
|
|
531
|
-
active: turnState.activeConversation,
|
|
532
|
-
last: turnState.lastConversation,
|
|
533
|
-
fallback: defaultWecomConversation,
|
|
534
|
-
})
|
|
431
|
+
await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message", channelDriver.capabilities.multiConversation
|
|
432
|
+
? turns.resolveTarget(operatorConversation)
|
|
535
433
|
: undefined);
|
|
536
434
|
});
|
|
537
435
|
const maybeDrainDeferredInboundMessages = async () => {
|
|
@@ -547,7 +445,7 @@ async function main() {
|
|
|
547
445
|
hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
|
|
548
446
|
hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
|
|
549
447
|
hasPendingApproval: Boolean(adapterState.pendingApproval),
|
|
550
|
-
hasActiveTask:
|
|
448
|
+
hasActiveTask: turns.hasActiveTask,
|
|
551
449
|
})) {
|
|
552
450
|
return;
|
|
553
451
|
}
|
|
@@ -559,7 +457,7 @@ async function main() {
|
|
|
559
457
|
try {
|
|
560
458
|
stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
|
|
561
459
|
const nextTask = createActiveTask(nextDeferred.message);
|
|
562
|
-
const lease =
|
|
460
|
+
const lease = turns.beginTurn(nextTask, nextDeferred.channelMessage?.conversation);
|
|
563
461
|
if (!lease) {
|
|
564
462
|
deferredInboundMessages.unshift(nextDeferred);
|
|
565
463
|
return;
|
|
@@ -567,6 +465,7 @@ async function main() {
|
|
|
567
465
|
try {
|
|
568
466
|
await dispatchInboundWechatText({
|
|
569
467
|
message: nextDeferred.message,
|
|
468
|
+
channelDriver,
|
|
570
469
|
options,
|
|
571
470
|
stateStore,
|
|
572
471
|
adapter,
|
|
@@ -574,7 +473,7 @@ async function main() {
|
|
|
574
473
|
});
|
|
575
474
|
}
|
|
576
475
|
catch (error) {
|
|
577
|
-
|
|
476
|
+
turns.rollback(lease);
|
|
578
477
|
throw error;
|
|
579
478
|
}
|
|
580
479
|
}
|
|
@@ -664,6 +563,12 @@ async function main() {
|
|
|
664
563
|
catch {
|
|
665
564
|
// Best effort shutdown.
|
|
666
565
|
}
|
|
566
|
+
try {
|
|
567
|
+
await channelDriver.endAllTyping?.();
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Best effort shutdown.
|
|
571
|
+
}
|
|
667
572
|
controller.clearLocalClientEndpoint();
|
|
668
573
|
stateStore.releaseLock();
|
|
669
574
|
};
|
|
@@ -732,6 +637,7 @@ async function main() {
|
|
|
732
637
|
try {
|
|
733
638
|
wireAdapterEvents({
|
|
734
639
|
adapter,
|
|
640
|
+
channelDriver,
|
|
735
641
|
resumeCoordinator,
|
|
736
642
|
options,
|
|
737
643
|
transport,
|
|
@@ -741,10 +647,10 @@ async function main() {
|
|
|
741
647
|
queueWechatMessage,
|
|
742
648
|
trackWechatForwardTask,
|
|
743
649
|
maybeDrainDeferredInboundMessages,
|
|
744
|
-
getActiveTask: () =>
|
|
650
|
+
getActiveTask: () => turns.activeTask,
|
|
745
651
|
clearActiveTask: (expectedTask) => {
|
|
746
652
|
if (expectedTask) {
|
|
747
|
-
|
|
653
|
+
turns.complete(expectedTask);
|
|
748
654
|
}
|
|
749
655
|
},
|
|
750
656
|
syncSharedSessionState: () => {
|
|
@@ -755,11 +661,7 @@ async function main() {
|
|
|
755
661
|
},
|
|
756
662
|
requestShutdown,
|
|
757
663
|
wecomTransport,
|
|
758
|
-
getWecomTarget: () =>
|
|
759
|
-
active: turnState.activeConversation,
|
|
760
|
-
last: turnState.lastConversation,
|
|
761
|
-
fallback: defaultWecomConversation,
|
|
762
|
-
}),
|
|
664
|
+
getWecomTarget: () => turns.resolveTarget(operatorConversation),
|
|
763
665
|
});
|
|
764
666
|
await adapter.start();
|
|
765
667
|
if (!ensureRuntimeOwnership()) {
|
|
@@ -768,24 +670,25 @@ async function main() {
|
|
|
768
670
|
syncSharedSessionState(stateStore, adapter);
|
|
769
671
|
controller.syncLocalClientEndpoint();
|
|
770
672
|
stateStore.appendLog(`Bridge started with channel=${options.channelId} adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
771
|
-
if (
|
|
772
|
-
|
|
773
|
-
|
|
673
|
+
if (channelDriver.start) {
|
|
674
|
+
await channelDriver.start({
|
|
675
|
+
onInboundMessage: async (channelMessage) => {
|
|
774
676
|
if (shutdownPromise || !ensureRuntimeOwnership()) {
|
|
775
677
|
return;
|
|
776
678
|
}
|
|
777
|
-
|
|
679
|
+
turns.observeConversation(channelMessage.conversation);
|
|
778
680
|
const queueInboundReply = (senderId, text, context) => queueWechatMessage(senderId, text, context, channelMessage.conversation);
|
|
779
681
|
if (pendingWechatMessages.list().length > 0) {
|
|
780
682
|
await flushPendingWechatMessages();
|
|
781
683
|
}
|
|
782
684
|
const message = toLegacyInboundWechatMessage(channelMessage);
|
|
783
|
-
const taskAtMessageStart =
|
|
685
|
+
const taskAtMessageStart = turns.activeTask;
|
|
784
686
|
stateStore.touchActivity(message.createdAt);
|
|
785
687
|
try {
|
|
786
688
|
await handleInboundMessage({
|
|
787
689
|
message,
|
|
788
690
|
channelMessage,
|
|
691
|
+
channelDriver,
|
|
789
692
|
options,
|
|
790
693
|
stateStore,
|
|
791
694
|
adapter,
|
|
@@ -794,30 +697,32 @@ async function main() {
|
|
|
794
697
|
outputBatcher,
|
|
795
698
|
clearActiveTask: () => {
|
|
796
699
|
if (taskAtMessageStart) {
|
|
797
|
-
|
|
700
|
+
turns.complete(taskAtMessageStart);
|
|
798
701
|
}
|
|
799
702
|
},
|
|
800
703
|
dispatchInboundText: async () => {
|
|
801
704
|
const nextActiveTask = createActiveTask(message);
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
705
|
+
const result = await turns.dispatch({
|
|
706
|
+
task: nextActiveTask,
|
|
707
|
+
conversation: channelMessage.conversation,
|
|
708
|
+
onBusy: async () => {
|
|
709
|
+
await queueInboundReply(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
|
|
710
|
+
},
|
|
711
|
+
forward: async () => {
|
|
712
|
+
await dispatchInboundWechatText({
|
|
713
|
+
message,
|
|
714
|
+
channelDriver,
|
|
715
|
+
options,
|
|
716
|
+
stateStore,
|
|
717
|
+
adapter,
|
|
718
|
+
activeTask: nextActiveTask,
|
|
719
|
+
});
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
if (result.status === "dispatched") {
|
|
723
|
+
void channelDriver.beginTyping?.(message.senderId);
|
|
820
724
|
}
|
|
725
|
+
return result.status === "dispatched" ? nextActiveTask : null;
|
|
821
726
|
},
|
|
822
727
|
deferInboundMessage: async (nextMessage) => {
|
|
823
728
|
deferredInboundMessages.push({
|
|
@@ -842,37 +747,38 @@ async function main() {
|
|
|
842
747
|
syncSharedSessionState(stateStore, adapter);
|
|
843
748
|
await maybeDrainDeferredInboundMessages();
|
|
844
749
|
},
|
|
845
|
-
|
|
750
|
+
onUnauthorizedSender: async (senderId, chatType) => {
|
|
846
751
|
stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
|
|
847
752
|
if (chatType === "direct") {
|
|
848
|
-
await
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
753
|
+
const result = await channelDriver.sendText({
|
|
754
|
+
target: channelDriver.directConversation(senderId),
|
|
755
|
+
text: "Unauthorized.",
|
|
756
|
+
context: "notice",
|
|
757
|
+
log: (entry) => stateStore.appendLog(entry),
|
|
758
|
+
});
|
|
759
|
+
if (result.status !== "sent") {
|
|
760
|
+
logError(`Failed to reply unauthorized notice to ${senderId}`);
|
|
761
|
+
}
|
|
855
762
|
}
|
|
856
763
|
},
|
|
857
|
-
|
|
764
|
+
onChannelFatal: async (error) => {
|
|
858
765
|
stateStore.appendLog(`wecom_fatal_error: ${error.message}`);
|
|
859
766
|
requestShutdown(error.message, 1);
|
|
860
767
|
},
|
|
861
|
-
|
|
768
|
+
onChannelConnected: async () => {
|
|
862
769
|
if (pendingWechatMessages.list().length > 0) {
|
|
863
770
|
await flushPendingWechatMessages();
|
|
864
771
|
}
|
|
865
772
|
},
|
|
866
773
|
});
|
|
867
|
-
|
|
868
|
-
await wecomTransport.waitUntilConnected();
|
|
774
|
+
await channelDriver.waitUntilReady?.();
|
|
869
775
|
}
|
|
870
|
-
log(`${
|
|
776
|
+
log(`${channelDriver.displayName} bridge is ready for adapter "${options.adapter}".`);
|
|
871
777
|
log(`Working directory: ${options.cwd}`);
|
|
872
778
|
if (options.profile) {
|
|
873
779
|
log(`Profile: ${options.profile}`);
|
|
874
780
|
}
|
|
875
|
-
log(`Authorized ${
|
|
781
|
+
log(`Authorized ${channelDriver.displayName} user: ${credentials.userId}`);
|
|
876
782
|
if (options.adapter === "codex") {
|
|
877
783
|
log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
|
|
878
784
|
}
|
|
@@ -890,7 +796,7 @@ async function main() {
|
|
|
890
796
|
cwd: options.cwd,
|
|
891
797
|
});
|
|
892
798
|
await queueWechatMessage(credentials.userId, welcomeText);
|
|
893
|
-
if (
|
|
799
|
+
if (channelDriver.capabilities.pushInbound) {
|
|
894
800
|
while (!shutdownPromise && ensureRuntimeOwnership()) {
|
|
895
801
|
await delay(1_000);
|
|
896
802
|
}
|
|
@@ -956,11 +862,12 @@ async function main() {
|
|
|
956
862
|
if (!ensureRuntimeOwnership()) {
|
|
957
863
|
break;
|
|
958
864
|
}
|
|
959
|
-
const taskAtMessageStart =
|
|
865
|
+
const taskAtMessageStart = turns.activeTask;
|
|
960
866
|
stateStore.touchActivity(message.createdAt);
|
|
961
867
|
try {
|
|
962
868
|
await handleInboundMessage({
|
|
963
869
|
message,
|
|
870
|
+
channelDriver,
|
|
964
871
|
options,
|
|
965
872
|
stateStore,
|
|
966
873
|
adapter,
|
|
@@ -969,30 +876,31 @@ async function main() {
|
|
|
969
876
|
outputBatcher,
|
|
970
877
|
clearActiveTask: () => {
|
|
971
878
|
if (taskAtMessageStart) {
|
|
972
|
-
|
|
879
|
+
turns.complete(taskAtMessageStart);
|
|
973
880
|
}
|
|
974
881
|
},
|
|
975
882
|
dispatchInboundText: async () => {
|
|
976
883
|
const nextActiveTask = createActiveTask(message);
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
884
|
+
const result = await turns.dispatch({
|
|
885
|
+
task: nextActiveTask,
|
|
886
|
+
onBusy: async () => {
|
|
887
|
+
await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
|
|
888
|
+
},
|
|
889
|
+
forward: async () => {
|
|
890
|
+
await dispatchInboundWechatText({
|
|
891
|
+
message,
|
|
892
|
+
channelDriver,
|
|
893
|
+
options,
|
|
894
|
+
stateStore,
|
|
895
|
+
adapter,
|
|
896
|
+
activeTask: nextActiveTask,
|
|
897
|
+
});
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
if (result.status === "dispatched") {
|
|
901
|
+
void channelDriver.beginTyping?.(message.senderId);
|
|
995
902
|
}
|
|
903
|
+
return result.status === "dispatched" ? nextActiveTask : null;
|
|
996
904
|
},
|
|
997
905
|
deferInboundMessage: async (nextMessage) => {
|
|
998
906
|
deferredInboundMessages.push({
|
|
@@ -1047,7 +955,7 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
1047
955
|
}
|
|
1048
956
|
}
|
|
1049
957
|
function wireAdapterEvents(params) {
|
|
1050
|
-
const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
|
|
958
|
+
const { adapter, channelDriver, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
|
|
1051
959
|
const channelPort = options.channelId === "wecom"
|
|
1052
960
|
? new WecomChannelPort({
|
|
1053
961
|
transport: wecomTransport,
|
|
@@ -1083,13 +991,9 @@ function wireAdapterEvents(params) {
|
|
|
1083
991
|
stateStore.clearPendingUserInput();
|
|
1084
992
|
}
|
|
1085
993
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
1086
|
-
const eventTarget =
|
|
994
|
+
const eventTarget = channelDriver.capabilities.multiConversation
|
|
1087
995
|
? getWecomTarget()
|
|
1088
|
-
:
|
|
1089
|
-
channelId: "wechat",
|
|
1090
|
-
conversationId: authorizedUserId,
|
|
1091
|
-
recipientId: authorizedUserId,
|
|
1092
|
-
};
|
|
996
|
+
: channelDriver.directConversation(authorizedUserId);
|
|
1093
997
|
const eventTask = getActiveTask();
|
|
1094
998
|
eventForwardChain = eventForwardChain
|
|
1095
999
|
.then(() => forwardBridgeEvent(event, {
|
|
@@ -1104,6 +1008,7 @@ function wireAdapterEvents(params) {
|
|
|
1104
1008
|
}
|
|
1105
1009
|
},
|
|
1106
1010
|
finalReply: (next) => {
|
|
1011
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1107
1012
|
lastFinalReplyAtMs = Date.now();
|
|
1108
1013
|
stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
|
|
1109
1014
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
@@ -1136,7 +1041,7 @@ function wireAdapterEvents(params) {
|
|
|
1136
1041
|
if (thinkingPreview) {
|
|
1137
1042
|
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
1138
1043
|
trackWechatForwardTask((async () => {
|
|
1139
|
-
await queueWechatMessage(authorizedUserId,
|
|
1044
|
+
await queueWechatMessage(authorizedUserId, channelDriver.id === "wecom"
|
|
1140
1045
|
? `Processing: ${thinkingPreview}`
|
|
1141
1046
|
: `思考: ${thinkingPreview}`, "thinking", eventTarget);
|
|
1142
1047
|
})());
|
|
@@ -1197,6 +1102,7 @@ function wireAdapterEvents(params) {
|
|
|
1197
1102
|
void maybeDrainDeferredInboundMessages();
|
|
1198
1103
|
},
|
|
1199
1104
|
taskComplete: () => {
|
|
1105
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1200
1106
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1201
1107
|
stateStore.clearPendingConfirmation();
|
|
1202
1108
|
stateStore.clearPendingUserInput();
|
|
@@ -1205,6 +1111,7 @@ function wireAdapterEvents(params) {
|
|
|
1205
1111
|
}));
|
|
1206
1112
|
},
|
|
1207
1113
|
taskFailed: (next) => {
|
|
1114
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1208
1115
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1209
1116
|
stateStore.clearPendingConfirmation();
|
|
1210
1117
|
stateStore.clearPendingUserInput();
|
|
@@ -1214,6 +1121,7 @@ function wireAdapterEvents(params) {
|
|
|
1214
1121
|
}));
|
|
1215
1122
|
},
|
|
1216
1123
|
fatalError: (next) => {
|
|
1124
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1217
1125
|
logError(next.message);
|
|
1218
1126
|
stateStore.appendLog(`fatal_error: ${next.message}`);
|
|
1219
1127
|
stateStore.clearPendingConfirmation();
|
|
@@ -1248,13 +1156,11 @@ function formatInboundMessagePreview(message) {
|
|
|
1248
1156
|
return "(empty)";
|
|
1249
1157
|
}
|
|
1250
1158
|
async function handleInboundMessage(params) {
|
|
1251
|
-
const { message, channelMessage, } = params;
|
|
1159
|
+
const { message, channelMessage, channelDriver, } = params;
|
|
1252
1160
|
const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, dispatchInboundText, deferInboundMessage, } = params;
|
|
1253
1161
|
const state = stateStore.getState();
|
|
1254
1162
|
if (message.senderId !== state.authorizedUserId) {
|
|
1255
|
-
await queueWechatMessage(message.senderId,
|
|
1256
|
-
? "Unauthorized. This bridge only accepts messages from the paired WeCom operator."
|
|
1257
|
-
: "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
1163
|
+
await queueWechatMessage(message.senderId, `Unauthorized. This bridge only accepts messages from ${channelDriver.operatorDescription}.`);
|
|
1258
1164
|
return null;
|
|
1259
1165
|
}
|
|
1260
1166
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
@@ -1267,12 +1173,15 @@ async function handleInboundMessage(params) {
|
|
|
1267
1173
|
case "plan":
|
|
1268
1174
|
await queueWechatMessage(message.senderId, await handleAdapterControl(adapter, message.senderId, systemCommand));
|
|
1269
1175
|
return null;
|
|
1176
|
+
case "broadcast":
|
|
1177
|
+
await queueWechatMessage(message.senderId, "/all is only available in daemon mode. Start the daemon with wechat-daemon or wecom-daemon to broadcast to all workers.");
|
|
1178
|
+
return null;
|
|
1270
1179
|
case "status":
|
|
1271
1180
|
await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
|
|
1272
1181
|
return null;
|
|
1273
1182
|
case "resume": {
|
|
1274
1183
|
if (!isWechatResumeEnabled(options.adapter)) {
|
|
1275
|
-
await queueWechatMessage(message.senderId, `${
|
|
1184
|
+
await queueWechatMessage(message.senderId, `${channelDriver.displayName} /resume is disabled in ${options.adapter} mode. Use /resume directly inside "${channelDriver.id}-${options.adapter}"; the remote channel will follow the active local session.`);
|
|
1276
1185
|
return null;
|
|
1277
1186
|
}
|
|
1278
1187
|
try {
|
|
@@ -1432,13 +1341,11 @@ function createActiveTask(message) {
|
|
|
1432
1341
|
};
|
|
1433
1342
|
}
|
|
1434
1343
|
async function dispatchInboundWechatText(params) {
|
|
1435
|
-
const { message, options, stateStore, adapter } = params;
|
|
1344
|
+
const { message, options, channelDriver, stateStore, adapter } = params;
|
|
1436
1345
|
const preview = formatInboundMessagePreview(message);
|
|
1437
1346
|
const activeTask = params.activeTask ?? createActiveTask(message);
|
|
1438
1347
|
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
|
|
1439
|
-
await adapter.sendInput(
|
|
1440
|
-
? buildWecomInboundPrompt(message.text, message.attachments)
|
|
1441
|
-
: buildWechatInboundPrompt(message.text, message.attachments));
|
|
1348
|
+
await adapter.sendInput(channelDriver.buildInboundPrompt(message.text, message.attachments));
|
|
1442
1349
|
return activeTask;
|
|
1443
1350
|
}
|
|
1444
1351
|
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|