cli-wechat-bridge 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -232
- package/bin/_run-entry.mjs +20 -5
- package/bin/wecom-check-update.mjs +5 -0
- package/bin/wecom-claude.mjs +5 -0
- package/bin/wecom-codex.mjs +5 -0
- package/bin/wecom-daemon.mjs +5 -0
- package/bin/wecom-opencode.mjs +5 -0
- package/bin/wecom-pi.mjs +5 -0
- package/bin/wecom-setup.mjs +5 -0
- package/dist/bridge/bridge-adapters.claude.js +128 -4
- package/dist/bridge/bridge-adapters.codex.js +214 -23
- package/dist/bridge/bridge-adapters.core.js +2 -1
- package/dist/bridge/bridge-adapters.pi.js +1 -1
- package/dist/bridge/bridge-adapters.shared.js +2 -1
- package/dist/bridge/bridge-final-reply.js +2 -65
- package/dist/bridge/bridge-process-reaper.js +35 -2
- package/dist/bridge/bridge-state.js +11 -4
- package/dist/bridge/bridge-utils.js +4 -96
- package/dist/bridge/wechat-bridge.js +476 -206
- package/dist/bridge/wechat-forwarding.js +2 -58
- package/dist/bridge/wechat-outbound-queue.js +2 -0
- package/dist/channels/wechat/bridge-final-reply.js +65 -0
- package/dist/channels/wechat/channel-message.js +24 -0
- package/dist/channels/wechat/index.js +5 -0
- package/dist/channels/wechat/wechat-channel-port.js +67 -0
- package/dist/channels/wechat/wechat-forwarding.js +79 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +79 -0
- package/dist/channels/wecom/bridge-final-reply.js +26 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +176 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +66 -0
- package/dist/channels/wecom/wecom-media.js +141 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +308 -0
- package/dist/companion/codex-remote-client.js +2 -1
- package/dist/companion/local-companion-start.js +49 -9
- package/dist/companion/local-companion.js +2 -1
- package/dist/core/bridge-control.js +32 -0
- package/dist/core/bridge-event-forwarder.js +50 -0
- package/dist/core/bridge-message-router.js +32 -0
- package/dist/core/channel-types.js +3 -0
- package/dist/core/direct-run.js +17 -0
- package/dist/core/inbound-message-claims.js +61 -0
- package/dist/core/text-utils.js +63 -0
- package/dist/daemon/daemon-link.js +3 -0
- package/dist/daemon/wechat-daemon.js +466 -172
- package/dist/i18n/messages-en.js +4 -2
- package/dist/i18n/messages-zh.js +4 -2
- package/dist/utils/doctor.js +30 -10
- package/dist/wechat/setup.js +113 -13
- package/package.json +117 -107
|
@@ -4,18 +4,26 @@ import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
|
|
|
4
4
|
import { delay } from "./bridge-adapters.shared.js";
|
|
5
5
|
import { t } from "../i18n/index.js";
|
|
6
6
|
import { BridgeController } from "./bridge-controller.js";
|
|
7
|
-
import { forwardWechatFinalReply } from "./bridge-final-reply.js";
|
|
8
7
|
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "./bridge-session-resume.js";
|
|
9
|
-
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "
|
|
8
|
+
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
|
|
10
9
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
11
10
|
import { BridgeStateStore } from "./bridge-state.js";
|
|
12
11
|
import { getProcessRecordByPid, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "./bridge-process-reaper.js";
|
|
13
12
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
13
|
+
import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
|
|
14
|
+
import { routeBridgeMessage } from "../core/bridge-message-router.js";
|
|
15
|
+
import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
16
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
17
|
+
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
18
|
+
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
19
|
+
import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
|
|
20
|
+
import { WecomTransport } from "../channels/wecom/wecom-transport.js";
|
|
21
|
+
import { buildWecomInboundPrompt, formatWecomVisibleText, } from "../channels/wecom/wecom-message.js";
|
|
14
22
|
import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, formatThinkingForWechat, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
|
|
15
23
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
|
|
24
|
+
import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
|
|
16
25
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
17
26
|
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
27
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
20
28
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
21
29
|
const PARENT_PROCESS_POLL_MS = 5_000;
|
|
@@ -102,6 +110,7 @@ export function parseCliArgs(argv) {
|
|
|
102
110
|
let profile;
|
|
103
111
|
let lifecycle = "persistent";
|
|
104
112
|
let sessionStartMode = "restore";
|
|
113
|
+
let channelId = "wechat";
|
|
105
114
|
for (let i = 0; i < argv.length; i += 1) {
|
|
106
115
|
const arg = argv[i];
|
|
107
116
|
const next = argv[i + 1];
|
|
@@ -148,6 +157,13 @@ export function parseCliArgs(argv) {
|
|
|
148
157
|
sessionStartMode = next;
|
|
149
158
|
i += 1;
|
|
150
159
|
break;
|
|
160
|
+
case "--channel":
|
|
161
|
+
if (!next || (next !== "wechat" && next !== "wecom")) {
|
|
162
|
+
throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
|
|
163
|
+
}
|
|
164
|
+
channelId = next;
|
|
165
|
+
i += 1;
|
|
166
|
+
break;
|
|
151
167
|
case "--shutdown-on-parent-exit":
|
|
152
168
|
lifecycle = "companion_bound";
|
|
153
169
|
break;
|
|
@@ -170,6 +186,51 @@ export function parseCliArgs(argv) {
|
|
|
170
186
|
profile,
|
|
171
187
|
lifecycle,
|
|
172
188
|
sessionStartMode,
|
|
189
|
+
channelId,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function toWecomOutputKind(context) {
|
|
193
|
+
const known = new Set([
|
|
194
|
+
"status",
|
|
195
|
+
"notice",
|
|
196
|
+
"thinking",
|
|
197
|
+
"final_reply",
|
|
198
|
+
"approval_required",
|
|
199
|
+
"user_input_required",
|
|
200
|
+
"mirrored_input",
|
|
201
|
+
"task_failed",
|
|
202
|
+
"fatal_error",
|
|
203
|
+
]);
|
|
204
|
+
return known.has(context)
|
|
205
|
+
? context
|
|
206
|
+
: context === "mirrored_user_input"
|
|
207
|
+
? "mirrored_input"
|
|
208
|
+
: "notice";
|
|
209
|
+
}
|
|
210
|
+
function toWechatSendContext(kind) {
|
|
211
|
+
if (kind === "mirrored_input") {
|
|
212
|
+
return "mirrored_user_input";
|
|
213
|
+
}
|
|
214
|
+
if (kind === "status") {
|
|
215
|
+
return "message";
|
|
216
|
+
}
|
|
217
|
+
return kind;
|
|
218
|
+
}
|
|
219
|
+
function toLegacyInboundWechatMessage(message) {
|
|
220
|
+
return {
|
|
221
|
+
senderId: message.senderId,
|
|
222
|
+
sender: message.senderId,
|
|
223
|
+
sessionId: message.conversation.conversationId,
|
|
224
|
+
text: message.text,
|
|
225
|
+
attachments: message.attachments.map((attachment) => ({
|
|
226
|
+
kind: attachment.kind === "image" ? "image" : "file",
|
|
227
|
+
path: attachment.path,
|
|
228
|
+
fileName: attachment.fileName || path.basename(attachment.path),
|
|
229
|
+
sizeBytes: attachment.sizeBytes ?? 0,
|
|
230
|
+
})),
|
|
231
|
+
contextToken: message.conversation.opaqueRef,
|
|
232
|
+
createdAt: message.createdAt,
|
|
233
|
+
createdAtMs: Date.parse(message.createdAt),
|
|
173
234
|
};
|
|
174
235
|
}
|
|
175
236
|
function printUsageAndExit() {
|
|
@@ -177,7 +238,7 @@ function printUsageAndExit() {
|
|
|
177
238
|
"Internal bridge runtime usage:",
|
|
178
239
|
" npm run bridge -- --adapter <codex|claude|opencode|pi> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>] [--session-start-mode <restore|new>]",
|
|
179
240
|
"",
|
|
180
|
-
"This entry is internal. Users should run
|
|
241
|
+
"This entry is internal. Users should run a wechat-* or wecom-* launcher.",
|
|
181
242
|
"",
|
|
182
243
|
].join("\n"));
|
|
183
244
|
process.exit(0);
|
|
@@ -191,21 +252,37 @@ async function main() {
|
|
|
191
252
|
const options = parseCliArgs(process.argv.slice(2));
|
|
192
253
|
const daemonEndpoint = readDaemonEndpoint();
|
|
193
254
|
if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
|
|
194
|
-
throw new Error(
|
|
255
|
+
throw new Error(`${daemonEndpoint.channelId ?? "wechat"}-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
|
|
195
256
|
}
|
|
196
257
|
if (daemonEndpoint) {
|
|
197
258
|
clearDaemonEndpoint(daemonEndpoint.pid);
|
|
198
259
|
log(`Cleared stale wechat-daemon endpoint for pid=${daemonEndpoint.pid}.`);
|
|
199
260
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
261
|
+
const wecomAccount = options.channelId === "wecom"
|
|
262
|
+
? await ensureWecomAccount({ log })
|
|
263
|
+
: null;
|
|
264
|
+
const credentials = wecomAccount
|
|
265
|
+
? { userId: wecomAccount.operatorUserId }
|
|
266
|
+
: await ensureWechatCredentials({
|
|
267
|
+
requireUserId: true,
|
|
268
|
+
validateExisting: true,
|
|
269
|
+
log,
|
|
270
|
+
});
|
|
205
271
|
if (!credentials.userId) {
|
|
206
|
-
throw new Error(
|
|
272
|
+
throw new Error(options.channelId === "wecom"
|
|
273
|
+
? "Saved WeCom credentials are missing operatorUserId."
|
|
274
|
+
: "Saved WeChat credentials are missing userId.");
|
|
207
275
|
}
|
|
208
276
|
const transport = new WeChatTransport({ log, logError });
|
|
277
|
+
const wecomTransport = wecomAccount
|
|
278
|
+
? new WecomTransport({
|
|
279
|
+
account: wecomAccount,
|
|
280
|
+
logger: {
|
|
281
|
+
log: (message) => log(message),
|
|
282
|
+
error: (message) => logError(message),
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
: null;
|
|
209
286
|
// 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
|
|
210
287
|
// unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
|
|
211
288
|
// 否则会与强制退出 teardown 竞态。
|
|
@@ -224,6 +301,7 @@ async function main() {
|
|
|
224
301
|
const stateStore = new BridgeStateStore({
|
|
225
302
|
...options,
|
|
226
303
|
authorizedUserId: credentials.userId,
|
|
304
|
+
channelId: options.channelId,
|
|
227
305
|
});
|
|
228
306
|
const reapedPeerPids = await reapPeerBridgeProcesses({
|
|
229
307
|
logger: (message) => stateStore.appendLog(message),
|
|
@@ -270,6 +348,9 @@ async function main() {
|
|
|
270
348
|
initialResumeConversationId: stateStore.getState().resumeConversationId,
|
|
271
349
|
initialTranscriptPath: stateStore.getState().transcriptPath,
|
|
272
350
|
});
|
|
351
|
+
const pendingWechatMessages = new PendingWechatMessageStore(options.channelId === "wechat"
|
|
352
|
+
? getPendingWechatMessagesFile(options.cwd)
|
|
353
|
+
: getPendingChannelMessagesFile(options.cwd, options.channelId));
|
|
273
354
|
const resumeCoordinator = new ResumeSessionCoordinator({
|
|
274
355
|
adapter: options.adapter,
|
|
275
356
|
runtime: adapter,
|
|
@@ -281,6 +362,15 @@ async function main() {
|
|
|
281
362
|
let attachmentSendChain = Promise.resolve();
|
|
282
363
|
const pendingWechatForwardTasks = new Set();
|
|
283
364
|
let activeTask = null;
|
|
365
|
+
let currentWecomConversation = null;
|
|
366
|
+
let activeWecomConversation = null;
|
|
367
|
+
let lastWecomConversation = {
|
|
368
|
+
channelId: "wecom",
|
|
369
|
+
accountId: wecomAccount?.botId,
|
|
370
|
+
conversationId: credentials.userId,
|
|
371
|
+
recipientId: credentials.userId,
|
|
372
|
+
metadata: { chatType: "direct" },
|
|
373
|
+
};
|
|
284
374
|
const deferredInboundMessages = [];
|
|
285
375
|
let drainingDeferredInboundMessages = false;
|
|
286
376
|
let consecutivePollFailures = 0;
|
|
@@ -295,48 +385,116 @@ async function main() {
|
|
|
295
385
|
attachmentSendChain = run.then(() => undefined, () => undefined);
|
|
296
386
|
return run;
|
|
297
387
|
};
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
388
|
+
const sendWechatMessageNow = async (senderId, text, context = "message", targetOverride) => {
|
|
389
|
+
if (options.channelId === "wecom") {
|
|
390
|
+
const target = targetOverride ??
|
|
391
|
+
(senderId === credentials.userId
|
|
392
|
+
? currentWecomConversation ??
|
|
393
|
+
activeWecomConversation ??
|
|
394
|
+
lastWecomConversation
|
|
395
|
+
: {
|
|
396
|
+
channelId: "wecom",
|
|
397
|
+
accountId: wecomAccount?.botId,
|
|
398
|
+
conversationId: senderId,
|
|
399
|
+
recipientId: senderId,
|
|
400
|
+
metadata: { chatType: "direct" },
|
|
401
|
+
});
|
|
402
|
+
const startedAtMs = Date.now();
|
|
403
|
+
stateStore.appendLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
|
|
404
|
+
try {
|
|
405
|
+
await wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
|
|
406
|
+
stateStore.appendLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
407
|
+
return { status: "sent" };
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
411
|
+
stateStore.appendLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
|
|
412
|
+
logError(`Failed to send WeCom ${context}: ${message}`);
|
|
413
|
+
return { status: "context_unavailable", error, target };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const startedAtMs = Date.now();
|
|
417
|
+
stateStore.appendLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
|
|
418
|
+
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
419
|
+
try {
|
|
420
|
+
await transport.sendText(senderId, text);
|
|
421
|
+
stateStore.appendLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
422
|
+
return { status: "sent" };
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
426
|
+
stateStore.appendLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
|
|
304
427
|
}
|
|
305
|
-
|
|
428
|
+
if (isWechatContextUnavailableError(err)) {
|
|
306
429
|
if (isWechatContextTokenStaleError(err)) {
|
|
307
430
|
transport.clearCachedContextToken(senderId);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
431
|
+
}
|
|
432
|
+
const hint = "WeChat conversation context is stale or unavailable. Ask the WeChat owner to send any message first, then local terminal replies can sync back to WeChat.";
|
|
433
|
+
logError(`Failed to send WeChat ${context}: ${hint}`);
|
|
434
|
+
stateStore.appendLog(isWechatContextTokenStaleError(err)
|
|
435
|
+
? formatWechatContextTokenStaleLogEntry({
|
|
311
436
|
context,
|
|
312
437
|
recipientId: senderId,
|
|
313
438
|
error: err,
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
318
|
-
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
319
|
-
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
320
|
-
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
439
|
+
})
|
|
440
|
+
: formatWechatSendFailureLogEntry({
|
|
321
441
|
context,
|
|
322
442
|
recipientId: senderId,
|
|
323
|
-
attempt,
|
|
324
|
-
delayMs,
|
|
325
443
|
error: err,
|
|
326
444
|
}));
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
445
|
+
return { status: "context_unavailable", error: err };
|
|
446
|
+
}
|
|
447
|
+
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
448
|
+
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
449
|
+
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
450
|
+
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
332
451
|
context,
|
|
333
452
|
recipientId: senderId,
|
|
453
|
+
attempt,
|
|
454
|
+
delayMs,
|
|
334
455
|
error: err,
|
|
335
456
|
}));
|
|
336
|
-
|
|
457
|
+
await delay(delayMs);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
|
|
461
|
+
stateStore.appendLog(formatWechatSendFailureLogEntry({
|
|
462
|
+
context,
|
|
463
|
+
recipientId: senderId,
|
|
464
|
+
error: err,
|
|
465
|
+
}));
|
|
466
|
+
return { status: "failed" };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return { status: "failed" };
|
|
470
|
+
};
|
|
471
|
+
const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
|
|
472
|
+
return queueWechatTextAction(async () => {
|
|
473
|
+
const result = await sendWechatMessageNow(senderId, text, context, targetOverride);
|
|
474
|
+
if (result.status === "context_unavailable") {
|
|
475
|
+
const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
|
|
476
|
+
if (pending) {
|
|
477
|
+
stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
|
|
337
478
|
}
|
|
338
479
|
}
|
|
339
|
-
return
|
|
480
|
+
return result.status === "sent";
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
const flushPendingWechatMessages = () => {
|
|
484
|
+
return queueWechatTextAction(async () => {
|
|
485
|
+
for (const pending of pendingWechatMessages.list()) {
|
|
486
|
+
const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
|
|
487
|
+
if (result.status === "sent") {
|
|
488
|
+
pendingWechatMessages.remove(pending.id);
|
|
489
|
+
stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (result.status === "context_unavailable") {
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
340
498
|
});
|
|
341
499
|
};
|
|
342
500
|
const trackWechatForwardTask = (task) => {
|
|
@@ -382,6 +540,10 @@ async function main() {
|
|
|
382
540
|
drainingDeferredInboundMessages = true;
|
|
383
541
|
try {
|
|
384
542
|
stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
|
|
543
|
+
if (nextDeferred.channelMessage) {
|
|
544
|
+
activeWecomConversation = nextDeferred.channelMessage.conversation;
|
|
545
|
+
lastWecomConversation = nextDeferred.channelMessage.conversation;
|
|
546
|
+
}
|
|
385
547
|
const nextTask = await dispatchInboundWechatText({
|
|
386
548
|
message: nextDeferred.message,
|
|
387
549
|
options,
|
|
@@ -470,6 +632,12 @@ async function main() {
|
|
|
470
632
|
catch {
|
|
471
633
|
// Best effort shutdown.
|
|
472
634
|
}
|
|
635
|
+
try {
|
|
636
|
+
wecomTransport?.stop();
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// Best effort shutdown.
|
|
640
|
+
}
|
|
473
641
|
controller.clearLocalClientEndpoint();
|
|
474
642
|
stateStore.releaseLock();
|
|
475
643
|
};
|
|
@@ -549,6 +717,7 @@ async function main() {
|
|
|
549
717
|
maybeDrainDeferredInboundMessages,
|
|
550
718
|
clearActiveTask: () => {
|
|
551
719
|
activeTask = null;
|
|
720
|
+
activeWecomConversation = null;
|
|
552
721
|
},
|
|
553
722
|
syncSharedSessionState: () => {
|
|
554
723
|
syncSharedSessionState(stateStore, adapter);
|
|
@@ -557,6 +726,8 @@ async function main() {
|
|
|
557
726
|
controller.syncLocalClientEndpoint();
|
|
558
727
|
},
|
|
559
728
|
requestShutdown,
|
|
729
|
+
wecomTransport,
|
|
730
|
+
getWecomTarget: () => activeWecomConversation ?? lastWecomConversation,
|
|
560
731
|
});
|
|
561
732
|
await adapter.start();
|
|
562
733
|
if (!ensureRuntimeOwnership()) {
|
|
@@ -564,13 +735,98 @@ async function main() {
|
|
|
564
735
|
}
|
|
565
736
|
syncSharedSessionState(stateStore, adapter);
|
|
566
737
|
controller.syncLocalClientEndpoint();
|
|
567
|
-
stateStore.appendLog(`Bridge started with adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
568
|
-
|
|
738
|
+
stateStore.appendLog(`Bridge started with channel=${options.channelId} adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
|
|
739
|
+
if (wecomTransport) {
|
|
740
|
+
wecomTransport.setHandlers({
|
|
741
|
+
onMessage: async (channelMessage) => {
|
|
742
|
+
if (shutdownPromise || !ensureRuntimeOwnership()) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
currentWecomConversation = channelMessage.conversation;
|
|
746
|
+
lastWecomConversation = channelMessage.conversation;
|
|
747
|
+
try {
|
|
748
|
+
if (pendingWechatMessages.list().length > 0) {
|
|
749
|
+
await flushPendingWechatMessages();
|
|
750
|
+
}
|
|
751
|
+
const message = toLegacyInboundWechatMessage(channelMessage);
|
|
752
|
+
stateStore.touchActivity(message.createdAt);
|
|
753
|
+
let nextTask = null;
|
|
754
|
+
try {
|
|
755
|
+
nextTask = await handleInboundMessage({
|
|
756
|
+
message,
|
|
757
|
+
channelMessage,
|
|
758
|
+
options,
|
|
759
|
+
stateStore,
|
|
760
|
+
adapter,
|
|
761
|
+
resumeCoordinator,
|
|
762
|
+
queueWechatMessage,
|
|
763
|
+
outputBatcher,
|
|
764
|
+
clearActiveTask: () => {
|
|
765
|
+
activeTask = null;
|
|
766
|
+
activeWecomConversation = null;
|
|
767
|
+
},
|
|
768
|
+
deferInboundMessage: async (nextMessage) => {
|
|
769
|
+
deferredInboundMessages.push({
|
|
770
|
+
message: nextMessage,
|
|
771
|
+
channelMessage,
|
|
772
|
+
});
|
|
773
|
+
stateStore.appendLog(`deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`);
|
|
774
|
+
await queueWechatMessage(nextMessage.senderId, formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length));
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
780
|
+
logError(errorText);
|
|
781
|
+
stateStore.appendLog(`inbound_error: ${errorText}`);
|
|
782
|
+
await queueWechatMessage(message.senderId, formatUserFacingInboundError({
|
|
783
|
+
adapter: options.adapter,
|
|
784
|
+
cwd: options.cwd,
|
|
785
|
+
errorText,
|
|
786
|
+
}), "inbound_error");
|
|
787
|
+
}
|
|
788
|
+
if (nextTask) {
|
|
789
|
+
activeTask = nextTask;
|
|
790
|
+
activeWecomConversation = channelMessage.conversation;
|
|
791
|
+
}
|
|
792
|
+
syncSharedSessionState(stateStore, adapter);
|
|
793
|
+
await maybeDrainDeferredInboundMessages();
|
|
794
|
+
}
|
|
795
|
+
finally {
|
|
796
|
+
currentWecomConversation = null;
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
onUnauthorized: async (senderId, chatType) => {
|
|
800
|
+
stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
|
|
801
|
+
if (chatType === "direct") {
|
|
802
|
+
await wecomTransport.sendText({
|
|
803
|
+
channelId: "wecom",
|
|
804
|
+
accountId: wecomAccount.botId,
|
|
805
|
+
conversationId: senderId,
|
|
806
|
+
recipientId: senderId,
|
|
807
|
+
metadata: { chatType: "direct" },
|
|
808
|
+
}, "Unauthorized.", "notice");
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
onFatal: async (error) => {
|
|
812
|
+
stateStore.appendLog(`wecom_fatal_error: ${error.message}`);
|
|
813
|
+
requestShutdown(error.message, 1);
|
|
814
|
+
},
|
|
815
|
+
onConnected: async () => {
|
|
816
|
+
if (pendingWechatMessages.list().length > 0) {
|
|
817
|
+
await flushPendingWechatMessages();
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
wecomTransport.start();
|
|
822
|
+
await wecomTransport.waitUntilConnected();
|
|
823
|
+
}
|
|
824
|
+
log(`${options.channelId === "wecom" ? "WeCom" : "WeChat"} bridge is ready for adapter "${options.adapter}".`);
|
|
569
825
|
log(`Working directory: ${options.cwd}`);
|
|
570
826
|
if (options.profile) {
|
|
571
827
|
log(`Profile: ${options.profile}`);
|
|
572
828
|
}
|
|
573
|
-
log(`Authorized WeChat user: ${credentials.userId}`);
|
|
829
|
+
log(`Authorized ${options.channelId === "wecom" ? "WeCom" : "WeChat"} user: ${credentials.userId}`);
|
|
574
830
|
if (options.adapter === "codex") {
|
|
575
831
|
log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
|
|
576
832
|
}
|
|
@@ -583,15 +839,17 @@ async function main() {
|
|
|
583
839
|
else if (options.adapter === "pi") {
|
|
584
840
|
log("For source-mode debugging, open the visible Pi client with: npm run pi:companion");
|
|
585
841
|
}
|
|
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
842
|
const welcomeText = t("bridge.welcome", {
|
|
591
843
|
adapter: options.adapter,
|
|
592
844
|
cwd: options.cwd,
|
|
593
845
|
});
|
|
594
846
|
await queueWechatMessage(credentials.userId, welcomeText);
|
|
847
|
+
if (options.channelId === "wecom") {
|
|
848
|
+
while (!shutdownPromise && ensureRuntimeOwnership()) {
|
|
849
|
+
await delay(1_000);
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
595
853
|
while (true) {
|
|
596
854
|
if (shutdownPromise) {
|
|
597
855
|
// Shutdown (e.g. SIGINT) was requested while we were awaiting the
|
|
@@ -634,6 +892,9 @@ async function main() {
|
|
|
634
892
|
log(`WeChat long poll recovered after ${recoveredFailures} transient error(s).`);
|
|
635
893
|
stateStore.appendLog(`poll_recovered: failures=${recoveredFailures}`);
|
|
636
894
|
}
|
|
895
|
+
if (pollResult.messages.length > 0 && pendingWechatMessages.list().length > 0) {
|
|
896
|
+
await flushPendingWechatMessages();
|
|
897
|
+
}
|
|
637
898
|
if (pollResult.ignoredBacklogCount > 0) {
|
|
638
899
|
stateStore.incrementIgnoredBacklog(pollResult.ignoredBacklogCount);
|
|
639
900
|
stateStore.appendLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
|
|
@@ -719,7 +980,30 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
719
980
|
}
|
|
720
981
|
}
|
|
721
982
|
function wireAdapterEvents(params) {
|
|
722
|
-
const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
983
|
+
const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
|
|
984
|
+
const channelPort = options.channelId === "wecom"
|
|
985
|
+
? new WecomChannelPort({
|
|
986
|
+
transport: wecomTransport,
|
|
987
|
+
sendText: (target, text, kind) => queueWechatMessage(stateStore.getState().authorizedUserId, text, toWechatSendContext(kind), target),
|
|
988
|
+
onEmptyVisibleReply: (adapter, rawText) => {
|
|
989
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
|
|
990
|
+
},
|
|
991
|
+
})
|
|
992
|
+
: new WechatChannelPort({
|
|
993
|
+
sendText: (recipientId, text, context) => queueWechatMessage(recipientId, text, context),
|
|
994
|
+
sendImage: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
|
|
995
|
+
sendFile: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
|
|
996
|
+
sendVoice: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
|
|
997
|
+
sendVideo: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVideo(filePath, { recipientId })),
|
|
998
|
+
onEmptyVisibleReply: (adapter, rawText) => {
|
|
999
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
|
|
1000
|
+
},
|
|
1001
|
+
onTextSent: (_adapter, text) => {
|
|
1002
|
+
stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
1005
|
+
let lastFinalReplyAtMs = 0;
|
|
1006
|
+
let eventForwardChain = Promise.resolve();
|
|
723
1007
|
adapter.setEventSink((event) => {
|
|
724
1008
|
syncSharedSessionState();
|
|
725
1009
|
syncLocalClientEndpoint();
|
|
@@ -732,156 +1016,155 @@ function wireAdapterEvents(params) {
|
|
|
732
1016
|
stateStore.clearPendingUserInput();
|
|
733
1017
|
}
|
|
734
1018
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
739
|
-
outputBatcher.push(
|
|
1019
|
+
eventForwardChain = eventForwardChain
|
|
1020
|
+
.then(() => forwardBridgeEvent(event, {
|
|
1021
|
+
stdout: (next) => {
|
|
1022
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
1023
|
+
outputBatcher.push(next.text);
|
|
740
1024
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1025
|
+
},
|
|
1026
|
+
stderr: (next) => {
|
|
1027
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
1028
|
+
outputBatcher.push(next.text);
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
finalReply: (next) => {
|
|
1032
|
+
lastFinalReplyAtMs = Date.now();
|
|
1033
|
+
stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
|
|
744
1034
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
745
|
-
await
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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;
|
|
1035
|
+
await channelPort.send({
|
|
1036
|
+
target: options.channelId === "wecom"
|
|
1037
|
+
? getWecomTarget()
|
|
1038
|
+
: {
|
|
1039
|
+
channelId: "wechat",
|
|
1040
|
+
conversationId: authorizedUserId,
|
|
1041
|
+
recipientId: authorizedUserId,
|
|
758
1042
|
},
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
sendVideo: (videoPath) => queueWechatAttachmentAction(() => transport.sendVideo(videoPath, { recipientId: authorizedUserId })),
|
|
763
|
-
},
|
|
1043
|
+
kind: "final_reply",
|
|
1044
|
+
text: next.text,
|
|
1045
|
+
adapter: options.adapter,
|
|
764
1046
|
});
|
|
765
1047
|
}));
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (
|
|
769
|
-
log(`${
|
|
770
|
-
stateStore.appendLog(`${
|
|
1048
|
+
},
|
|
1049
|
+
status: (next) => {
|
|
1050
|
+
if (next.message) {
|
|
1051
|
+
log(`${next.status}: ${next.message}`);
|
|
1052
|
+
stateStore.appendLog(`${next.status}: ${next.message}`);
|
|
771
1053
|
}
|
|
772
1054
|
void maybeDrainDeferredInboundMessages();
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
stateStore.appendLog(`${
|
|
776
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
1055
|
+
},
|
|
1056
|
+
notice: (next) => {
|
|
1057
|
+
stateStore.appendLog(`${next.level}_notice: ${truncatePreview(next.text)}`);
|
|
1058
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
777
1059
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
778
|
-
await queueWechatMessage(authorizedUserId,
|
|
1060
|
+
await queueWechatMessage(authorizedUserId, next.text, "notice");
|
|
779
1061
|
}));
|
|
780
1062
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (
|
|
784
|
-
const thinkingPreview = formatThinkingForWechat(
|
|
1063
|
+
},
|
|
1064
|
+
thinking: (next) => {
|
|
1065
|
+
if (next.text) {
|
|
1066
|
+
const thinkingPreview = formatThinkingForWechat(next.text, 500);
|
|
785
1067
|
if (thinkingPreview) {
|
|
786
1068
|
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
787
1069
|
trackWechatForwardTask((async () => {
|
|
788
|
-
await queueWechatMessage(authorizedUserId,
|
|
1070
|
+
await queueWechatMessage(authorizedUserId, options.channelId === "wecom"
|
|
1071
|
+
? `Processing: ${thinkingPreview}`
|
|
1072
|
+
: `思考: ${thinkingPreview}`, "thinking");
|
|
789
1073
|
})());
|
|
790
1074
|
}
|
|
791
1075
|
}
|
|
792
|
-
|
|
793
|
-
|
|
1076
|
+
},
|
|
1077
|
+
approvalRequired: (next) => {
|
|
794
1078
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
795
|
-
const pending = toPendingApproval(
|
|
1079
|
+
const pending = toPendingApproval(next.request);
|
|
796
1080
|
stateStore.setPendingConfirmation(pending);
|
|
797
1081
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
798
1082
|
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
799
1083
|
}));
|
|
800
|
-
|
|
801
|
-
|
|
1084
|
+
},
|
|
1085
|
+
userInputRequired: (next) => {
|
|
802
1086
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
803
|
-
const pending = toPendingUserInput(
|
|
1087
|
+
const pending = toPendingUserInput(next.request);
|
|
804
1088
|
stateStore.setPendingUserInput(pending);
|
|
805
1089
|
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
806
1090
|
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
|
|
807
1091
|
}));
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(
|
|
811
|
-
if (shouldForwardBridgeEventToWechat(options.adapter,
|
|
1092
|
+
},
|
|
1093
|
+
mirroredUserInput: (next) => {
|
|
1094
|
+
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(next.text)}`);
|
|
1095
|
+
if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
|
|
812
1096
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
813
|
-
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter,
|
|
1097
|
+
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input");
|
|
814
1098
|
}));
|
|
815
1099
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (
|
|
1100
|
+
},
|
|
1101
|
+
sessionSwitched: (next) => {
|
|
1102
|
+
if (next.source === "local")
|
|
819
1103
|
resumeCoordinator.clear();
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
823
|
-
shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
1104
|
+
stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
|
|
1105
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
824
1106
|
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");
|
|
1107
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched");
|
|
831
1108
|
}));
|
|
832
1109
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if (
|
|
1110
|
+
},
|
|
1111
|
+
threadSwitched: (next) => {
|
|
1112
|
+
if (next.source === "local")
|
|
836
1113
|
resumeCoordinator.clear();
|
|
1114
|
+
stateStore.appendLog(`thread_switched: ${next.threadId} source=${next.source} reason=${next.reason}`);
|
|
1115
|
+
if (shouldSuppressCodexLocalThreadNotice({
|
|
1116
|
+
adapter: options.adapter,
|
|
1117
|
+
source: next.source,
|
|
1118
|
+
activeTurnOrigin: adapter.getState().activeTurnOrigin,
|
|
1119
|
+
lastFinalReplyAtMs,
|
|
1120
|
+
})) {
|
|
1121
|
+
return;
|
|
837
1122
|
}
|
|
838
|
-
|
|
839
|
-
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
840
|
-
shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
1123
|
+
if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
|
|
841
1124
|
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");
|
|
1125
|
+
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched");
|
|
848
1126
|
}));
|
|
849
1127
|
}
|
|
850
1128
|
void maybeDrainDeferredInboundMessages();
|
|
851
|
-
|
|
852
|
-
|
|
1129
|
+
},
|
|
1130
|
+
taskComplete: () => {
|
|
853
1131
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
854
1132
|
stateStore.clearPendingConfirmation();
|
|
855
1133
|
stateStore.clearPendingUserInput();
|
|
856
1134
|
clearActiveTask();
|
|
857
1135
|
await maybeDrainDeferredInboundMessages();
|
|
858
1136
|
}));
|
|
859
|
-
|
|
860
|
-
|
|
1137
|
+
},
|
|
1138
|
+
taskFailed: (next) => {
|
|
861
1139
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
862
1140
|
stateStore.clearPendingConfirmation();
|
|
863
1141
|
stateStore.clearPendingUserInput();
|
|
864
1142
|
clearActiveTask();
|
|
865
|
-
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter,
|
|
1143
|
+
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed");
|
|
866
1144
|
await maybeDrainDeferredInboundMessages();
|
|
867
1145
|
}));
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
logError(
|
|
871
|
-
stateStore.appendLog(`fatal_error: ${
|
|
1146
|
+
},
|
|
1147
|
+
fatalError: (next) => {
|
|
1148
|
+
logError(next.message);
|
|
1149
|
+
stateStore.appendLog(`fatal_error: ${next.message}`);
|
|
872
1150
|
stateStore.clearPendingConfirmation();
|
|
873
1151
|
stateStore.clearPendingUserInput();
|
|
874
1152
|
clearActiveTask();
|
|
875
1153
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
876
|
-
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(
|
|
1154
|
+
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error");
|
|
877
1155
|
await maybeDrainDeferredInboundMessages();
|
|
878
1156
|
}));
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
stateStore.appendLog(`shutdown_requested: ${
|
|
882
|
-
requestShutdown(
|
|
883
|
-
|
|
884
|
-
}
|
|
1157
|
+
},
|
|
1158
|
+
shutdownRequested: (next) => {
|
|
1159
|
+
stateStore.appendLog(`shutdown_requested: ${next.reason}`);
|
|
1160
|
+
requestShutdown(next.message, next.exitCode ?? 0);
|
|
1161
|
+
},
|
|
1162
|
+
}))
|
|
1163
|
+
.catch((error) => {
|
|
1164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1165
|
+
logError(`Bridge event handling failed: ${message}`);
|
|
1166
|
+
stateStore.appendLog(`event_forward_failed: ${message}`);
|
|
1167
|
+
});
|
|
885
1168
|
});
|
|
886
1169
|
}
|
|
887
1170
|
function formatInboundMessagePreview(message) {
|
|
@@ -896,42 +1179,13 @@ function formatInboundMessagePreview(message) {
|
|
|
896
1179
|
return "(empty)";
|
|
897
1180
|
}
|
|
898
1181
|
async function handleInboundMessage(params) {
|
|
899
|
-
|
|
1182
|
+
const { message, channelMessage, } = params;
|
|
900
1183
|
const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, deferInboundMessage, } = params;
|
|
901
1184
|
const state = stateStore.getState();
|
|
902
1185
|
if (message.senderId !== state.authorizedUserId) {
|
|
903
|
-
await queueWechatMessage(message.senderId,
|
|
904
|
-
|
|
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());
|
|
1186
|
+
await queueWechatMessage(message.senderId, options.channelId === "wecom"
|
|
1187
|
+
? "Unauthorized. This bridge only accepts messages from the paired WeCom operator."
|
|
1188
|
+
: "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
935
1189
|
return null;
|
|
936
1190
|
}
|
|
937
1191
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
@@ -945,7 +1199,7 @@ async function handleInboundMessage(params) {
|
|
|
945
1199
|
return null;
|
|
946
1200
|
case "resume": {
|
|
947
1201
|
if (!isWechatResumeEnabled(options.adapter)) {
|
|
948
|
-
await queueWechatMessage(message.senderId,
|
|
1202
|
+
await queueWechatMessage(message.senderId, `${options.channelId === "wecom" ? "WeCom" : "WeChat"} /resume is disabled in ${options.adapter} mode. Use /resume directly inside "${options.channelId}-${options.adapter}"; the remote channel will follow the active local session.`);
|
|
949
1203
|
return null;
|
|
950
1204
|
}
|
|
951
1205
|
try {
|
|
@@ -1056,40 +1310,54 @@ async function handleInboundMessage(params) {
|
|
|
1056
1310
|
};
|
|
1057
1311
|
}
|
|
1058
1312
|
}
|
|
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
1313
|
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
|
-
|
|
1314
|
+
const routeResult = await routeBridgeMessage({
|
|
1315
|
+
message: channelMessage ?? toChannelInboundMessage(message),
|
|
1316
|
+
authorized: true,
|
|
1317
|
+
command: null,
|
|
1318
|
+
adapterState,
|
|
1319
|
+
hasPendingApproval: Boolean(state.pendingConfirmation),
|
|
1320
|
+
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
1321
|
+
shouldDefer: shouldDeferCodexInboundMessage({
|
|
1322
|
+
adapter: options.adapter,
|
|
1323
|
+
status: adapterState.status,
|
|
1324
|
+
activeTurnOrigin: adapterState.activeTurnOrigin,
|
|
1325
|
+
hasPendingConfirmation: Boolean(state.pendingConfirmation),
|
|
1326
|
+
hasSystemCommand: Boolean(systemCommand),
|
|
1327
|
+
}),
|
|
1328
|
+
onUnauthorized: async () => undefined,
|
|
1329
|
+
handleCommand: async () => false,
|
|
1330
|
+
remindPendingApproval: async () => {
|
|
1331
|
+
await queueWechatMessage(message.senderId, formatPendingApprovalReminder(stateStore.getState().pendingConfirmation, adapter.getState()));
|
|
1332
|
+
},
|
|
1333
|
+
remindPendingUserInput: async () => {
|
|
1334
|
+
const pendingUserInput = stateStore.getState().pendingUserInput;
|
|
1335
|
+
await queueWechatMessage(message.senderId, pendingUserInput
|
|
1336
|
+
? formatPendingUserInputReminder(pendingUserInput)
|
|
1337
|
+
: `${options.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`);
|
|
1338
|
+
},
|
|
1339
|
+
remindBusy: async () => {
|
|
1340
|
+
const currentState = adapter.getState();
|
|
1341
|
+
if ((options.adapter === "codex" || options.adapter === "opencode" || options.adapter === "pi") &&
|
|
1342
|
+
currentState.activeTurnOrigin === "local") {
|
|
1343
|
+
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.`);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
|
|
1347
|
+
},
|
|
1348
|
+
defer: async () => {
|
|
1349
|
+
await deferInboundMessage(message);
|
|
1350
|
+
},
|
|
1351
|
+
dispatch: async () => {
|
|
1352
|
+
return await dispatchInboundWechatText({
|
|
1353
|
+
message,
|
|
1354
|
+
options,
|
|
1355
|
+
stateStore,
|
|
1356
|
+
adapter,
|
|
1357
|
+
});
|
|
1358
|
+
},
|
|
1092
1359
|
});
|
|
1360
|
+
return routeResult.kind === "dispatched" ? routeResult.result : null;
|
|
1093
1361
|
}
|
|
1094
1362
|
async function dispatchInboundWechatText(params) {
|
|
1095
1363
|
const { message, options, stateStore, adapter } = params;
|
|
@@ -1099,10 +1367,12 @@ async function dispatchInboundWechatText(params) {
|
|
|
1099
1367
|
inputPreview: truncatePreview(preview, 180),
|
|
1100
1368
|
};
|
|
1101
1369
|
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
|
|
1102
|
-
await adapter.sendInput(
|
|
1370
|
+
await adapter.sendInput(options.channelId === "wecom"
|
|
1371
|
+
? buildWecomInboundPrompt(message.text, message.attachments)
|
|
1372
|
+
: buildWechatInboundPrompt(message.text, message.attachments));
|
|
1103
1373
|
return activeTask;
|
|
1104
1374
|
}
|
|
1105
|
-
const isDirectRun =
|
|
1375
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
1106
1376
|
if (isDirectRun) {
|
|
1107
1377
|
main().catch((err) => {
|
|
1108
1378
|
logError(describeWechatTransportError(err));
|