@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.1
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 +468 -475
- 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 +10 -0
- package/bin/wecom-claude.mjs +10 -0
- package/bin/wecom-codex.mjs +10 -0
- package/bin/wecom-daemon.mjs +10 -0
- package/bin/wecom-opencode.mjs +10 -0
- package/bin/wecom-pi.mjs +10 -0
- package/bin/wecom-setup.mjs +10 -0
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +161 -4
- package/dist/bridge/bridge-adapters.codex.js +206 -14
- package/dist/bridge/bridge-adapters.core.js +25 -1
- package/dist/bridge/bridge-adapters.opencode.js +298 -20
- 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 +41 -2
- package/dist/bridge/bridge-state.js +16 -3
- package/dist/bridge/bridge-utils.js +6 -31
- package/dist/bridge/claude-hook.js +6 -0
- package/dist/bridge/claude-native-controls.js +145 -0
- package/dist/bridge/codex-visible-thread-proxy.js +140 -0
- package/dist/bridge/native-terminal-control.js +271 -0
- package/dist/bridge/opencode-v2-compat.js +450 -0
- package/dist/bridge/wechat-bridge.js +380 -171
- 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 +49 -3
- package/dist/channels/wecom/bridge-final-reply.js +32 -0
- package/dist/channels/wecom/setup-cli.js +12 -0
- package/dist/channels/wecom/setup.js +182 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +72 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +147 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +314 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +30 -3
- 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 +62 -11
- package/dist/companion/local-companion.js +25 -1
- package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
- package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -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/channel-types.js +3 -1
- package/dist/core/conversation-routing.js +58 -0
- package/dist/core/direct-run.js +23 -0
- package/dist/core/inbound-message-claims.js +67 -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 +9 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +675 -122
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +9 -1
- package/dist/i18n/messages-zh.js +9 -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 +36 -10
- package/dist/utils/version-checker.js +17 -11
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +119 -13
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +169 -58
- package/package.json +16 -4
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -1,22 +1,38 @@
|
|
|
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";
|
|
26
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
16
27
|
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
28
|
+
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
29
|
+
import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
|
|
30
|
+
import { WecomTransport } from "../channels/wecom/wecom-transport.js";
|
|
31
|
+
import { WecomChannelDriver } from "../channels/wecom/wecom-driver.js";
|
|
32
|
+
import { WechatChannelDriver } from "../channels/wechat/wechat-driver.js";
|
|
17
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";
|
|
18
|
-
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError,
|
|
19
|
-
import { getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
34
|
+
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, } from "../wechat/wechat-transport.js";
|
|
35
|
+
import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
20
36
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
21
37
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
22
38
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
@@ -73,31 +89,7 @@ function toPendingUserInput(request) {
|
|
|
73
89
|
createdAt: nowIso(),
|
|
74
90
|
};
|
|
75
91
|
}
|
|
76
|
-
export
|
|
77
|
-
return (params.adapter === "codex" &&
|
|
78
|
-
!params.hasPendingConfirmation &&
|
|
79
|
-
!params.hasSystemCommand &&
|
|
80
|
-
params.activeTurnOrigin === "local" &&
|
|
81
|
-
(params.status === "busy" || params.status === "awaiting_approval"));
|
|
82
|
-
}
|
|
83
|
-
export function canDrainDeferredCodexInboundQueue(params) {
|
|
84
|
-
return (params.adapter === "codex" &&
|
|
85
|
-
params.deferredCount > 0 &&
|
|
86
|
-
!params.hasPendingConfirmation &&
|
|
87
|
-
!params.hasPendingUserInput &&
|
|
88
|
-
!params.hasPendingApproval &&
|
|
89
|
-
!params.hasActiveTask &&
|
|
90
|
-
!params.activeTurnId &&
|
|
91
|
-
params.status !== "busy" &&
|
|
92
|
-
params.status !== "awaiting_approval" &&
|
|
93
|
-
params.status !== "awaiting_input");
|
|
94
|
-
}
|
|
95
|
-
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
96
|
-
return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
|
|
97
|
-
}
|
|
98
|
-
export function isRetryableDeferredCodexDrainError(errorText) {
|
|
99
|
-
return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
|
|
100
|
-
}
|
|
92
|
+
export { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessage, isRetryableDeferredCodexDrainError, shouldDeferCodexInboundMessage, } from "../core/bridge-defer.js";
|
|
101
93
|
export function parseCliArgs(argv) {
|
|
102
94
|
let adapter = null;
|
|
103
95
|
let commandOverride;
|
|
@@ -105,6 +97,7 @@ export function parseCliArgs(argv) {
|
|
|
105
97
|
let profile;
|
|
106
98
|
let lifecycle = "persistent";
|
|
107
99
|
let sessionStartMode = "restore";
|
|
100
|
+
let channelId = "wechat";
|
|
108
101
|
for (let i = 0; i < argv.length; i += 1) {
|
|
109
102
|
const arg = argv[i];
|
|
110
103
|
const next = argv[i + 1];
|
|
@@ -151,6 +144,13 @@ export function parseCliArgs(argv) {
|
|
|
151
144
|
sessionStartMode = next;
|
|
152
145
|
i += 1;
|
|
153
146
|
break;
|
|
147
|
+
case "--channel":
|
|
148
|
+
if (!next || (next !== "wechat" && next !== "wecom")) {
|
|
149
|
+
throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
|
|
150
|
+
}
|
|
151
|
+
channelId = next;
|
|
152
|
+
i += 1;
|
|
153
|
+
break;
|
|
154
154
|
case "--shutdown-on-parent-exit":
|
|
155
155
|
lifecycle = "companion_bound";
|
|
156
156
|
break;
|
|
@@ -173,6 +173,33 @@ export function parseCliArgs(argv) {
|
|
|
173
173
|
profile,
|
|
174
174
|
lifecycle,
|
|
175
175
|
sessionStartMode,
|
|
176
|
+
channelId,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function toWechatSendContext(kind) {
|
|
180
|
+
if (kind === "mirrored_input") {
|
|
181
|
+
return "mirrored_user_input";
|
|
182
|
+
}
|
|
183
|
+
if (kind === "status") {
|
|
184
|
+
return "message";
|
|
185
|
+
}
|
|
186
|
+
return kind;
|
|
187
|
+
}
|
|
188
|
+
function toLegacyInboundWechatMessage(message) {
|
|
189
|
+
return {
|
|
190
|
+
senderId: message.senderId,
|
|
191
|
+
sender: message.senderId,
|
|
192
|
+
sessionId: message.conversation.conversationId,
|
|
193
|
+
text: message.text,
|
|
194
|
+
attachments: message.attachments.map((attachment) => ({
|
|
195
|
+
kind: attachment.kind === "image" ? "image" : "file",
|
|
196
|
+
path: attachment.path,
|
|
197
|
+
fileName: attachment.fileName || path.basename(attachment.path),
|
|
198
|
+
sizeBytes: attachment.sizeBytes ?? 0,
|
|
199
|
+
})),
|
|
200
|
+
contextToken: message.conversation.opaqueRef,
|
|
201
|
+
createdAt: message.createdAt,
|
|
202
|
+
createdAtMs: Date.parse(message.createdAt),
|
|
176
203
|
};
|
|
177
204
|
}
|
|
178
205
|
function printUsageAndExit() {
|
|
@@ -180,7 +207,7 @@ function printUsageAndExit() {
|
|
|
180
207
|
"Internal bridge runtime usage:",
|
|
181
208
|
" 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
209
|
"",
|
|
183
|
-
"This entry is internal. Users should run
|
|
210
|
+
"This entry is internal. Users should run a wechat-* or wecom-* launcher.",
|
|
184
211
|
"",
|
|
185
212
|
].join("\n"));
|
|
186
213
|
process.exit(0);
|
|
@@ -194,21 +221,49 @@ async function main() {
|
|
|
194
221
|
const options = parseCliArgs(process.argv.slice(2));
|
|
195
222
|
const daemonEndpoint = readDaemonEndpoint();
|
|
196
223
|
if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
|
|
197
|
-
throw new Error(
|
|
224
|
+
throw new Error(`${daemonEndpoint.channelId ?? "wechat"}-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
|
|
198
225
|
}
|
|
199
226
|
if (daemonEndpoint) {
|
|
200
227
|
clearDaemonEndpoint(daemonEndpoint.pid);
|
|
201
228
|
log(`Cleared stale wechat-daemon endpoint for pid=${daemonEndpoint.pid}.`);
|
|
202
229
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
230
|
+
const wecomAccount = options.channelId === "wecom"
|
|
231
|
+
? await ensureWecomAccount({ log })
|
|
232
|
+
: null;
|
|
233
|
+
const credentials = wecomAccount
|
|
234
|
+
? { userId: wecomAccount.operatorUserId }
|
|
235
|
+
: await ensureWechatCredentials({
|
|
236
|
+
requireUserId: true,
|
|
237
|
+
validateExisting: true,
|
|
238
|
+
log,
|
|
239
|
+
});
|
|
208
240
|
if (!credentials.userId) {
|
|
209
|
-
throw new Error(
|
|
241
|
+
throw new Error(options.channelId === "wecom"
|
|
242
|
+
? "Saved WeCom credentials are missing operatorUserId."
|
|
243
|
+
: "Saved WeChat credentials are missing userId.");
|
|
210
244
|
}
|
|
211
245
|
const transport = new WeChatTransport({ log, logError });
|
|
246
|
+
const wecomTransport = wecomAccount
|
|
247
|
+
? new WecomTransport({
|
|
248
|
+
account: wecomAccount,
|
|
249
|
+
logger: {
|
|
250
|
+
log: (message) => log(message),
|
|
251
|
+
error: (message) => logError(message),
|
|
252
|
+
},
|
|
253
|
+
})
|
|
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
|
+
});
|
|
212
267
|
// 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
|
|
213
268
|
// unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
|
|
214
269
|
// 否则会与强制退出 teardown 竞态。
|
|
@@ -227,6 +282,7 @@ async function main() {
|
|
|
227
282
|
const stateStore = new BridgeStateStore({
|
|
228
283
|
...options,
|
|
229
284
|
authorizedUserId: credentials.userId,
|
|
285
|
+
channelId: options.channelId,
|
|
230
286
|
});
|
|
231
287
|
const reapedPeerPids = await reapPeerBridgeProcesses({
|
|
232
288
|
logger: (message) => stateStore.appendLog(message),
|
|
@@ -273,7 +329,9 @@ async function main() {
|
|
|
273
329
|
initialResumeConversationId: stateStore.getState().resumeConversationId,
|
|
274
330
|
initialTranscriptPath: stateStore.getState().transcriptPath,
|
|
275
331
|
});
|
|
276
|
-
const pendingWechatMessages = new PendingWechatMessageStore(
|
|
332
|
+
const pendingWechatMessages = new PendingWechatMessageStore(options.channelId === "wechat"
|
|
333
|
+
? getPendingWechatMessagesFile(options.cwd)
|
|
334
|
+
: getPendingChannelMessagesFile(options.cwd, options.channelId));
|
|
277
335
|
const resumeCoordinator = new ResumeSessionCoordinator({
|
|
278
336
|
adapter: options.adapter,
|
|
279
337
|
runtime: adapter,
|
|
@@ -284,7 +342,13 @@ async function main() {
|
|
|
284
342
|
let textSendChain = Promise.resolve();
|
|
285
343
|
let attachmentSendChain = Promise.resolve();
|
|
286
344
|
const pendingWechatForwardTasks = new Set();
|
|
287
|
-
|
|
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
|
+
});
|
|
288
352
|
const deferredInboundMessages = [];
|
|
289
353
|
let drainingDeferredInboundMessages = false;
|
|
290
354
|
let consecutivePollFailures = 0;
|
|
@@ -299,67 +363,30 @@ async function main() {
|
|
|
299
363
|
attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
300
364
|
return run;
|
|
301
365
|
};
|
|
302
|
-
const sendWechatMessageNow = async (senderId, text, context = "message") => {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
transport.clearCachedContextToken(senderId);
|
|
318
|
-
}
|
|
319
|
-
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.";
|
|
320
|
-
logError(`Failed to send WeChat ${context}: ${hint}`);
|
|
321
|
-
stateStore.appendLog(isWechatContextTokenStaleError(err)
|
|
322
|
-
? formatWechatContextTokenStaleLogEntry({
|
|
323
|
-
context,
|
|
324
|
-
recipientId: senderId,
|
|
325
|
-
error: err,
|
|
326
|
-
})
|
|
327
|
-
: formatWechatSendFailureLogEntry({
|
|
328
|
-
context,
|
|
329
|
-
recipientId: senderId,
|
|
330
|
-
error: err,
|
|
331
|
-
}));
|
|
332
|
-
return { status: "context_unavailable", error: err };
|
|
333
|
-
}
|
|
334
|
-
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
335
|
-
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
336
|
-
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
337
|
-
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
338
|
-
context,
|
|
339
|
-
recipientId: senderId,
|
|
340
|
-
attempt,
|
|
341
|
-
delayMs,
|
|
342
|
-
error: err,
|
|
343
|
-
}));
|
|
344
|
-
await delay(delayMs);
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
|
|
348
|
-
stateStore.appendLog(formatWechatSendFailureLogEntry({
|
|
349
|
-
context,
|
|
350
|
-
recipientId: senderId,
|
|
351
|
-
error: err,
|
|
352
|
-
}));
|
|
353
|
-
return { status: "failed" };
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return { status: "failed" };
|
|
366
|
+
const sendWechatMessageNow = async (senderId, text, context = "message", targetOverride) => {
|
|
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
|
+
});
|
|
357
381
|
};
|
|
358
|
-
const queueWechatMessage = (senderId, text, context = "message") => {
|
|
382
|
+
const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
|
|
383
|
+
const queuedTarget = senderId === operatorId && channelDriver.capabilities.multiConversation
|
|
384
|
+
? targetOverride ?? turns.resolveTarget(operatorConversation)
|
|
385
|
+
: targetOverride;
|
|
359
386
|
return queueWechatTextAction(async () => {
|
|
360
|
-
const result = await sendWechatMessageNow(senderId, text, context);
|
|
361
|
-
if (result.status === "
|
|
362
|
-
const pending = pendingWechatMessages.enqueue(senderId, text, context);
|
|
387
|
+
const result = await sendWechatMessageNow(senderId, text, context, queuedTarget);
|
|
388
|
+
if (result.status === "target_stale") {
|
|
389
|
+
const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
|
|
363
390
|
if (pending) {
|
|
364
391
|
stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
|
|
365
392
|
}
|
|
@@ -370,13 +397,13 @@ async function main() {
|
|
|
370
397
|
const flushPendingWechatMessages = () => {
|
|
371
398
|
return queueWechatTextAction(async () => {
|
|
372
399
|
for (const pending of pendingWechatMessages.list()) {
|
|
373
|
-
const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
|
|
400
|
+
const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
|
|
374
401
|
if (result.status === "sent") {
|
|
375
402
|
pendingWechatMessages.remove(pending.id);
|
|
376
403
|
stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
377
404
|
continue;
|
|
378
405
|
}
|
|
379
|
-
if (result.status === "
|
|
406
|
+
if (result.status === "target_stale") {
|
|
380
407
|
break;
|
|
381
408
|
}
|
|
382
409
|
stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
@@ -401,7 +428,9 @@ async function main() {
|
|
|
401
428
|
}
|
|
402
429
|
};
|
|
403
430
|
const outputBatcher = new OutputBatcher(async (text) => {
|
|
404
|
-
await queueWechatMessage(stateStore.getState().authorizedUserId, text
|
|
431
|
+
await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message", channelDriver.capabilities.multiConversation
|
|
432
|
+
? turns.resolveTarget(operatorConversation)
|
|
433
|
+
: undefined);
|
|
405
434
|
});
|
|
406
435
|
const maybeDrainDeferredInboundMessages = async () => {
|
|
407
436
|
if (drainingDeferredInboundMessages || !ensureRuntimeOwnership()) {
|
|
@@ -416,7 +445,7 @@ async function main() {
|
|
|
416
445
|
hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
|
|
417
446
|
hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
|
|
418
447
|
hasPendingApproval: Boolean(adapterState.pendingApproval),
|
|
419
|
-
hasActiveTask:
|
|
448
|
+
hasActiveTask: turns.hasActiveTask,
|
|
420
449
|
})) {
|
|
421
450
|
return;
|
|
422
451
|
}
|
|
@@ -427,13 +456,26 @@ async function main() {
|
|
|
427
456
|
drainingDeferredInboundMessages = true;
|
|
428
457
|
try {
|
|
429
458
|
stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
|
|
430
|
-
const nextTask =
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
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;
|
|
478
|
+
}
|
|
437
479
|
}
|
|
438
480
|
catch (err) {
|
|
439
481
|
const errorText = err instanceof Error ? err.message : String(err);
|
|
@@ -448,7 +490,7 @@ async function main() {
|
|
|
448
490
|
adapter: options.adapter,
|
|
449
491
|
cwd: options.cwd,
|
|
450
492
|
errorText,
|
|
451
|
-
}), "inbound_error");
|
|
493
|
+
}), "inbound_error", nextDeferred.channelMessage?.conversation);
|
|
452
494
|
}
|
|
453
495
|
finally {
|
|
454
496
|
drainingDeferredInboundMessages = false;
|
|
@@ -515,6 +557,18 @@ async function main() {
|
|
|
515
557
|
catch {
|
|
516
558
|
// Best effort shutdown.
|
|
517
559
|
}
|
|
560
|
+
try {
|
|
561
|
+
wecomTransport?.stop();
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Best effort shutdown.
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
await channelDriver.endAllTyping?.();
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Best effort shutdown.
|
|
571
|
+
}
|
|
518
572
|
controller.clearLocalClientEndpoint();
|
|
519
573
|
stateStore.releaseLock();
|
|
520
574
|
};
|
|
@@ -583,6 +637,7 @@ async function main() {
|
|
|
583
637
|
try {
|
|
584
638
|
wireAdapterEvents({
|
|
585
639
|
adapter,
|
|
640
|
+
channelDriver,
|
|
586
641
|
resumeCoordinator,
|
|
587
642
|
options,
|
|
588
643
|
transport,
|
|
@@ -592,8 +647,11 @@ async function main() {
|
|
|
592
647
|
queueWechatMessage,
|
|
593
648
|
trackWechatForwardTask,
|
|
594
649
|
maybeDrainDeferredInboundMessages,
|
|
595
|
-
|
|
596
|
-
|
|
650
|
+
getActiveTask: () => turns.activeTask,
|
|
651
|
+
clearActiveTask: (expectedTask) => {
|
|
652
|
+
if (expectedTask) {
|
|
653
|
+
turns.complete(expectedTask);
|
|
654
|
+
}
|
|
597
655
|
},
|
|
598
656
|
syncSharedSessionState: () => {
|
|
599
657
|
syncSharedSessionState(stateStore, adapter);
|
|
@@ -602,6 +660,8 @@ async function main() {
|
|
|
602
660
|
controller.syncLocalClientEndpoint();
|
|
603
661
|
},
|
|
604
662
|
requestShutdown,
|
|
663
|
+
wecomTransport,
|
|
664
|
+
getWecomTarget: () => turns.resolveTarget(operatorConversation),
|
|
605
665
|
});
|
|
606
666
|
await adapter.start();
|
|
607
667
|
if (!ensureRuntimeOwnership()) {
|
|
@@ -609,13 +669,116 @@ async function main() {
|
|
|
609
669
|
}
|
|
610
670
|
syncSharedSessionState(stateStore, adapter);
|
|
611
671
|
controller.syncLocalClientEndpoint();
|
|
612
|
-
stateStore.appendLog(`Bridge started with adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
613
|
-
|
|
672
|
+
stateStore.appendLog(`Bridge started with channel=${options.channelId} adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
673
|
+
if (channelDriver.start) {
|
|
674
|
+
await channelDriver.start({
|
|
675
|
+
onInboundMessage: async (channelMessage) => {
|
|
676
|
+
if (shutdownPromise || !ensureRuntimeOwnership()) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
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);
|
|
687
|
+
try {
|
|
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
|
+
});
|
|
736
|
+
}
|
|
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");
|
|
746
|
+
}
|
|
747
|
+
syncSharedSessionState(stateStore, adapter);
|
|
748
|
+
await maybeDrainDeferredInboundMessages();
|
|
749
|
+
},
|
|
750
|
+
onUnauthorizedSender: async (senderId, chatType) => {
|
|
751
|
+
stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
|
|
752
|
+
if (chatType === "direct") {
|
|
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
|
+
}
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
onChannelFatal: async (error) => {
|
|
765
|
+
stateStore.appendLog(`wecom_fatal_error: ${error.message}`);
|
|
766
|
+
requestShutdown(error.message, 1);
|
|
767
|
+
},
|
|
768
|
+
onChannelConnected: async () => {
|
|
769
|
+
if (pendingWechatMessages.list().length > 0) {
|
|
770
|
+
await flushPendingWechatMessages();
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
await channelDriver.waitUntilReady?.();
|
|
775
|
+
}
|
|
776
|
+
log(`${channelDriver.displayName} bridge is ready for adapter "${options.adapter}".`);
|
|
614
777
|
log(`Working directory: ${options.cwd}`);
|
|
615
778
|
if (options.profile) {
|
|
616
779
|
log(`Profile: ${options.profile}`);
|
|
617
780
|
}
|
|
618
|
-
log(`Authorized
|
|
781
|
+
log(`Authorized ${channelDriver.displayName} user: ${credentials.userId}`);
|
|
619
782
|
if (options.adapter === "codex") {
|
|
620
783
|
log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
|
|
621
784
|
}
|
|
@@ -633,6 +796,12 @@ async function main() {
|
|
|
633
796
|
cwd: options.cwd,
|
|
634
797
|
});
|
|
635
798
|
await queueWechatMessage(credentials.userId, welcomeText);
|
|
799
|
+
if (channelDriver.capabilities.pushInbound) {
|
|
800
|
+
while (!shutdownPromise && ensureRuntimeOwnership()) {
|
|
801
|
+
await delay(1_000);
|
|
802
|
+
}
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
636
805
|
while (true) {
|
|
637
806
|
if (shutdownPromise) {
|
|
638
807
|
// Shutdown (e.g. SIGINT) was requested while we were awaiting the
|
|
@@ -693,11 +862,12 @@ async function main() {
|
|
|
693
862
|
if (!ensureRuntimeOwnership()) {
|
|
694
863
|
break;
|
|
695
864
|
}
|
|
865
|
+
const taskAtMessageStart = turns.activeTask;
|
|
696
866
|
stateStore.touchActivity(message.createdAt);
|
|
697
|
-
let nextTask = null;
|
|
698
867
|
try {
|
|
699
|
-
|
|
868
|
+
await handleInboundMessage({
|
|
700
869
|
message,
|
|
870
|
+
channelDriver,
|
|
701
871
|
options,
|
|
702
872
|
stateStore,
|
|
703
873
|
adapter,
|
|
@@ -705,7 +875,32 @@ async function main() {
|
|
|
705
875
|
queueWechatMessage,
|
|
706
876
|
outputBatcher,
|
|
707
877
|
clearActiveTask: () => {
|
|
708
|
-
|
|
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;
|
|
709
904
|
},
|
|
710
905
|
deferInboundMessage: async (nextMessage) => {
|
|
711
906
|
deferredInboundMessages.push({
|
|
@@ -726,9 +921,6 @@ async function main() {
|
|
|
726
921
|
errorText,
|
|
727
922
|
}), "inbound_error");
|
|
728
923
|
}
|
|
729
|
-
if (nextTask) {
|
|
730
|
-
activeTask = nextTask;
|
|
731
|
-
}
|
|
732
924
|
syncSharedSessionState(stateStore, adapter);
|
|
733
925
|
await maybeDrainDeferredInboundMessages();
|
|
734
926
|
}
|
|
@@ -763,20 +955,28 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
763
955
|
}
|
|
764
956
|
}
|
|
765
957
|
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
|
-
|
|
958
|
+
const { adapter, channelDriver, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
|
|
959
|
+
const channelPort = options.channelId === "wecom"
|
|
960
|
+
? new WecomChannelPort({
|
|
961
|
+
transport: wecomTransport,
|
|
962
|
+
sendText: (target, text, kind) => queueWechatMessage(stateStore.getState().authorizedUserId, text, toWechatSendContext(kind), target),
|
|
963
|
+
onEmptyVisibleReply: (adapter, rawText) => {
|
|
964
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
|
|
965
|
+
},
|
|
966
|
+
})
|
|
967
|
+
: new WechatChannelPort({
|
|
968
|
+
sendText: (recipientId, text, context) => queueWechatMessage(recipientId, text, context),
|
|
969
|
+
sendImage: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
|
|
970
|
+
sendFile: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
|
|
971
|
+
sendVoice: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
|
|
972
|
+
sendVideo: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVideo(filePath, { recipientId })),
|
|
973
|
+
onEmptyVisibleReply: (adapter, rawText) => {
|
|
974
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
|
|
975
|
+
},
|
|
976
|
+
onTextSent: (_adapter, text) => {
|
|
977
|
+
stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
|
|
978
|
+
},
|
|
979
|
+
});
|
|
780
980
|
let lastFinalReplyAtMs = 0;
|
|
781
981
|
let eventForwardChain = Promise.resolve();
|
|
782
982
|
adapter.setEventSink((event) => {
|
|
@@ -791,6 +991,10 @@ function wireAdapterEvents(params) {
|
|
|
791
991
|
stateStore.clearPendingUserInput();
|
|
792
992
|
}
|
|
793
993
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
994
|
+
const eventTarget = channelDriver.capabilities.multiConversation
|
|
995
|
+
? getWecomTarget()
|
|
996
|
+
: channelDriver.directConversation(authorizedUserId);
|
|
997
|
+
const eventTask = getActiveTask();
|
|
794
998
|
eventForwardChain = eventForwardChain
|
|
795
999
|
.then(() => forwardBridgeEvent(event, {
|
|
796
1000
|
stdout: (next) => {
|
|
@@ -804,15 +1008,12 @@ function wireAdapterEvents(params) {
|
|
|
804
1008
|
}
|
|
805
1009
|
},
|
|
806
1010
|
finalReply: (next) => {
|
|
1011
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
807
1012
|
lastFinalReplyAtMs = Date.now();
|
|
808
1013
|
stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
|
|
809
1014
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
810
1015
|
await channelPort.send({
|
|
811
|
-
target:
|
|
812
|
-
channelId: "wechat",
|
|
813
|
-
conversationId: authorizedUserId,
|
|
814
|
-
recipientId: authorizedUserId,
|
|
815
|
-
},
|
|
1016
|
+
target: eventTarget,
|
|
816
1017
|
kind: "final_reply",
|
|
817
1018
|
text: next.text,
|
|
818
1019
|
adapter: options.adapter,
|
|
@@ -830,7 +1031,7 @@ function wireAdapterEvents(params) {
|
|
|
830
1031
|
stateStore.appendLog(`${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
831
1032
|
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
832
1033
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
833
|
-
await queueWechatMessage(authorizedUserId, next.text, "notice");
|
|
1034
|
+
await queueWechatMessage(authorizedUserId, next.text, "notice", eventTarget);
|
|
834
1035
|
}));
|
|
835
1036
|
}
|
|
836
1037
|
},
|
|
@@ -840,7 +1041,9 @@ function wireAdapterEvents(params) {
|
|
|
840
1041
|
if (thinkingPreview) {
|
|
841
1042
|
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
842
1043
|
trackWechatForwardTask((async () => {
|
|
843
|
-
await queueWechatMessage(authorizedUserId,
|
|
1044
|
+
await queueWechatMessage(authorizedUserId, channelDriver.id === "wecom"
|
|
1045
|
+
? `Processing: ${thinkingPreview}`
|
|
1046
|
+
: `思考: ${thinkingPreview}`, "thinking", eventTarget);
|
|
844
1047
|
})());
|
|
845
1048
|
}
|
|
846
1049
|
}
|
|
@@ -850,7 +1053,7 @@ function wireAdapterEvents(params) {
|
|
|
850
1053
|
const pending = toPendingApproval(next.request);
|
|
851
1054
|
stateStore.setPendingConfirmation(pending);
|
|
852
1055
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
853
|
-
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
1056
|
+
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required", eventTarget);
|
|
854
1057
|
}));
|
|
855
1058
|
},
|
|
856
1059
|
userInputRequired: (next) => {
|
|
@@ -858,14 +1061,14 @@ function wireAdapterEvents(params) {
|
|
|
858
1061
|
const pending = toPendingUserInput(next.request);
|
|
859
1062
|
stateStore.setPendingUserInput(pending);
|
|
860
1063
|
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
861
|
-
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
|
|
1064
|
+
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required", eventTarget);
|
|
862
1065
|
}));
|
|
863
1066
|
},
|
|
864
1067
|
mirroredUserInput: (next) => {
|
|
865
1068
|
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(next.text)}`);
|
|
866
1069
|
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
867
1070
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
868
|
-
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input");
|
|
1071
|
+
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input", eventTarget);
|
|
869
1072
|
}));
|
|
870
1073
|
}
|
|
871
1074
|
},
|
|
@@ -875,7 +1078,7 @@ function wireAdapterEvents(params) {
|
|
|
875
1078
|
stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
876
1079
|
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
877
1080
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
878
|
-
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);
|
|
879
1082
|
}));
|
|
880
1083
|
}
|
|
881
1084
|
},
|
|
@@ -893,36 +1096,39 @@ function wireAdapterEvents(params) {
|
|
|
893
1096
|
}
|
|
894
1097
|
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
895
1098
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
896
|
-
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);
|
|
897
1100
|
}));
|
|
898
1101
|
}
|
|
899
1102
|
void maybeDrainDeferredInboundMessages();
|
|
900
1103
|
},
|
|
901
1104
|
taskComplete: () => {
|
|
1105
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
902
1106
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
903
1107
|
stateStore.clearPendingConfirmation();
|
|
904
1108
|
stateStore.clearPendingUserInput();
|
|
905
|
-
clearActiveTask();
|
|
1109
|
+
clearActiveTask(eventTask);
|
|
906
1110
|
await maybeDrainDeferredInboundMessages();
|
|
907
1111
|
}));
|
|
908
1112
|
},
|
|
909
1113
|
taskFailed: (next) => {
|
|
1114
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
910
1115
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
911
1116
|
stateStore.clearPendingConfirmation();
|
|
912
1117
|
stateStore.clearPendingUserInput();
|
|
913
|
-
clearActiveTask();
|
|
914
|
-
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);
|
|
915
1120
|
await maybeDrainDeferredInboundMessages();
|
|
916
1121
|
}));
|
|
917
1122
|
},
|
|
918
1123
|
fatalError: (next) => {
|
|
1124
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
919
1125
|
logError(next.message);
|
|
920
1126
|
stateStore.appendLog(`fatal_error: ${next.message}`);
|
|
921
1127
|
stateStore.clearPendingConfirmation();
|
|
922
1128
|
stateStore.clearPendingUserInput();
|
|
923
|
-
clearActiveTask();
|
|
1129
|
+
clearActiveTask(eventTask);
|
|
924
1130
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
925
|
-
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error");
|
|
1131
|
+
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error", eventTarget);
|
|
926
1132
|
await maybeDrainDeferredInboundMessages();
|
|
927
1133
|
}));
|
|
928
1134
|
},
|
|
@@ -950,11 +1156,11 @@ function formatInboundMessagePreview(message) {
|
|
|
950
1156
|
return "(empty)";
|
|
951
1157
|
}
|
|
952
1158
|
async function handleInboundMessage(params) {
|
|
953
|
-
const { message, } = params;
|
|
954
|
-
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;
|
|
955
1161
|
const state = stateStore.getState();
|
|
956
1162
|
if (message.senderId !== state.authorizedUserId) {
|
|
957
|
-
await queueWechatMessage(message.senderId,
|
|
1163
|
+
await queueWechatMessage(message.senderId, `Unauthorized. This bridge only accepts messages from ${channelDriver.operatorDescription}.`);
|
|
958
1164
|
return null;
|
|
959
1165
|
}
|
|
960
1166
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
@@ -963,12 +1169,19 @@ async function handleInboundMessage(params) {
|
|
|
963
1169
|
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
964
1170
|
});
|
|
965
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;
|
|
966
1179
|
case "status":
|
|
967
1180
|
await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
|
|
968
1181
|
return null;
|
|
969
1182
|
case "resume": {
|
|
970
1183
|
if (!isWechatResumeEnabled(options.adapter)) {
|
|
971
|
-
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.`);
|
|
972
1185
|
return null;
|
|
973
1186
|
}
|
|
974
1187
|
try {
|
|
@@ -1081,7 +1294,7 @@ async function handleInboundMessage(params) {
|
|
|
1081
1294
|
}
|
|
1082
1295
|
const adapterState = adapter.getState();
|
|
1083
1296
|
const routeResult = await routeBridgeMessage({
|
|
1084
|
-
message: toChannelInboundMessage(message),
|
|
1297
|
+
message: channelMessage ?? toChannelInboundMessage(message),
|
|
1085
1298
|
authorized: true,
|
|
1086
1299
|
command: null,
|
|
1087
1300
|
adapterState,
|
|
@@ -1117,29 +1330,25 @@ async function handleInboundMessage(params) {
|
|
|
1117
1330
|
defer: async () => {
|
|
1118
1331
|
await deferInboundMessage(message);
|
|
1119
1332
|
},
|
|
1120
|
-
dispatch:
|
|
1121
|
-
return await dispatchInboundWechatText({
|
|
1122
|
-
message,
|
|
1123
|
-
options,
|
|
1124
|
-
stateStore,
|
|
1125
|
-
adapter,
|
|
1126
|
-
});
|
|
1127
|
-
},
|
|
1333
|
+
dispatch: dispatchInboundText,
|
|
1128
1334
|
});
|
|
1129
1335
|
return routeResult.kind === "dispatched" ? routeResult.result : null;
|
|
1130
1336
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const preview = formatInboundMessagePreview(message);
|
|
1134
|
-
const activeTask = {
|
|
1337
|
+
function createActiveTask(message) {
|
|
1338
|
+
return {
|
|
1135
1339
|
startedAt: Date.now(),
|
|
1136
|
-
inputPreview: truncatePreview(
|
|
1340
|
+
inputPreview: truncatePreview(formatInboundMessagePreview(message), 180),
|
|
1137
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);
|
|
1138
1347
|
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
|
|
1139
|
-
await adapter.sendInput(
|
|
1348
|
+
await adapter.sendInput(channelDriver.buildInboundPrompt(message.text, message.attachments));
|
|
1140
1349
|
return activeTask;
|
|
1141
1350
|
}
|
|
1142
|
-
const isDirectRun =
|
|
1351
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
1143
1352
|
if (isDirectRun) {
|
|
1144
1353
|
main().catch((err) => {
|
|
1145
1354
|
logError(describeWechatTransportError(err));
|