@unlinearity/cli-wechat-bridge 1.1.5 → 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 +434 -427
- package/bin/_run-entry.mjs +47 -9
- 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 +163 -5
- package/dist/bridge/bridge-adapters.codex.js +331 -29
- package/dist/bridge/bridge-adapters.core.js +27 -2
- package/dist/bridge/bridge-adapters.opencode.js +156 -1
- package/dist/bridge/bridge-adapters.pi.js +46 -1
- package/dist/bridge/bridge-adapters.shared.js +8 -1
- package/dist/bridge/bridge-controller.js +6 -0
- package/dist/bridge/bridge-final-reply.js +8 -65
- package/dist/bridge/bridge-process-reaper.js +41 -2
- package/dist/bridge/bridge-state.js +17 -4
- package/dist/bridge/bridge-utils.js +10 -127
- 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 +520 -274
- package/dist/bridge/wechat-forwarding.js +8 -58
- package/dist/bridge/wechat-outbound-queue.js +8 -0
- package/dist/channels/wechat/bridge-final-reply.js +56 -0
- package/dist/channels/wechat/channel-message.js +24 -0
- package/dist/channels/wechat/index.js +11 -0
- package/dist/channels/wechat/wechat-channel-port.js +67 -0
- package/dist/channels/wechat/wechat-driver.js +191 -0
- package/dist/channels/wechat/wechat-forwarding.js +97 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +118 -0
- 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/pi-tui-bridge-extension.js +62 -3
- package/dist/core/bridge-control.js +42 -0
- package/dist/core/bridge-defer.js +25 -0
- package/dist/core/bridge-event-forwarder.js +50 -0
- package/dist/core/bridge-message-router.js +32 -0
- package/dist/core/channel-driver.js +1 -0
- package/dist/core/channel-types.js +3 -0
- 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 +69 -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 +827 -209
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +11 -3
- package/dist/i18n/messages-zh.js +11 -3
- 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 +14 -4
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -1,21 +1,40 @@
|
|
|
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
|
-
import { forwardWechatFinalReply } from "./bridge-final-reply.js";
|
|
8
13
|
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "./bridge-session-resume.js";
|
|
9
|
-
import {
|
|
14
|
+
import { formatUserFacingBridgeFatalError, formatUserFacingInboundError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
|
|
10
15
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
11
16
|
import { BridgeStateStore } from "./bridge-state.js";
|
|
12
17
|
import { getProcessRecordByPid, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "./bridge-process-reaper.js";
|
|
13
18
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
19
|
+
import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
|
|
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";
|
|
25
|
+
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
26
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
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";
|
|
14
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";
|
|
15
|
-
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError,
|
|
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";
|
|
16
36
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
17
37
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
18
|
-
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "../daemon/emoji-bindings.js";
|
|
19
38
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
20
39
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
21
40
|
const PARENT_PROCESS_POLL_MS = 5_000;
|
|
@@ -70,31 +89,7 @@ function toPendingUserInput(request) {
|
|
|
70
89
|
createdAt: nowIso(),
|
|
71
90
|
};
|
|
72
91
|
}
|
|
73
|
-
export
|
|
74
|
-
return (params.adapter === "codex" &&
|
|
75
|
-
!params.hasPendingConfirmation &&
|
|
76
|
-
!params.hasSystemCommand &&
|
|
77
|
-
params.activeTurnOrigin === "local" &&
|
|
78
|
-
(params.status === "busy" || params.status === "awaiting_approval"));
|
|
79
|
-
}
|
|
80
|
-
export function canDrainDeferredCodexInboundQueue(params) {
|
|
81
|
-
return (params.adapter === "codex" &&
|
|
82
|
-
params.deferredCount > 0 &&
|
|
83
|
-
!params.hasPendingConfirmation &&
|
|
84
|
-
!params.hasPendingUserInput &&
|
|
85
|
-
!params.hasPendingApproval &&
|
|
86
|
-
!params.hasActiveTask &&
|
|
87
|
-
!params.activeTurnId &&
|
|
88
|
-
params.status !== "busy" &&
|
|
89
|
-
params.status !== "awaiting_approval" &&
|
|
90
|
-
params.status !== "awaiting_input");
|
|
91
|
-
}
|
|
92
|
-
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
93
|
-
return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
|
|
94
|
-
}
|
|
95
|
-
export function isRetryableDeferredCodexDrainError(errorText) {
|
|
96
|
-
return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
|
|
97
|
-
}
|
|
92
|
+
export { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessage, isRetryableDeferredCodexDrainError, shouldDeferCodexInboundMessage, } from "../core/bridge-defer.js";
|
|
98
93
|
export function parseCliArgs(argv) {
|
|
99
94
|
let adapter = null;
|
|
100
95
|
let commandOverride;
|
|
@@ -102,6 +97,7 @@ export function parseCliArgs(argv) {
|
|
|
102
97
|
let profile;
|
|
103
98
|
let lifecycle = "persistent";
|
|
104
99
|
let sessionStartMode = "restore";
|
|
100
|
+
let channelId = "wechat";
|
|
105
101
|
for (let i = 0; i < argv.length; i += 1) {
|
|
106
102
|
const arg = argv[i];
|
|
107
103
|
const next = argv[i + 1];
|
|
@@ -148,6 +144,13 @@ export function parseCliArgs(argv) {
|
|
|
148
144
|
sessionStartMode = next;
|
|
149
145
|
i += 1;
|
|
150
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;
|
|
151
154
|
case "--shutdown-on-parent-exit":
|
|
152
155
|
lifecycle = "companion_bound";
|
|
153
156
|
break;
|
|
@@ -170,6 +173,33 @@ export function parseCliArgs(argv) {
|
|
|
170
173
|
profile,
|
|
171
174
|
lifecycle,
|
|
172
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),
|
|
173
203
|
};
|
|
174
204
|
}
|
|
175
205
|
function printUsageAndExit() {
|
|
@@ -177,7 +207,7 @@ function printUsageAndExit() {
|
|
|
177
207
|
"Internal bridge runtime usage:",
|
|
178
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>]",
|
|
179
209
|
"",
|
|
180
|
-
"This entry is internal. Users should run
|
|
210
|
+
"This entry is internal. Users should run a wechat-* or wecom-* launcher.",
|
|
181
211
|
"",
|
|
182
212
|
].join("\n"));
|
|
183
213
|
process.exit(0);
|
|
@@ -191,21 +221,49 @@ async function main() {
|
|
|
191
221
|
const options = parseCliArgs(process.argv.slice(2));
|
|
192
222
|
const daemonEndpoint = readDaemonEndpoint();
|
|
193
223
|
if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
|
|
194
|
-
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.`);
|
|
195
225
|
}
|
|
196
226
|
if (daemonEndpoint) {
|
|
197
227
|
clearDaemonEndpoint(daemonEndpoint.pid);
|
|
198
228
|
log(`Cleared stale wechat-daemon endpoint for pid=${daemonEndpoint.pid}.`);
|
|
199
229
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
});
|
|
205
240
|
if (!credentials.userId) {
|
|
206
|
-
throw new Error(
|
|
241
|
+
throw new Error(options.channelId === "wecom"
|
|
242
|
+
? "Saved WeCom credentials are missing operatorUserId."
|
|
243
|
+
: "Saved WeChat credentials are missing userId.");
|
|
207
244
|
}
|
|
208
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
|
+
});
|
|
209
267
|
// 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
|
|
210
268
|
// unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
|
|
211
269
|
// 否则会与强制退出 teardown 竞态。
|
|
@@ -224,6 +282,7 @@ async function main() {
|
|
|
224
282
|
const stateStore = new BridgeStateStore({
|
|
225
283
|
...options,
|
|
226
284
|
authorizedUserId: credentials.userId,
|
|
285
|
+
channelId: options.channelId,
|
|
227
286
|
});
|
|
228
287
|
const reapedPeerPids = await reapPeerBridgeProcesses({
|
|
229
288
|
logger: (message) => stateStore.appendLog(message),
|
|
@@ -270,6 +329,9 @@ async function main() {
|
|
|
270
329
|
initialResumeConversationId: stateStore.getState().resumeConversationId,
|
|
271
330
|
initialTranscriptPath: stateStore.getState().transcriptPath,
|
|
272
331
|
});
|
|
332
|
+
const pendingWechatMessages = new PendingWechatMessageStore(options.channelId === "wechat"
|
|
333
|
+
? getPendingWechatMessagesFile(options.cwd)
|
|
334
|
+
: getPendingChannelMessagesFile(options.cwd, options.channelId));
|
|
273
335
|
const resumeCoordinator = new ResumeSessionCoordinator({
|
|
274
336
|
adapter: options.adapter,
|
|
275
337
|
runtime: adapter,
|
|
@@ -280,7 +342,13 @@ async function main() {
|
|
|
280
342
|
let textSendChain = Promise.resolve();
|
|
281
343
|
let attachmentSendChain = Promise.resolve();
|
|
282
344
|
const pendingWechatForwardTasks = new Set();
|
|
283
|
-
|
|
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
|
+
});
|
|
284
352
|
const deferredInboundMessages = [];
|
|
285
353
|
let drainingDeferredInboundMessages = false;
|
|
286
354
|
let consecutivePollFailures = 0;
|
|
@@ -295,48 +363,52 @@ async function main() {
|
|
|
295
363
|
attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
296
364
|
return run;
|
|
297
365
|
};
|
|
298
|
-
const
|
|
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
|
+
});
|
|
381
|
+
};
|
|
382
|
+
const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
|
|
383
|
+
const queuedTarget = senderId === operatorId && channelDriver.capabilities.multiConversation
|
|
384
|
+
? targetOverride ?? turns.resolveTarget(operatorConversation)
|
|
385
|
+
: targetOverride;
|
|
299
386
|
return queueWechatTextAction(async () => {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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);
|
|
390
|
+
if (pending) {
|
|
391
|
+
stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
|
|
304
392
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
321
|
-
context,
|
|
322
|
-
recipientId: senderId,
|
|
323
|
-
attempt,
|
|
324
|
-
delayMs,
|
|
325
|
-
error: err,
|
|
326
|
-
}));
|
|
327
|
-
await delay(delayMs);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
|
|
331
|
-
stateStore.appendLog(formatWechatSendFailureLogEntry({
|
|
332
|
-
context,
|
|
333
|
-
recipientId: senderId,
|
|
334
|
-
error: err,
|
|
335
|
-
}));
|
|
336
|
-
return false;
|
|
393
|
+
}
|
|
394
|
+
return result.status === "sent";
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
const flushPendingWechatMessages = () => {
|
|
398
|
+
return queueWechatTextAction(async () => {
|
|
399
|
+
for (const pending of pendingWechatMessages.list()) {
|
|
400
|
+
const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
|
|
401
|
+
if (result.status === "sent") {
|
|
402
|
+
pendingWechatMessages.remove(pending.id);
|
|
403
|
+
stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (result.status === "target_stale") {
|
|
407
|
+
break;
|
|
337
408
|
}
|
|
409
|
+
stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
410
|
+
break;
|
|
338
411
|
}
|
|
339
|
-
return false;
|
|
340
412
|
});
|
|
341
413
|
};
|
|
342
414
|
const trackWechatForwardTask = (task) => {
|
|
@@ -356,7 +428,9 @@ async function main() {
|
|
|
356
428
|
}
|
|
357
429
|
};
|
|
358
430
|
const outputBatcher = new OutputBatcher(async (text) => {
|
|
359
|
-
await queueWechatMessage(stateStore.getState().authorizedUserId, text
|
|
431
|
+
await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message", channelDriver.capabilities.multiConversation
|
|
432
|
+
? turns.resolveTarget(operatorConversation)
|
|
433
|
+
: undefined);
|
|
360
434
|
});
|
|
361
435
|
const maybeDrainDeferredInboundMessages = async () => {
|
|
362
436
|
if (drainingDeferredInboundMessages || !ensureRuntimeOwnership()) {
|
|
@@ -371,7 +445,7 @@ async function main() {
|
|
|
371
445
|
hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
|
|
372
446
|
hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
|
|
373
447
|
hasPendingApproval: Boolean(adapterState.pendingApproval),
|
|
374
|
-
hasActiveTask:
|
|
448
|
+
hasActiveTask: turns.hasActiveTask,
|
|
375
449
|
})) {
|
|
376
450
|
return;
|
|
377
451
|
}
|
|
@@ -382,13 +456,26 @@ async function main() {
|
|
|
382
456
|
drainingDeferredInboundMessages = true;
|
|
383
457
|
try {
|
|
384
458
|
stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
|
|
385
|
-
const nextTask =
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
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
|
+
}
|
|
392
479
|
}
|
|
393
480
|
catch (err) {
|
|
394
481
|
const errorText = err instanceof Error ? err.message : String(err);
|
|
@@ -403,7 +490,7 @@ async function main() {
|
|
|
403
490
|
adapter: options.adapter,
|
|
404
491
|
cwd: options.cwd,
|
|
405
492
|
errorText,
|
|
406
|
-
}), "inbound_error");
|
|
493
|
+
}), "inbound_error", nextDeferred.channelMessage?.conversation);
|
|
407
494
|
}
|
|
408
495
|
finally {
|
|
409
496
|
drainingDeferredInboundMessages = false;
|
|
@@ -470,6 +557,18 @@ async function main() {
|
|
|
470
557
|
catch {
|
|
471
558
|
// Best effort shutdown.
|
|
472
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
|
+
}
|
|
473
572
|
controller.clearLocalClientEndpoint();
|
|
474
573
|
stateStore.releaseLock();
|
|
475
574
|
};
|
|
@@ -538,6 +637,7 @@ async function main() {
|
|
|
538
637
|
try {
|
|
539
638
|
wireAdapterEvents({
|
|
540
639
|
adapter,
|
|
640
|
+
channelDriver,
|
|
541
641
|
resumeCoordinator,
|
|
542
642
|
options,
|
|
543
643
|
transport,
|
|
@@ -547,8 +647,11 @@ async function main() {
|
|
|
547
647
|
queueWechatMessage,
|
|
548
648
|
trackWechatForwardTask,
|
|
549
649
|
maybeDrainDeferredInboundMessages,
|
|
550
|
-
|
|
551
|
-
|
|
650
|
+
getActiveTask: () => turns.activeTask,
|
|
651
|
+
clearActiveTask: (expectedTask) => {
|
|
652
|
+
if (expectedTask) {
|
|
653
|
+
turns.complete(expectedTask);
|
|
654
|
+
}
|
|
552
655
|
},
|
|
553
656
|
syncSharedSessionState: () => {
|
|
554
657
|
syncSharedSessionState(stateStore, adapter);
|
|
@@ -557,6 +660,8 @@ async function main() {
|
|
|
557
660
|
controller.syncLocalClientEndpoint();
|
|
558
661
|
},
|
|
559
662
|
requestShutdown,
|
|
663
|
+
wecomTransport,
|
|
664
|
+
getWecomTarget: () => turns.resolveTarget(operatorConversation),
|
|
560
665
|
});
|
|
561
666
|
await adapter.start();
|
|
562
667
|
if (!ensureRuntimeOwnership()) {
|
|
@@ -564,13 +669,116 @@ async function main() {
|
|
|
564
669
|
}
|
|
565
670
|
syncSharedSessionState(stateStore, adapter);
|
|
566
671
|
controller.syncLocalClientEndpoint();
|
|
567
|
-
stateStore.appendLog(`Bridge started with adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
568
|
-
|
|
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}".`);
|
|
569
777
|
log(`Working directory: ${options.cwd}`);
|
|
570
778
|
if (options.profile) {
|
|
571
779
|
log(`Profile: ${options.profile}`);
|
|
572
780
|
}
|
|
573
|
-
log(`Authorized
|
|
781
|
+
log(`Authorized ${channelDriver.displayName} user: ${credentials.userId}`);
|
|
574
782
|
if (options.adapter === "codex") {
|
|
575
783
|
log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
|
|
576
784
|
}
|
|
@@ -583,15 +791,17 @@ async function main() {
|
|
|
583
791
|
else if (options.adapter === "pi") {
|
|
584
792
|
log("For source-mode debugging, open the visible Pi client with: npm run pi:companion");
|
|
585
793
|
}
|
|
586
|
-
loadEmojiBindings();
|
|
587
|
-
// Keep the standalone bridge's WeChat welcome brief: command and emoji
|
|
588
|
-
// reference material belongs to /status and /bindings, which users can
|
|
589
|
-
// request on demand. The daemon sends its own, fuller welcome.
|
|
590
794
|
const welcomeText = t("bridge.welcome", {
|
|
591
795
|
adapter: options.adapter,
|
|
592
796
|
cwd: options.cwd,
|
|
593
797
|
});
|
|
594
798
|
await queueWechatMessage(credentials.userId, welcomeText);
|
|
799
|
+
if (channelDriver.capabilities.pushInbound) {
|
|
800
|
+
while (!shutdownPromise && ensureRuntimeOwnership()) {
|
|
801
|
+
await delay(1_000);
|
|
802
|
+
}
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
595
805
|
while (true) {
|
|
596
806
|
if (shutdownPromise) {
|
|
597
807
|
// Shutdown (e.g. SIGINT) was requested while we were awaiting the
|
|
@@ -634,6 +844,9 @@ async function main() {
|
|
|
634
844
|
log(`WeChat long poll recovered after ${recoveredFailures} transient error(s).`);
|
|
635
845
|
stateStore.appendLog(`poll_recovered: failures=${recoveredFailures}`);
|
|
636
846
|
}
|
|
847
|
+
if (pollResult.messages.length > 0 && pendingWechatMessages.list().length > 0) {
|
|
848
|
+
await flushPendingWechatMessages();
|
|
849
|
+
}
|
|
637
850
|
if (pollResult.ignoredBacklogCount > 0) {
|
|
638
851
|
stateStore.incrementIgnoredBacklog(pollResult.ignoredBacklogCount);
|
|
639
852
|
stateStore.appendLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
|
|
@@ -649,11 +862,12 @@ async function main() {
|
|
|
649
862
|
if (!ensureRuntimeOwnership()) {
|
|
650
863
|
break;
|
|
651
864
|
}
|
|
865
|
+
const taskAtMessageStart = turns.activeTask;
|
|
652
866
|
stateStore.touchActivity(message.createdAt);
|
|
653
|
-
let nextTask = null;
|
|
654
867
|
try {
|
|
655
|
-
|
|
868
|
+
await handleInboundMessage({
|
|
656
869
|
message,
|
|
870
|
+
channelDriver,
|
|
657
871
|
options,
|
|
658
872
|
stateStore,
|
|
659
873
|
adapter,
|
|
@@ -661,7 +875,32 @@ async function main() {
|
|
|
661
875
|
queueWechatMessage,
|
|
662
876
|
outputBatcher,
|
|
663
877
|
clearActiveTask: () => {
|
|
664
|
-
|
|
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;
|
|
665
904
|
},
|
|
666
905
|
deferInboundMessage: async (nextMessage) => {
|
|
667
906
|
deferredInboundMessages.push({
|
|
@@ -682,9 +921,6 @@ async function main() {
|
|
|
682
921
|
errorText,
|
|
683
922
|
}), "inbound_error");
|
|
684
923
|
}
|
|
685
|
-
if (nextTask) {
|
|
686
|
-
activeTask = nextTask;
|
|
687
|
-
}
|
|
688
924
|
syncSharedSessionState(stateStore, adapter);
|
|
689
925
|
await maybeDrainDeferredInboundMessages();
|
|
690
926
|
}
|
|
@@ -719,7 +955,30 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
719
955
|
}
|
|
720
956
|
}
|
|
721
957
|
function wireAdapterEvents(params) {
|
|
722
|
-
const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
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
|
+
});
|
|
980
|
+
let lastFinalReplyAtMs = 0;
|
|
981
|
+
let eventForwardChain = Promise.resolve();
|
|
723
982
|
adapter.setEventSink((event) => {
|
|
724
983
|
syncSharedSessionState();
|
|
725
984
|
syncLocalClientEndpoint();
|
|
@@ -732,156 +991,157 @@ function wireAdapterEvents(params) {
|
|
|
732
991
|
stateStore.clearPendingUserInput();
|
|
733
992
|
}
|
|
734
993
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
994
|
+
const eventTarget = channelDriver.capabilities.multiConversation
|
|
995
|
+
? getWecomTarget()
|
|
996
|
+
: channelDriver.directConversation(authorizedUserId);
|
|
997
|
+
const eventTask = getActiveTask();
|
|
998
|
+
eventForwardChain = eventForwardChain
|
|
999
|
+
.then(() => forwardBridgeEvent(event, {
|
|
1000
|
+
stdout: (next) => {
|
|
1001
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
1002
|
+
outputBatcher.push(next.text);
|
|
740
1003
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1004
|
+
},
|
|
1005
|
+
stderr: (next) => {
|
|
1006
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
1007
|
+
outputBatcher.push(next.text);
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
finalReply: (next) => {
|
|
1011
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1012
|
+
lastFinalReplyAtMs = Date.now();
|
|
1013
|
+
stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
|
|
744
1014
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
745
|
-
await
|
|
1015
|
+
await channelPort.send({
|
|
1016
|
+
target: eventTarget,
|
|
1017
|
+
kind: "final_reply",
|
|
1018
|
+
text: next.text,
|
|
746
1019
|
adapter: options.adapter,
|
|
747
|
-
rawText: event.text,
|
|
748
|
-
onEmptyVisibleReply: ({ rawVisibleText }) => {
|
|
749
|
-
stateStore.appendLog(`empty_visible_final_reply: adapter=${options.adapter} raw=${truncatePreview(rawVisibleText)}`);
|
|
750
|
-
},
|
|
751
|
-
sender: {
|
|
752
|
-
sendText: async (text) => {
|
|
753
|
-
const sent = await queueWechatMessage(authorizedUserId, text, "final_reply");
|
|
754
|
-
if (sent) {
|
|
755
|
-
stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
|
|
756
|
-
}
|
|
757
|
-
return sent;
|
|
758
|
-
},
|
|
759
|
-
sendImage: (imagePath) => queueWechatAttachmentAction(() => transport.sendImage(imagePath, { recipientId: authorizedUserId })),
|
|
760
|
-
sendFile: (filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId: authorizedUserId })),
|
|
761
|
-
sendVoice: (voicePath) => queueWechatAttachmentAction(() => transport.sendVoice(voicePath, authorizedUserId)),
|
|
762
|
-
sendVideo: (videoPath) => queueWechatAttachmentAction(() => transport.sendVideo(videoPath, { recipientId: authorizedUserId })),
|
|
763
|
-
},
|
|
764
1020
|
});
|
|
765
1021
|
}));
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (
|
|
769
|
-
log(`${
|
|
770
|
-
stateStore.appendLog(`${
|
|
1022
|
+
},
|
|
1023
|
+
status: (next) => {
|
|
1024
|
+
if (next.message) {
|
|
1025
|
+
log(`${next.status}: ${next.message}`);
|
|
1026
|
+
stateStore.appendLog(`${next.status}: ${next.message}`);
|
|
771
1027
|
}
|
|
772
1028
|
void maybeDrainDeferredInboundMessages();
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
stateStore.appendLog(`${
|
|
776
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
1029
|
+
},
|
|
1030
|
+
notice: (next) => {
|
|
1031
|
+
stateStore.appendLog(`${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
1032
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
777
1033
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
778
|
-
await queueWechatMessage(authorizedUserId,
|
|
1034
|
+
await queueWechatMessage(authorizedUserId, next.text, "notice", eventTarget);
|
|
779
1035
|
}));
|
|
780
1036
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (
|
|
784
|
-
const thinkingPreview = formatThinkingForWechat(
|
|
1037
|
+
},
|
|
1038
|
+
thinking: (next) => {
|
|
1039
|
+
if (next.text) {
|
|
1040
|
+
const thinkingPreview = formatThinkingForWechat(next.text, 500);
|
|
785
1041
|
if (thinkingPreview) {
|
|
786
1042
|
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
787
1043
|
trackWechatForwardTask((async () => {
|
|
788
|
-
await queueWechatMessage(authorizedUserId,
|
|
1044
|
+
await queueWechatMessage(authorizedUserId, channelDriver.id === "wecom"
|
|
1045
|
+
? `Processing: ${thinkingPreview}`
|
|
1046
|
+
: `思考: ${thinkingPreview}`, "thinking", eventTarget);
|
|
789
1047
|
})());
|
|
790
1048
|
}
|
|
791
1049
|
}
|
|
792
|
-
|
|
793
|
-
|
|
1050
|
+
},
|
|
1051
|
+
approvalRequired: (next) => {
|
|
794
1052
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
795
|
-
const pending = toPendingApproval(
|
|
1053
|
+
const pending = toPendingApproval(next.request);
|
|
796
1054
|
stateStore.setPendingConfirmation(pending);
|
|
797
1055
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
798
|
-
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
1056
|
+
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required", eventTarget);
|
|
799
1057
|
}));
|
|
800
|
-
|
|
801
|
-
|
|
1058
|
+
},
|
|
1059
|
+
userInputRequired: (next) => {
|
|
802
1060
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
803
|
-
const pending = toPendingUserInput(
|
|
1061
|
+
const pending = toPendingUserInput(next.request);
|
|
804
1062
|
stateStore.setPendingUserInput(pending);
|
|
805
1063
|
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
806
|
-
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
|
|
1064
|
+
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required", eventTarget);
|
|
807
1065
|
}));
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(
|
|
811
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
1066
|
+
},
|
|
1067
|
+
mirroredUserInput: (next) => {
|
|
1068
|
+
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(next.text)}`);
|
|
1069
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
812
1070
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
813
|
-
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter,
|
|
1071
|
+
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input", eventTarget);
|
|
814
1072
|
}));
|
|
815
1073
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (
|
|
1074
|
+
},
|
|
1075
|
+
sessionSwitched: (next) => {
|
|
1076
|
+
if (next.source === "local")
|
|
819
1077
|
resumeCoordinator.clear();
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
823
|
-
shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
1078
|
+
stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
1079
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
824
1080
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
825
|
-
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
826
|
-
adapter: options.adapter,
|
|
827
|
-
sessionId: event.sessionId,
|
|
828
|
-
source: event.source,
|
|
829
|
-
reason: event.reason,
|
|
830
|
-
}), "session_switched");
|
|
1081
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched", eventTarget);
|
|
831
1082
|
}));
|
|
832
1083
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if (
|
|
1084
|
+
},
|
|
1085
|
+
threadSwitched: (next) => {
|
|
1086
|
+
if (next.source === "local")
|
|
836
1087
|
resumeCoordinator.clear();
|
|
1088
|
+
stateStore.appendLog(`thread_switched: ${next.threadId} source=${next.source} reason=${next.reason}`);
|
|
1089
|
+
if (shouldSuppressCodexLocalThreadNotice({
|
|
1090
|
+
adapter: options.adapter,
|
|
1091
|
+
source: next.source,
|
|
1092
|
+
activeTurnOrigin: adapter.getState().activeTurnOrigin,
|
|
1093
|
+
lastFinalReplyAtMs,
|
|
1094
|
+
})) {
|
|
1095
|
+
return;
|
|
837
1096
|
}
|
|
838
|
-
|
|
839
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
840
|
-
shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
1097
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
841
1098
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
842
|
-
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
843
|
-
adapter: options.adapter,
|
|
844
|
-
sessionId: event.threadId,
|
|
845
|
-
source: event.source,
|
|
846
|
-
reason: event.reason,
|
|
847
|
-
}), "thread_switched");
|
|
1099
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched", eventTarget);
|
|
848
1100
|
}));
|
|
849
1101
|
}
|
|
850
1102
|
void maybeDrainDeferredInboundMessages();
|
|
851
|
-
|
|
852
|
-
|
|
1103
|
+
},
|
|
1104
|
+
taskComplete: () => {
|
|
1105
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
853
1106
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
854
1107
|
stateStore.clearPendingConfirmation();
|
|
855
1108
|
stateStore.clearPendingUserInput();
|
|
856
|
-
clearActiveTask();
|
|
1109
|
+
clearActiveTask(eventTask);
|
|
857
1110
|
await maybeDrainDeferredInboundMessages();
|
|
858
1111
|
}));
|
|
859
|
-
|
|
860
|
-
|
|
1112
|
+
},
|
|
1113
|
+
taskFailed: (next) => {
|
|
1114
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
861
1115
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
862
1116
|
stateStore.clearPendingConfirmation();
|
|
863
1117
|
stateStore.clearPendingUserInput();
|
|
864
|
-
clearActiveTask();
|
|
865
|
-
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter,
|
|
1118
|
+
clearActiveTask(eventTask);
|
|
1119
|
+
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed", eventTarget);
|
|
866
1120
|
await maybeDrainDeferredInboundMessages();
|
|
867
1121
|
}));
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1122
|
+
},
|
|
1123
|
+
fatalError: (next) => {
|
|
1124
|
+
void channelDriver.endTyping?.(authorizedUserId);
|
|
1125
|
+
logError(next.message);
|
|
1126
|
+
stateStore.appendLog(`fatal_error: ${next.message}`);
|
|
872
1127
|
stateStore.clearPendingConfirmation();
|
|
873
1128
|
stateStore.clearPendingUserInput();
|
|
874
|
-
clearActiveTask();
|
|
1129
|
+
clearActiveTask(eventTask);
|
|
875
1130
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
876
|
-
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(
|
|
1131
|
+
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error", eventTarget);
|
|
877
1132
|
await maybeDrainDeferredInboundMessages();
|
|
878
1133
|
}));
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
stateStore.appendLog(`shutdown_requested: ${
|
|
882
|
-
requestShutdown(
|
|
883
|
-
|
|
884
|
-
}
|
|
1134
|
+
},
|
|
1135
|
+
shutdownRequested: (next) => {
|
|
1136
|
+
stateStore.appendLog(`shutdown_requested: ${next.reason}`);
|
|
1137
|
+
requestShutdown(next.message, next.exitCode ?? 0);
|
|
1138
|
+
},
|
|
1139
|
+
}))
|
|
1140
|
+
.catch((error) => {
|
|
1141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1142
|
+
logError(`Bridge event handling failed: ${message}`);
|
|
1143
|
+
stateStore.appendLog(`event_forward_failed: ${message}`);
|
|
1144
|
+
});
|
|
885
1145
|
});
|
|
886
1146
|
}
|
|
887
1147
|
function formatInboundMessagePreview(message) {
|
|
@@ -896,42 +1156,11 @@ function formatInboundMessagePreview(message) {
|
|
|
896
1156
|
return "(empty)";
|
|
897
1157
|
}
|
|
898
1158
|
async function handleInboundMessage(params) {
|
|
899
|
-
|
|
900
|
-
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;
|
|
901
1161
|
const state = stateStore.getState();
|
|
902
1162
|
if (message.senderId !== state.authorizedUserId) {
|
|
903
|
-
await queueWechatMessage(message.senderId,
|
|
904
|
-
return null;
|
|
905
|
-
}
|
|
906
|
-
// Emoji resolution: rewrite message text if it starts with a bound emoji
|
|
907
|
-
const emojiMatch = resolveEmojiCommand(message.text);
|
|
908
|
-
if (emojiMatch) {
|
|
909
|
-
const rewritten = emojiMatch.remainder
|
|
910
|
-
? `${emojiMatch.command} ${emojiMatch.remainder}`
|
|
911
|
-
: emojiMatch.command;
|
|
912
|
-
message = { ...message, text: rewritten };
|
|
913
|
-
}
|
|
914
|
-
// Emoji binding management commands
|
|
915
|
-
const bindingsCmd = parseEmojiBindingsCommand(message.text);
|
|
916
|
-
if (bindingsCmd) {
|
|
917
|
-
switch (bindingsCmd.type) {
|
|
918
|
-
case "list":
|
|
919
|
-
await queueWechatMessage(message.senderId, formatBindingsListMessage(listBindings()));
|
|
920
|
-
break;
|
|
921
|
-
case "bind":
|
|
922
|
-
setBinding(bindingsCmd.emoji, bindingsCmd.command);
|
|
923
|
-
await queueWechatMessage(message.senderId, `Bound ${bindingsCmd.emoji} → ${bindingsCmd.command}`);
|
|
924
|
-
break;
|
|
925
|
-
case "unbind": {
|
|
926
|
-
const removed = removeBinding(bindingsCmd.emoji);
|
|
927
|
-
await queueWechatMessage(message.senderId, removed ? `Unbound ${bindingsCmd.emoji}` : `No binding found for ${bindingsCmd.emoji}`);
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
return null;
|
|
932
|
-
}
|
|
933
|
-
if (isBindCommandPrefix(message.text)) {
|
|
934
|
-
await queueWechatMessage(message.senderId, formatBindCommandUsage());
|
|
1163
|
+
await queueWechatMessage(message.senderId, `Unauthorized. This bridge only accepts messages from ${channelDriver.operatorDescription}.`);
|
|
935
1164
|
return null;
|
|
936
1165
|
}
|
|
937
1166
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
@@ -940,12 +1169,19 @@ async function handleInboundMessage(params) {
|
|
|
940
1169
|
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
941
1170
|
});
|
|
942
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;
|
|
943
1179
|
case "status":
|
|
944
1180
|
await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
|
|
945
1181
|
return null;
|
|
946
1182
|
case "resume": {
|
|
947
1183
|
if (!isWechatResumeEnabled(options.adapter)) {
|
|
948
|
-
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.`);
|
|
949
1185
|
return null;
|
|
950
1186
|
}
|
|
951
1187
|
try {
|
|
@@ -1056,53 +1292,63 @@ async function handleInboundMessage(params) {
|
|
|
1056
1292
|
};
|
|
1057
1293
|
}
|
|
1058
1294
|
}
|
|
1059
|
-
if (state.pendingConfirmation) {
|
|
1060
|
-
await queueWechatMessage(message.senderId, formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()));
|
|
1061
|
-
return null;
|
|
1062
|
-
}
|
|
1063
|
-
if (state.pendingUserInput) {
|
|
1064
|
-
await queueWechatMessage(message.senderId, formatPendingUserInputReminder(state.pendingUserInput));
|
|
1065
|
-
return null;
|
|
1066
|
-
}
|
|
1067
1295
|
const adapterState = adapter.getState();
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1296
|
+
const routeResult = await routeBridgeMessage({
|
|
1297
|
+
message: channelMessage ?? toChannelInboundMessage(message),
|
|
1298
|
+
authorized: true,
|
|
1299
|
+
command: null,
|
|
1300
|
+
adapterState,
|
|
1301
|
+
hasPendingApproval: Boolean(state.pendingConfirmation),
|
|
1302
|
+
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
1303
|
+
shouldDefer: shouldDeferCodexInboundMessage({
|
|
1304
|
+
adapter: options.adapter,
|
|
1305
|
+
status: adapterState.status,
|
|
1306
|
+
activeTurnOrigin: adapterState.activeTurnOrigin,
|
|
1307
|
+
hasPendingConfirmation: Boolean(state.pendingConfirmation),
|
|
1308
|
+
hasSystemCommand: Boolean(systemCommand),
|
|
1309
|
+
}),
|
|
1310
|
+
onUnauthorized: async () => undefined,
|
|
1311
|
+
handleCommand: async () => false,
|
|
1312
|
+
remindPendingApproval: async () => {
|
|
1313
|
+
await queueWechatMessage(message.senderId, formatPendingApprovalReminder(stateStore.getState().pendingConfirmation, adapter.getState()));
|
|
1314
|
+
},
|
|
1315
|
+
remindPendingUserInput: async () => {
|
|
1316
|
+
const pendingUserInput = stateStore.getState().pendingUserInput;
|
|
1317
|
+
await queueWechatMessage(message.senderId, pendingUserInput
|
|
1318
|
+
? formatPendingUserInputReminder(pendingUserInput)
|
|
1319
|
+
: `${options.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`);
|
|
1320
|
+
},
|
|
1321
|
+
remindBusy: async () => {
|
|
1322
|
+
const currentState = adapter.getState();
|
|
1323
|
+
if ((options.adapter === "codex" || options.adapter === "opencode" || options.adapter === "pi") &&
|
|
1324
|
+
currentState.activeTurnOrigin === "local") {
|
|
1325
|
+
await queueWechatMessage(message.senderId, `${options.adapter === "opencode" ? "OpenCode" : options.adapter === "pi" ? "Pi" : "codex"} is currently busy with a local terminal turn. Wait for it to finish or use /stop.`);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
|
|
1329
|
+
},
|
|
1330
|
+
defer: async () => {
|
|
1331
|
+
await deferInboundMessage(message);
|
|
1332
|
+
},
|
|
1333
|
+
dispatch: dispatchInboundText,
|
|
1092
1334
|
});
|
|
1335
|
+
return routeResult.kind === "dispatched" ? routeResult.result : null;
|
|
1093
1336
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const preview = formatInboundMessagePreview(message);
|
|
1097
|
-
const activeTask = {
|
|
1337
|
+
function createActiveTask(message) {
|
|
1338
|
+
return {
|
|
1098
1339
|
startedAt: Date.now(),
|
|
1099
|
-
inputPreview: truncatePreview(
|
|
1340
|
+
inputPreview: truncatePreview(formatInboundMessagePreview(message), 180),
|
|
1100
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);
|
|
1101
1347
|
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
|
|
1102
|
-
await adapter.sendInput(
|
|
1348
|
+
await adapter.sendInput(channelDriver.buildInboundPrompt(message.text, message.attachments));
|
|
1103
1349
|
return activeTask;
|
|
1104
1350
|
}
|
|
1105
|
-
const isDirectRun =
|
|
1351
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
1106
1352
|
if (isDirectRun) {
|
|
1107
1353
|
main().catch((err) => {
|
|
1108
1354
|
logError(describeWechatTransportError(err));
|