cli-wechat-bridge 1.1.7 → 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/LICENSE.txt +661 -661
- package/README.md +461 -434
- package/bin/_run-entry.mjs +29 -6
- 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/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +35 -1
- package/dist/bridge/bridge-adapters.codex.js +118 -7
- package/dist/bridge/bridge-adapters.core.js +25 -1
- package/dist/bridge/bridge-adapters.opencode.js +156 -1
- package/dist/bridge/bridge-adapters.pi.js +45 -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/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +259 -0
- package/dist/bridge/wechat-bridge.js +250 -274
- 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 +28 -2
- package/dist/companion/codex-visible-client-link.js +107 -0
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion-start.js +13 -2
- package/dist/companion/local-companion.js +23 -0
- package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
- package/dist/companion/pi-tui-bridge-extension.js +62 -3
- package/dist/core/bridge-control.js +10 -0
- package/dist/core/bridge-defer.js +25 -0
- package/dist/core/channel-driver.js +1 -0
- package/dist/core/conversation-routing.js +58 -0
- 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 +529 -205
- 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/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/atomic-file.js +6 -0
- package/dist/utils/doctor.js +6 -0
- package/dist/utils/version-checker.js +17 -11
- 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 +169 -58
- package/package.json +108 -106
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -1,26 +1,37 @@
|
|
|
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";
|
|
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";
|
|
24
|
+
import { handleAdapterControl } from "./adapter-control.js";
|
|
15
25
|
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
16
26
|
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
17
27
|
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
18
28
|
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
19
29
|
import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
|
|
20
30
|
import { WecomTransport } from "../channels/wecom/wecom-transport.js";
|
|
21
|
-
import {
|
|
31
|
+
import { WecomChannelDriver } from "../channels/wecom/wecom-driver.js";
|
|
32
|
+
import { WechatChannelDriver } from "../channels/wechat/wechat-driver.js";
|
|
22
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";
|
|
23
|
-
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError,
|
|
34
|
+
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, } from "../wechat/wechat-transport.js";
|
|
24
35
|
import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
25
36
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
26
37
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
@@ -78,31 +89,7 @@ function toPendingUserInput(request) {
|
|
|
78
89
|
createdAt: nowIso(),
|
|
79
90
|
};
|
|
80
91
|
}
|
|
81
|
-
export
|
|
82
|
-
return (params.adapter === "codex" &&
|
|
83
|
-
!params.hasPendingConfirmation &&
|
|
84
|
-
!params.hasSystemCommand &&
|
|
85
|
-
params.activeTurnOrigin === "local" &&
|
|
86
|
-
(params.status === "busy" || params.status === "awaiting_approval"));
|
|
87
|
-
}
|
|
88
|
-
export function canDrainDeferredCodexInboundQueue(params) {
|
|
89
|
-
return (params.adapter === "codex" &&
|
|
90
|
-
params.deferredCount > 0 &&
|
|
91
|
-
!params.hasPendingConfirmation &&
|
|
92
|
-
!params.hasPendingUserInput &&
|
|
93
|
-
!params.hasPendingApproval &&
|
|
94
|
-
!params.hasActiveTask &&
|
|
95
|
-
!params.activeTurnId &&
|
|
96
|
-
params.status !== "busy" &&
|
|
97
|
-
params.status !== "awaiting_approval" &&
|
|
98
|
-
params.status !== "awaiting_input");
|
|
99
|
-
}
|
|
100
|
-
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
101
|
-
return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
|
|
102
|
-
}
|
|
103
|
-
export function isRetryableDeferredCodexDrainError(errorText) {
|
|
104
|
-
return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
|
|
105
|
-
}
|
|
92
|
+
export { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessage, isRetryableDeferredCodexDrainError, shouldDeferCodexInboundMessage, } from "../core/bridge-defer.js";
|
|
106
93
|
export function parseCliArgs(argv) {
|
|
107
94
|
let adapter = null;
|
|
108
95
|
let commandOverride;
|
|
@@ -189,24 +176,6 @@ export function parseCliArgs(argv) {
|
|
|
189
176
|
channelId,
|
|
190
177
|
};
|
|
191
178
|
}
|
|
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
179
|
function toWechatSendContext(kind) {
|
|
211
180
|
if (kind === "mirrored_input") {
|
|
212
181
|
return "mirrored_user_input";
|
|
@@ -283,6 +252,18 @@ async function main() {
|
|
|
283
252
|
},
|
|
284
253
|
})
|
|
285
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
|
+
});
|
|
286
267
|
// 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
|
|
287
268
|
// unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
|
|
288
269
|
// 否则会与强制退出 teardown 竞态。
|
|
@@ -361,16 +342,13 @@ async function main() {
|
|
|
361
342
|
let textSendChain = Promise.resolve();
|
|
362
343
|
let attachmentSendChain = Promise.resolve();
|
|
363
344
|
const pendingWechatForwardTasks = new Set();
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
recipientId: credentials.userId,
|
|
372
|
-
metadata: { chatType: "direct" },
|
|
373
|
-
};
|
|
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
|
+
});
|
|
374
352
|
const deferredInboundMessages = [];
|
|
375
353
|
let drainingDeferredInboundMessages = false;
|
|
376
354
|
let consecutivePollFailures = 0;
|
|
@@ -386,92 +364,28 @@ async function main() {
|
|
|
386
364
|
return run;
|
|
387
365
|
};
|
|
388
366
|
const sendWechatMessageNow = async (senderId, text, context = "message", targetOverride) => {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
}
|
|
416
|
-
const startedAtMs = Date.now();
|
|
417
|
-
stateStore.appendLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
418
|
-
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
419
|
-
try {
|
|
420
|
-
await transport.sendText(senderId, text);
|
|
421
|
-
stateStore.appendLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
422
|
-
return { status: "sent" };
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
426
|
-
stateStore.appendLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
427
|
-
}
|
|
428
|
-
if (isWechatContextUnavailableError(err)) {
|
|
429
|
-
if (isWechatContextTokenStaleError(err)) {
|
|
430
|
-
transport.clearCachedContextToken(senderId);
|
|
431
|
-
}
|
|
432
|
-
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.";
|
|
433
|
-
logError(`Failed to send WeChat ${context}: ${hint}`);
|
|
434
|
-
stateStore.appendLog(isWechatContextTokenStaleError(err)
|
|
435
|
-
? formatWechatContextTokenStaleLogEntry({
|
|
436
|
-
context,
|
|
437
|
-
recipientId: senderId,
|
|
438
|
-
error: err,
|
|
439
|
-
})
|
|
440
|
-
: formatWechatSendFailureLogEntry({
|
|
441
|
-
context,
|
|
442
|
-
recipientId: senderId,
|
|
443
|
-
error: err,
|
|
444
|
-
}));
|
|
445
|
-
return { status: "context_unavailable", error: err };
|
|
446
|
-
}
|
|
447
|
-
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
448
|
-
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
449
|
-
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
450
|
-
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
451
|
-
context,
|
|
452
|
-
recipientId: senderId,
|
|
453
|
-
attempt,
|
|
454
|
-
delayMs,
|
|
455
|
-
error: err,
|
|
456
|
-
}));
|
|
457
|
-
await delay(delayMs);
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
|
|
461
|
-
stateStore.appendLog(formatWechatSendFailureLogEntry({
|
|
462
|
-
context,
|
|
463
|
-
recipientId: senderId,
|
|
464
|
-
error: err,
|
|
465
|
-
}));
|
|
466
|
-
return { status: "failed" };
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
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
|
+
});
|
|
470
381
|
};
|
|
471
382
|
const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
|
|
383
|
+
const queuedTarget = senderId === operatorId && channelDriver.capabilities.multiConversation
|
|
384
|
+
? targetOverride ?? turns.resolveTarget(operatorConversation)
|
|
385
|
+
: targetOverride;
|
|
472
386
|
return queueWechatTextAction(async () => {
|
|
473
|
-
const result = await sendWechatMessageNow(senderId, text, context,
|
|
474
|
-
if (result.status === "
|
|
387
|
+
const result = await sendWechatMessageNow(senderId, text, context, queuedTarget);
|
|
388
|
+
if (result.status === "target_stale") {
|
|
475
389
|
const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
|
|
476
390
|
if (pending) {
|
|
477
391
|
stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
|
|
@@ -489,7 +403,7 @@ async function main() {
|
|
|
489
403
|
stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
490
404
|
continue;
|
|
491
405
|
}
|
|
492
|
-
if (result.status === "
|
|
406
|
+
if (result.status === "target_stale") {
|
|
493
407
|
break;
|
|
494
408
|
}
|
|
495
409
|
stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
@@ -514,7 +428,9 @@ async function main() {
|
|
|
514
428
|
}
|
|
515
429
|
};
|
|
516
430
|
const outputBatcher = new OutputBatcher(async (text) => {
|
|
517
|
-
await queueWechatMessage(stateStore.getState().authorizedUserId, text
|
|
431
|
+
await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message", channelDriver.capabilities.multiConversation
|
|
432
|
+
? turns.resolveTarget(operatorConversation)
|
|
433
|
+
: undefined);
|
|
518
434
|
});
|
|
519
435
|
const maybeDrainDeferredInboundMessages = async () => {
|
|
520
436
|
if (drainingDeferredInboundMessages || !ensureRuntimeOwnership()) {
|
|
@@ -529,7 +445,7 @@ async function main() {
|
|
|
529
445
|
hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
|
|
530
446
|
hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
|
|
531
447
|
hasPendingApproval: Boolean(adapterState.pendingApproval),
|
|
532
|
-
hasActiveTask:
|
|
448
|
+
hasActiveTask: turns.hasActiveTask,
|
|
533
449
|
})) {
|
|
534
450
|
return;
|
|
535
451
|
}
|
|
@@ -540,17 +456,26 @@ async function main() {
|
|
|
540
456
|
drainingDeferredInboundMessages = true;
|
|
541
457
|
try {
|
|
542
458
|
stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
459
|
+
const nextTask = createActiveTask(nextDeferred.message);
|
|
460
|
+
const lease = turns.beginTurn(nextTask, nextDeferred.channelMessage?.conversation);
|
|
461
|
+
if (!lease) {
|
|
462
|
+
deferredInboundMessages.unshift(nextDeferred);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
await dispatchInboundWechatText({
|
|
467
|
+
message: nextDeferred.message,
|
|
468
|
+
channelDriver,
|
|
469
|
+
options,
|
|
470
|
+
stateStore,
|
|
471
|
+
adapter,
|
|
472
|
+
activeTask: nextTask,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
turns.rollback(lease);
|
|
477
|
+
throw error;
|
|
546
478
|
}
|
|
547
|
-
const nextTask = await dispatchInboundWechatText({
|
|
548
|
-
message: nextDeferred.message,
|
|
549
|
-
options,
|
|
550
|
-
stateStore,
|
|
551
|
-
adapter,
|
|
552
|
-
});
|
|
553
|
-
activeTask = nextTask;
|
|
554
479
|
}
|
|
555
480
|
catch (err) {
|
|
556
481
|
const errorText = err instanceof Error ? err.message : String(err);
|
|
@@ -565,7 +490,7 @@ async function main() {
|
|
|
565
490
|
adapter: options.adapter,
|
|
566
491
|
cwd: options.cwd,
|
|
567
492
|
errorText,
|
|
568
|
-
}), "inbound_error");
|
|
493
|
+
}), "inbound_error", nextDeferred.channelMessage?.conversation);
|
|
569
494
|
}
|
|
570
495
|
finally {
|
|
571
496
|
drainingDeferredInboundMessages = false;
|
|
@@ -638,6 +563,12 @@ async function main() {
|
|
|
638
563
|
catch {
|
|
639
564
|
// Best effort shutdown.
|
|
640
565
|
}
|
|
566
|
+
try {
|
|
567
|
+
await channelDriver.endAllTyping?.();
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Best effort shutdown.
|
|
571
|
+
}
|
|
641
572
|
controller.clearLocalClientEndpoint();
|
|
642
573
|
stateStore.releaseLock();
|
|
643
574
|
};
|
|
@@ -706,6 +637,7 @@ async function main() {
|
|
|
706
637
|
try {
|
|
707
638
|
wireAdapterEvents({
|
|
708
639
|
adapter,
|
|
640
|
+
channelDriver,
|
|
709
641
|
resumeCoordinator,
|
|
710
642
|
options,
|
|
711
643
|
transport,
|
|
@@ -715,9 +647,11 @@ async function main() {
|
|
|
715
647
|
queueWechatMessage,
|
|
716
648
|
trackWechatForwardTask,
|
|
717
649
|
maybeDrainDeferredInboundMessages,
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
650
|
+
getActiveTask: () => turns.activeTask,
|
|
651
|
+
clearActiveTask: (expectedTask) => {
|
|
652
|
+
if (expectedTask) {
|
|
653
|
+
turns.complete(expectedTask);
|
|
654
|
+
}
|
|
721
655
|
},
|
|
722
656
|
syncSharedSessionState: () => {
|
|
723
657
|
syncSharedSessionState(stateStore, adapter);
|
|
@@ -727,7 +661,7 @@ async function main() {
|
|
|
727
661
|
},
|
|
728
662
|
requestShutdown,
|
|
729
663
|
wecomTransport,
|
|
730
|
-
getWecomTarget: () =>
|
|
664
|
+
getWecomTarget: () => turns.resolveTarget(operatorConversation),
|
|
731
665
|
});
|
|
732
666
|
await adapter.start();
|
|
733
667
|
if (!ensureRuntimeOwnership()) {
|
|
@@ -736,97 +670,115 @@ async function main() {
|
|
|
736
670
|
syncSharedSessionState(stateStore, adapter);
|
|
737
671
|
controller.syncLocalClientEndpoint();
|
|
738
672
|
stateStore.appendLog(`Bridge started with channel=${options.channelId} adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
|
|
673
|
+
if (channelDriver.start) {
|
|
674
|
+
await channelDriver.start({
|
|
675
|
+
onInboundMessage: async (channelMessage) => {
|
|
742
676
|
if (shutdownPromise || !ensureRuntimeOwnership()) {
|
|
743
677
|
return;
|
|
744
678
|
}
|
|
745
|
-
|
|
746
|
-
|
|
679
|
+
turns.observeConversation(channelMessage.conversation);
|
|
680
|
+
const queueInboundReply = (senderId, text, context) => queueWechatMessage(senderId, text, context, channelMessage.conversation);
|
|
681
|
+
if (pendingWechatMessages.list().length > 0) {
|
|
682
|
+
await flushPendingWechatMessages();
|
|
683
|
+
}
|
|
684
|
+
const message = toLegacyInboundWechatMessage(channelMessage);
|
|
685
|
+
const taskAtMessageStart = turns.activeTask;
|
|
686
|
+
stateStore.touchActivity(message.createdAt);
|
|
747
687
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
688
|
+
await handleInboundMessage({
|
|
689
|
+
message,
|
|
690
|
+
channelMessage,
|
|
691
|
+
channelDriver,
|
|
692
|
+
options,
|
|
693
|
+
stateStore,
|
|
694
|
+
adapter,
|
|
695
|
+
resumeCoordinator,
|
|
696
|
+
queueWechatMessage: queueInboundReply,
|
|
697
|
+
outputBatcher,
|
|
698
|
+
clearActiveTask: () => {
|
|
699
|
+
if (taskAtMessageStart) {
|
|
700
|
+
turns.complete(taskAtMessageStart);
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
dispatchInboundText: async () => {
|
|
704
|
+
const nextActiveTask = createActiveTask(message);
|
|
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);
|
|
724
|
+
}
|
|
725
|
+
return result.status === "dispatched" ? nextActiveTask : null;
|
|
726
|
+
},
|
|
727
|
+
deferInboundMessage: async (nextMessage) => {
|
|
728
|
+
deferredInboundMessages.push({
|
|
729
|
+
message: nextMessage,
|
|
730
|
+
channelMessage,
|
|
731
|
+
});
|
|
732
|
+
stateStore.appendLog(`deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`);
|
|
733
|
+
await queueInboundReply(nextMessage.senderId, formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length));
|
|
734
|
+
},
|
|
735
|
+
});
|
|
794
736
|
}
|
|
795
|
-
|
|
796
|
-
|
|
737
|
+
catch (error) {
|
|
738
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
739
|
+
logError(errorText);
|
|
740
|
+
stateStore.appendLog(`inbound_error: ${errorText}`);
|
|
741
|
+
await queueInboundReply(message.senderId, formatUserFacingInboundError({
|
|
742
|
+
adapter: options.adapter,
|
|
743
|
+
cwd: options.cwd,
|
|
744
|
+
errorText,
|
|
745
|
+
}), "inbound_error");
|
|
797
746
|
}
|
|
747
|
+
syncSharedSessionState(stateStore, adapter);
|
|
748
|
+
await maybeDrainDeferredInboundMessages();
|
|
798
749
|
},
|
|
799
|
-
|
|
750
|
+
onUnauthorizedSender: async (senderId, chatType) => {
|
|
800
751
|
stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
|
|
801
752
|
if (chatType === "direct") {
|
|
802
|
-
await
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
+
}
|
|
809
762
|
}
|
|
810
763
|
},
|
|
811
|
-
|
|
764
|
+
onChannelFatal: async (error) => {
|
|
812
765
|
stateStore.appendLog(`wecom_fatal_error: ${error.message}`);
|
|
813
766
|
requestShutdown(error.message, 1);
|
|
814
767
|
},
|
|
815
|
-
|
|
768
|
+
onChannelConnected: async () => {
|
|
816
769
|
if (pendingWechatMessages.list().length > 0) {
|
|
817
770
|
await flushPendingWechatMessages();
|
|
818
771
|
}
|
|
819
772
|
},
|
|
820
773
|
});
|
|
821
|
-
|
|
822
|
-
await wecomTransport.waitUntilConnected();
|
|
774
|
+
await channelDriver.waitUntilReady?.();
|
|
823
775
|
}
|
|
824
|
-
log(`${
|
|
776
|
+
log(`${channelDriver.displayName} bridge is ready for adapter "${options.adapter}".`);
|
|
825
777
|
log(`Working directory: ${options.cwd}`);
|
|
826
778
|
if (options.profile) {
|
|
827
779
|
log(`Profile: ${options.profile}`);
|
|
828
780
|
}
|
|
829
|
-
log(`Authorized ${
|
|
781
|
+
log(`Authorized ${channelDriver.displayName} user: ${credentials.userId}`);
|
|
830
782
|
if (options.adapter === "codex") {
|
|
831
783
|
log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
|
|
832
784
|
}
|
|
@@ -844,7 +796,7 @@ async function main() {
|
|
|
844
796
|
cwd: options.cwd,
|
|
845
797
|
});
|
|
846
798
|
await queueWechatMessage(credentials.userId, welcomeText);
|
|
847
|
-
if (
|
|
799
|
+
if (channelDriver.capabilities.pushInbound) {
|
|
848
800
|
while (!shutdownPromise && ensureRuntimeOwnership()) {
|
|
849
801
|
await delay(1_000);
|
|
850
802
|
}
|
|
@@ -910,11 +862,12 @@ async function main() {
|
|
|
910
862
|
if (!ensureRuntimeOwnership()) {
|
|
911
863
|
break;
|
|
912
864
|
}
|
|
865
|
+
const taskAtMessageStart = turns.activeTask;
|
|
913
866
|
stateStore.touchActivity(message.createdAt);
|
|
914
|
-
let nextTask = null;
|
|
915
867
|
try {
|
|
916
|
-
|
|
868
|
+
await handleInboundMessage({
|
|
917
869
|
message,
|
|
870
|
+
channelDriver,
|
|
918
871
|
options,
|
|
919
872
|
stateStore,
|
|
920
873
|
adapter,
|
|
@@ -922,7 +875,32 @@ async function main() {
|
|
|
922
875
|
queueWechatMessage,
|
|
923
876
|
outputBatcher,
|
|
924
877
|
clearActiveTask: () => {
|
|
925
|
-
|
|
878
|
+
if (taskAtMessageStart) {
|
|
879
|
+
turns.complete(taskAtMessageStart);
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
dispatchInboundText: async () => {
|
|
883
|
+
const nextActiveTask = createActiveTask(message);
|
|
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);
|
|
902
|
+
}
|
|
903
|
+
return result.status === "dispatched" ? nextActiveTask : null;
|
|
926
904
|
},
|
|
927
905
|
deferInboundMessage: async (nextMessage) => {
|
|
928
906
|
deferredInboundMessages.push({
|
|
@@ -943,9 +921,6 @@ async function main() {
|
|
|
943
921
|
errorText,
|
|
944
922
|
}), "inbound_error");
|
|
945
923
|
}
|
|
946
|
-
if (nextTask) {
|
|
947
|
-
activeTask = nextTask;
|
|
948
|
-
}
|
|
949
924
|
syncSharedSessionState(stateStore, adapter);
|
|
950
925
|
await maybeDrainDeferredInboundMessages();
|
|
951
926
|
}
|
|
@@ -980,7 +955,7 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
980
955
|
}
|
|
981
956
|
}
|
|
982
957
|
function wireAdapterEvents(params) {
|
|
983
|
-
const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, 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;
|
|
984
959
|
const channelPort = options.channelId === "wecom"
|
|
985
960
|
? new WecomChannelPort({
|
|
986
961
|
transport: wecomTransport,
|
|
@@ -1016,6 +991,10 @@ function wireAdapterEvents(params) {
|
|
|
1016
991
|
stateStore.clearPendingUserInput();
|
|
1017
992
|
}
|
|
1018
993
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
994
|
+
const eventTarget = channelDriver.capabilities.multiConversation
|
|
995
|
+
? getWecomTarget()
|
|
996
|
+
: channelDriver.directConversation(authorizedUserId);
|
|
997
|
+
const eventTask = getActiveTask();
|
|
1019
998
|
eventForwardChain = eventForwardChain
|
|
1020
999
|
.then(() => forwardBridgeEvent(event, {
|
|
1021
1000
|
stdout: (next) => {
|
|
@@ -1029,17 +1008,12 @@ function wireAdapterEvents(params) {
|
|
|
1029
1008
|
}
|
|
1030
1009
|
},
|
|
1031
1010
|
finalReply: (next) => {
|
|
1011
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1032
1012
|
lastFinalReplyAtMs = Date.now();
|
|
1033
1013
|
stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
|
|
1034
1014
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1035
1015
|
await channelPort.send({
|
|
1036
|
-
target:
|
|
1037
|
-
? getWecomTarget()
|
|
1038
|
-
: {
|
|
1039
|
-
channelId: "wechat",
|
|
1040
|
-
conversationId: authorizedUserId,
|
|
1041
|
-
recipientId: authorizedUserId,
|
|
1042
|
-
},
|
|
1016
|
+
target: eventTarget,
|
|
1043
1017
|
kind: "final_reply",
|
|
1044
1018
|
text: next.text,
|
|
1045
1019
|
adapter: options.adapter,
|
|
@@ -1057,7 +1031,7 @@ function wireAdapterEvents(params) {
|
|
|
1057
1031
|
stateStore.appendLog(`${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
1058
1032
|
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
1059
1033
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1060
|
-
await queueWechatMessage(authorizedUserId, next.text, "notice");
|
|
1034
|
+
await queueWechatMessage(authorizedUserId, next.text, "notice", eventTarget);
|
|
1061
1035
|
}));
|
|
1062
1036
|
}
|
|
1063
1037
|
},
|
|
@@ -1067,9 +1041,9 @@ function wireAdapterEvents(params) {
|
|
|
1067
1041
|
if (thinkingPreview) {
|
|
1068
1042
|
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
1069
1043
|
trackWechatForwardTask((async () => {
|
|
1070
|
-
await queueWechatMessage(authorizedUserId,
|
|
1044
|
+
await queueWechatMessage(authorizedUserId, channelDriver.id === "wecom"
|
|
1071
1045
|
? `Processing: ${thinkingPreview}`
|
|
1072
|
-
: `思考: ${thinkingPreview}`, "thinking");
|
|
1046
|
+
: `思考: ${thinkingPreview}`, "thinking", eventTarget);
|
|
1073
1047
|
})());
|
|
1074
1048
|
}
|
|
1075
1049
|
}
|
|
@@ -1079,7 +1053,7 @@ function wireAdapterEvents(params) {
|
|
|
1079
1053
|
const pending = toPendingApproval(next.request);
|
|
1080
1054
|
stateStore.setPendingConfirmation(pending);
|
|
1081
1055
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
1082
|
-
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
1056
|
+
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required", eventTarget);
|
|
1083
1057
|
}));
|
|
1084
1058
|
},
|
|
1085
1059
|
userInputRequired: (next) => {
|
|
@@ -1087,14 +1061,14 @@ function wireAdapterEvents(params) {
|
|
|
1087
1061
|
const pending = toPendingUserInput(next.request);
|
|
1088
1062
|
stateStore.setPendingUserInput(pending);
|
|
1089
1063
|
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
1090
|
-
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
|
|
1064
|
+
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required", eventTarget);
|
|
1091
1065
|
}));
|
|
1092
1066
|
},
|
|
1093
1067
|
mirroredUserInput: (next) => {
|
|
1094
1068
|
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(next.text)}`);
|
|
1095
1069
|
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
1096
1070
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1097
|
-
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input");
|
|
1071
|
+
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input", eventTarget);
|
|
1098
1072
|
}));
|
|
1099
1073
|
}
|
|
1100
1074
|
},
|
|
@@ -1104,7 +1078,7 @@ function wireAdapterEvents(params) {
|
|
|
1104
1078
|
stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
1105
1079
|
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
1106
1080
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1107
|
-
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched");
|
|
1081
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched", eventTarget);
|
|
1108
1082
|
}));
|
|
1109
1083
|
}
|
|
1110
1084
|
},
|
|
@@ -1122,36 +1096,39 @@ function wireAdapterEvents(params) {
|
|
|
1122
1096
|
}
|
|
1123
1097
|
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
1124
1098
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1125
|
-
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched");
|
|
1099
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched", eventTarget);
|
|
1126
1100
|
}));
|
|
1127
1101
|
}
|
|
1128
1102
|
void maybeDrainDeferredInboundMessages();
|
|
1129
1103
|
},
|
|
1130
1104
|
taskComplete: () => {
|
|
1105
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1131
1106
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1132
1107
|
stateStore.clearPendingConfirmation();
|
|
1133
1108
|
stateStore.clearPendingUserInput();
|
|
1134
|
-
clearActiveTask();
|
|
1109
|
+
clearActiveTask(eventTask);
|
|
1135
1110
|
await maybeDrainDeferredInboundMessages();
|
|
1136
1111
|
}));
|
|
1137
1112
|
},
|
|
1138
1113
|
taskFailed: (next) => {
|
|
1114
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1139
1115
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1140
1116
|
stateStore.clearPendingConfirmation();
|
|
1141
1117
|
stateStore.clearPendingUserInput();
|
|
1142
|
-
clearActiveTask();
|
|
1143
|
-
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed");
|
|
1118
|
+
clearActiveTask(eventTask);
|
|
1119
|
+
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed", eventTarget);
|
|
1144
1120
|
await maybeDrainDeferredInboundMessages();
|
|
1145
1121
|
}));
|
|
1146
1122
|
},
|
|
1147
1123
|
fatalError: (next) => {
|
|
1124
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1148
1125
|
logError(next.message);
|
|
1149
1126
|
stateStore.appendLog(`fatal_error: ${next.message}`);
|
|
1150
1127
|
stateStore.clearPendingConfirmation();
|
|
1151
1128
|
stateStore.clearPendingUserInput();
|
|
1152
|
-
clearActiveTask();
|
|
1129
|
+
clearActiveTask(eventTask);
|
|
1153
1130
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1154
|
-
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error");
|
|
1131
|
+
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error", eventTarget);
|
|
1155
1132
|
await maybeDrainDeferredInboundMessages();
|
|
1156
1133
|
}));
|
|
1157
1134
|
},
|
|
@@ -1179,13 +1156,11 @@ function formatInboundMessagePreview(message) {
|
|
|
1179
1156
|
return "(empty)";
|
|
1180
1157
|
}
|
|
1181
1158
|
async function handleInboundMessage(params) {
|
|
1182
|
-
const { message, channelMessage, } = params;
|
|
1183
|
-
const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, deferInboundMessage, } = params;
|
|
1159
|
+
const { message, channelMessage, channelDriver, } = params;
|
|
1160
|
+
const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, dispatchInboundText, deferInboundMessage, } = params;
|
|
1184
1161
|
const state = stateStore.getState();
|
|
1185
1162
|
if (message.senderId !== state.authorizedUserId) {
|
|
1186
|
-
await queueWechatMessage(message.senderId,
|
|
1187
|
-
? "Unauthorized. This bridge only accepts messages from the paired WeCom operator."
|
|
1188
|
-
: "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}.`);
|
|
1189
1164
|
return null;
|
|
1190
1165
|
}
|
|
1191
1166
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
@@ -1194,12 +1169,19 @@ async function handleInboundMessage(params) {
|
|
|
1194
1169
|
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
1195
1170
|
});
|
|
1196
1171
|
switch (systemCommand?.type) {
|
|
1172
|
+
case "model":
|
|
1173
|
+
case "plan":
|
|
1174
|
+
await queueWechatMessage(message.senderId, await handleAdapterControl(adapter, message.senderId, systemCommand));
|
|
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;
|
|
1197
1179
|
case "status":
|
|
1198
1180
|
await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
|
|
1199
1181
|
return null;
|
|
1200
1182
|
case "resume": {
|
|
1201
1183
|
if (!isWechatResumeEnabled(options.adapter)) {
|
|
1202
|
-
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.`);
|
|
1203
1185
|
return null;
|
|
1204
1186
|
}
|
|
1205
1187
|
try {
|
|
@@ -1348,28 +1330,22 @@ async function handleInboundMessage(params) {
|
|
|
1348
1330
|
defer: async () => {
|
|
1349
1331
|
await deferInboundMessage(message);
|
|
1350
1332
|
},
|
|
1351
|
-
dispatch:
|
|
1352
|
-
return await dispatchInboundWechatText({
|
|
1353
|
-
message,
|
|
1354
|
-
options,
|
|
1355
|
-
stateStore,
|
|
1356
|
-
adapter,
|
|
1357
|
-
});
|
|
1358
|
-
},
|
|
1333
|
+
dispatch: dispatchInboundText,
|
|
1359
1334
|
});
|
|
1360
1335
|
return routeResult.kind === "dispatched" ? routeResult.result : null;
|
|
1361
1336
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
const preview = formatInboundMessagePreview(message);
|
|
1365
|
-
const activeTask = {
|
|
1337
|
+
function createActiveTask(message) {
|
|
1338
|
+
return {
|
|
1366
1339
|
startedAt: Date.now(),
|
|
1367
|
-
inputPreview: truncatePreview(
|
|
1340
|
+
inputPreview: truncatePreview(formatInboundMessagePreview(message), 180),
|
|
1368
1341
|
};
|
|
1342
|
+
}
|
|
1343
|
+
async function dispatchInboundWechatText(params) {
|
|
1344
|
+
const { message, options, channelDriver, stateStore, adapter } = params;
|
|
1345
|
+
const preview = formatInboundMessagePreview(message);
|
|
1346
|
+
const activeTask = params.activeTask ?? createActiveTask(message);
|
|
1369
1347
|
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
|
|
1370
|
-
await adapter.sendInput(
|
|
1371
|
-
? buildWecomInboundPrompt(message.text, message.attachments)
|
|
1372
|
-
: buildWechatInboundPrompt(message.text, message.attachments));
|
|
1348
|
+
await adapter.sendInput(channelDriver.buildInboundPrompt(message.text, message.attachments));
|
|
1373
1349
|
return activeTask;
|
|
1374
1350
|
}
|
|
1375
1351
|
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|