cli-wechat-bridge 1.0.5

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.
Files changed (54) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +637 -0
  3. package/bin/_run-entry.mjs +35 -0
  4. package/bin/wechat-bridge-claude.mjs +5 -0
  5. package/bin/wechat-bridge-codex.mjs +5 -0
  6. package/bin/wechat-bridge-opencode.mjs +5 -0
  7. package/bin/wechat-bridge-shell.mjs +5 -0
  8. package/bin/wechat-bridge.mjs +5 -0
  9. package/bin/wechat-check-update.mjs +5 -0
  10. package/bin/wechat-claude-start.mjs +5 -0
  11. package/bin/wechat-claude.mjs +5 -0
  12. package/bin/wechat-codex-start.mjs +5 -0
  13. package/bin/wechat-codex.mjs +5 -0
  14. package/bin/wechat-daemon.mjs +5 -0
  15. package/bin/wechat-opencode-start.mjs +5 -0
  16. package/bin/wechat-opencode.mjs +5 -0
  17. package/bin/wechat-setup.mjs +5 -0
  18. package/dist/bridge/bridge-adapter-common.js +95 -0
  19. package/dist/bridge/bridge-adapters.claude.js +829 -0
  20. package/dist/bridge/bridge-adapters.codex.js +2228 -0
  21. package/dist/bridge/bridge-adapters.core.js +717 -0
  22. package/dist/bridge/bridge-adapters.js +26 -0
  23. package/dist/bridge/bridge-adapters.opencode.js +2129 -0
  24. package/dist/bridge/bridge-adapters.shared.js +1005 -0
  25. package/dist/bridge/bridge-adapters.shell.js +363 -0
  26. package/dist/bridge/bridge-controller.js +48 -0
  27. package/dist/bridge/bridge-final-reply.js +46 -0
  28. package/dist/bridge/bridge-process-reaper.js +348 -0
  29. package/dist/bridge/bridge-state.js +362 -0
  30. package/dist/bridge/bridge-types.js +1 -0
  31. package/dist/bridge/bridge-utils.js +1240 -0
  32. package/dist/bridge/claude-hook.js +82 -0
  33. package/dist/bridge/claude-hooks.js +267 -0
  34. package/dist/bridge/wechat-bridge.js +1026 -0
  35. package/dist/commands/check-update.js +30 -0
  36. package/dist/companion/codex-panel-link.js +72 -0
  37. package/dist/companion/codex-panel.js +179 -0
  38. package/dist/companion/codex-remote-client.js +124 -0
  39. package/dist/companion/local-companion-link.js +240 -0
  40. package/dist/companion/local-companion-start.js +420 -0
  41. package/dist/companion/local-companion.js +424 -0
  42. package/dist/daemon/daemon-link.js +175 -0
  43. package/dist/daemon/wechat-daemon.js +1202 -0
  44. package/dist/media/media-types.js +1 -0
  45. package/dist/runtime/create-runtime-host.js +12 -0
  46. package/dist/runtime/legacy-adapter-runtime.js +46 -0
  47. package/dist/runtime/runtime-types.js +5 -0
  48. package/dist/utils/version-checker.js +161 -0
  49. package/dist/wechat/channel-config.js +196 -0
  50. package/dist/wechat/setup.js +283 -0
  51. package/dist/wechat/standalone-bot.js +355 -0
  52. package/dist/wechat/wechat-channel.js +492 -0
  53. package/dist/wechat/wechat-transport.js +1213 -0
  54. package/package.json +101 -0
@@ -0,0 +1,1026 @@
1
+ #!/usr/bin/env bun
2
+ import path from "node:path";
3
+ import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
4
+ import { delay } from "./bridge-adapters.shared.js";
5
+ import { BridgeController } from "./bridge-controller.js";
6
+ import { forwardWechatFinalReply } from "./bridge-final-reply.js";
7
+ import { ensureWechatCredentials } from "../wechat/setup.js";
8
+ import { BridgeStateStore } from "./bridge-state.js";
9
+ import { reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses } from "./bridge-process-reaper.js";
10
+ import { createRuntimeHost } from "../runtime/create-runtime-host.js";
11
+ import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
12
+ import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
13
+ import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
14
+ import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
15
+ const POLL_RETRY_BASE_MS = 1_000;
16
+ const POLL_RETRY_MAX_MS = 30_000;
17
+ const PARENT_PROCESS_POLL_MS = 5_000;
18
+ const WECHAT_SEND_MAX_ATTEMPTS = 3;
19
+ const WECHAT_SEND_RETRY_BASE_MS = 750;
20
+ function log(message) {
21
+ process.stderr.write(`[wechat-bridge] ${message}\n`);
22
+ }
23
+ function logError(message) {
24
+ process.stderr.write(`[wechat-bridge] ERROR: ${message}\n`);
25
+ }
26
+ function computePollRetryDelayMs(consecutiveFailures) {
27
+ const normalizedFailures = Math.max(1, consecutiveFailures);
28
+ const exponent = Math.min(normalizedFailures - 1, 5);
29
+ return Math.min(POLL_RETRY_MAX_MS, POLL_RETRY_BASE_MS * 2 ** exponent);
30
+ }
31
+ function isPidAlive(pid) {
32
+ if (!Number.isInteger(pid) || pid <= 0) {
33
+ return false;
34
+ }
35
+ try {
36
+ process.kill(pid, 0);
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ export function formatUserFacingBridgeFatalError(message) {
44
+ return `Bridge error: ${message.replace(/\s+Recent app-server log:.*$/s, "").trim()}`;
45
+ }
46
+ export function shouldForwardBridgeEventToWechat(adapter, eventType, options = {}) {
47
+ if (adapter !== "opencode") {
48
+ return true;
49
+ }
50
+ switch (eventType) {
51
+ case "stdout":
52
+ case "stderr":
53
+ case "thread_switched":
54
+ return false;
55
+ case "notice":
56
+ return /^OpenCode local draft:\s*/i.test(options.text ?? "");
57
+ case "mirrored_user_input":
58
+ return true;
59
+ default:
60
+ return true;
61
+ }
62
+ }
63
+ export function formatUserFacingInboundError(params) {
64
+ const { adapter, cwd, errorText, isUserFacingShellRejection } = params;
65
+ if (isUserFacingShellRejection) {
66
+ return errorText;
67
+ }
68
+ if (adapter === "opencode" &&
69
+ /opencode companion is not connected/i.test(errorText)) {
70
+ return cwd
71
+ ? `OpenCode companion is not connected for bridge workspace:\n${cwd}\nRun "wechat-opencode" in that directory to reconnect the current local terminal, or run "wechat-bridge-opencode" and then "wechat-opencode" in your target project to replace this bridge.`
72
+ : 'OpenCode companion is not connected. Start "wechat-opencode" in this directory to reconnect it, then retry.';
73
+ }
74
+ return `Bridge error: ${errorText}`;
75
+ }
76
+ export function formatWechatSendFailureLogEntry(params) {
77
+ return `wechat_send_failed: context=${params.context} recipient=${params.recipientId} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
78
+ }
79
+ export function formatWechatContextTokenStaleLogEntry(params) {
80
+ return `wechat_context_token_stale: context=${params.context} recipient=${params.recipientId} action=wechat_message_required error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
81
+ }
82
+ function formatWechatSendRetryLogEntry(params) {
83
+ return `wechat_send_retry: context=${params.context} recipient=${params.recipientId} attempt=${params.attempt} delay_ms=${params.delayMs} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
84
+ }
85
+ export function isRetryableWechatSendError(error) {
86
+ if (isWechatContextTokenStaleError(error)) {
87
+ return false;
88
+ }
89
+ const classification = classifyWechatTransportError(error);
90
+ if (classification.retryable) {
91
+ return true;
92
+ }
93
+ const details = describeWechatTransportError(error);
94
+ return /^(?:Error|WechatApiResponseError): sendmessage failed:/i.test(details) &&
95
+ !/errcode=-14\b.*session timeout/i.test(details);
96
+ }
97
+ function computeWechatSendRetryDelayMs(attempt) {
98
+ return WECHAT_SEND_RETRY_BASE_MS * attempt;
99
+ }
100
+ export function shouldWatchParentProcess(options) {
101
+ return (options.startupParentPid > 1 &&
102
+ (options.attachedToTerminal || options.lifecycle === "companion_bound"));
103
+ }
104
+ function toPendingApproval(request) {
105
+ if (typeof request.code === "string") {
106
+ return request;
107
+ }
108
+ return {
109
+ ...request,
110
+ code: buildOneTimeCode(),
111
+ createdAt: nowIso(),
112
+ };
113
+ }
114
+ export function shouldDeferCodexInboundMessage(params) {
115
+ return (params.adapter === "codex" &&
116
+ !params.hasPendingConfirmation &&
117
+ !params.hasSystemCommand &&
118
+ params.activeTurnOrigin === "local" &&
119
+ (params.status === "busy" || params.status === "awaiting_approval"));
120
+ }
121
+ export function canDrainDeferredCodexInboundQueue(params) {
122
+ return (params.adapter === "codex" &&
123
+ params.deferredCount > 0 &&
124
+ !params.hasPendingConfirmation &&
125
+ !params.hasPendingApproval &&
126
+ !params.hasActiveTask &&
127
+ !params.activeTurnId &&
128
+ params.status !== "busy" &&
129
+ params.status !== "awaiting_approval");
130
+ }
131
+ export function formatDeferredCodexInboundQueueMessage(queuePosition) {
132
+ return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
133
+ }
134
+ export function isRetryableDeferredCodexDrainError(errorText) {
135
+ return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
136
+ }
137
+ export function parseCliArgs(argv) {
138
+ let adapter = null;
139
+ let commandOverride;
140
+ let cwd = process.cwd();
141
+ let profile;
142
+ let lifecycle = "persistent";
143
+ for (let i = 0; i < argv.length; i += 1) {
144
+ const arg = argv[i];
145
+ const next = argv[i + 1];
146
+ switch (arg) {
147
+ case "--adapter":
148
+ if (!next || !["codex", "claude", "opencode", "shell"].includes(next)) {
149
+ throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
150
+ }
151
+ adapter = next;
152
+ i += 1;
153
+ break;
154
+ case "--cmd":
155
+ if (!next) {
156
+ throw new Error("--cmd requires a value");
157
+ }
158
+ commandOverride = next;
159
+ i += 1;
160
+ break;
161
+ case "--cwd":
162
+ if (!next) {
163
+ throw new Error("--cwd requires a value");
164
+ }
165
+ cwd = path.resolve(next);
166
+ i += 1;
167
+ break;
168
+ case "--profile":
169
+ if (!next) {
170
+ throw new Error("--profile requires a value");
171
+ }
172
+ profile = next;
173
+ i += 1;
174
+ break;
175
+ case "--lifecycle":
176
+ if (!next || !["persistent", "companion_bound"].includes(next)) {
177
+ throw new Error(`Invalid lifecycle: ${next ?? "(missing)"}`);
178
+ }
179
+ lifecycle = next;
180
+ i += 1;
181
+ break;
182
+ case "--shutdown-on-parent-exit":
183
+ lifecycle = "companion_bound";
184
+ break;
185
+ case "--help":
186
+ case "-h":
187
+ printUsageAndExit();
188
+ break;
189
+ default:
190
+ throw new Error(`Unknown argument: ${arg}`);
191
+ }
192
+ }
193
+ if (!adapter) {
194
+ throw new Error("Missing required --adapter <codex|claude|opencode|shell>");
195
+ }
196
+ const defaultCommand = resolveDefaultAdapterCommand(adapter);
197
+ return {
198
+ adapter,
199
+ command: commandOverride ?? defaultCommand,
200
+ cwd,
201
+ profile,
202
+ lifecycle,
203
+ };
204
+ }
205
+ function printUsageAndExit() {
206
+ process.stdout.write([
207
+ "Usage: wechat-bridge --adapter <codex|claude|opencode|shell> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>]",
208
+ "",
209
+ "Examples:",
210
+ " wechat-bridge-codex",
211
+ " wechat-bridge-claude --cwd ~/work/my-project",
212
+ " wechat-bridge-opencode --cwd ~/work/my-project",
213
+ " wechat-bridge-shell --cmd pwsh # headless shell executor for non-interactive commands/scripts",
214
+ " wechat-bridge-shell --cmd bash # headless shell executor for non-interactive commands/scripts",
215
+ " wechat-bridge-codex --lifecycle companion_bound",
216
+ " bun run bridge:codex # repo-local development entrypoint",
217
+ " bun run bridge:opencode # repo-local development entrypoint",
218
+ "",
219
+ ].join("\n"));
220
+ process.exit(0);
221
+ }
222
+ async function main() {
223
+ const options = parseCliArgs(process.argv.slice(2));
224
+ const daemonEndpoint = readDaemonEndpoint();
225
+ if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
226
+ throw new Error(`wechat-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
227
+ }
228
+ if (daemonEndpoint) {
229
+ clearDaemonEndpoint(daemonEndpoint.pid);
230
+ log(`Cleared stale wechat-daemon endpoint for pid=${daemonEndpoint.pid}.`);
231
+ }
232
+ const credentials = await ensureWechatCredentials({
233
+ requireUserId: true,
234
+ validateExisting: true,
235
+ log,
236
+ });
237
+ if (!credentials.userId) {
238
+ throw new Error("Saved WeChat credentials are missing userId.");
239
+ }
240
+ const transport = new WeChatTransport({ log, logError });
241
+ // 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
242
+ setTimeout(async () => {
243
+ try {
244
+ const versionInfo = await checkForUpdate();
245
+ if (versionInfo?.hasUpdate) {
246
+ log(formatUpdateMessage(versionInfo));
247
+ }
248
+ }
249
+ catch (error) {
250
+ // 静默失败,不影响正常使用
251
+ }
252
+ }, 3000); // 延迟3秒,确保不影响启动
253
+ const stateStore = new BridgeStateStore({
254
+ ...options,
255
+ authorizedUserId: credentials.userId,
256
+ });
257
+ const reapedPeerPids = await reapPeerBridgeProcesses({
258
+ logger: (message) => stateStore.appendLog(message),
259
+ });
260
+ if (reapedPeerPids.length > 0) {
261
+ log(`Reaped ${reapedPeerPids.length} stale bridge process(es): ${reapedPeerPids.join(", ")}`);
262
+ }
263
+ if (options.adapter === "opencode") {
264
+ const reapedOpencodePids = await reapOrphanedOpencodeProcesses({
265
+ logger: (message) => stateStore.appendLog(message),
266
+ });
267
+ if (reapedOpencodePids.length > 0) {
268
+ log(`Reaped ${reapedOpencodePids.length} orphaned opencode process(es): ${reapedOpencodePids.join(", ")}`);
269
+ }
270
+ }
271
+ let lockRehydratedLogged = false;
272
+ const ensureRuntimeOwnership = () => {
273
+ const ownership = stateStore.verifyRuntimeOwnership();
274
+ if (!ownership.ok) {
275
+ if (ownership.reason === "superseded") {
276
+ requestShutdown(`Bridge instance ${stateStore.getState().instanceId} was superseded by ${ownership.activeInstanceId}. Stopping duplicate bridge.`);
277
+ return false;
278
+ }
279
+ requestShutdown(`Bridge instance ${stateStore.getState().instanceId} lost the global lock to pid=${ownership.activePid} (${ownership.activeInstanceId}). Stopping duplicate bridge.`);
280
+ return false;
281
+ }
282
+ if (ownership.rehydratedLock && !lockRehydratedLogged) {
283
+ lockRehydratedLogged = true;
284
+ stateStore.appendLog(`lock_rehydrated: pid=${process.pid} instanceId=${stateStore.getState().instanceId} adapter=${options.adapter} cwd=${options.cwd}`);
285
+ }
286
+ return true;
287
+ };
288
+ // Clear any stale endpoint left by a previous bridge for this workspace.
289
+ // This prevents `wechat-*` companions from reconnecting to a dead bridge
290
+ // while the new runtime is still starting up.
291
+ const adapter = createRuntimeHost({
292
+ kind: options.adapter,
293
+ command: options.command,
294
+ cwd: options.cwd,
295
+ profile: options.profile,
296
+ lifecycle: options.lifecycle,
297
+ initialSharedSessionId: stateStore.getState().sharedSessionId ?? stateStore.getState().sharedThreadId,
298
+ initialResumeConversationId: stateStore.getState().resumeConversationId,
299
+ initialTranscriptPath: stateStore.getState().transcriptPath,
300
+ });
301
+ const controller = new BridgeController(adapter, options.cwd);
302
+ controller.clearLocalClientEndpoint();
303
+ stateStore.appendLog(`Cleared stale companion endpoint for ${options.cwd} before adapter start.`);
304
+ let textSendChain = Promise.resolve();
305
+ let attachmentSendChain = Promise.resolve();
306
+ const pendingWechatForwardTasks = new Set();
307
+ let activeTask = null;
308
+ const deferredInboundMessages = [];
309
+ let drainingDeferredInboundMessages = false;
310
+ let lastOutputAt = 0;
311
+ let lastHeartbeatAt = 0;
312
+ let consecutivePollFailures = 0;
313
+ const queueWechatTextAction = (action) => {
314
+ const run = textSendChain.then(action);
315
+ textSendChain = run.then(() => undefined, () => undefined);
316
+ return run;
317
+ };
318
+ const queueWechatAttachmentAction = (action) => {
319
+ const run = attachmentSendChain.then(action);
320
+ attachmentSendChain = run.then(() => undefined, () => undefined);
321
+ return run;
322
+ };
323
+ const queueWechatMessage = (senderId, text, context = "message") => {
324
+ return queueWechatTextAction(async () => {
325
+ for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
326
+ try {
327
+ await transport.sendText(senderId, text);
328
+ return true;
329
+ }
330
+ catch (err) {
331
+ if (isWechatContextTokenStaleError(err)) {
332
+ transport.clearCachedContextToken(senderId);
333
+ const hint = "WeChat conversation context is stale. Ask the WeChat owner to send any message first, then local terminal replies can sync back to WeChat.";
334
+ logError(`Failed to send WeChat ${context}: ${hint}`);
335
+ stateStore.appendLog(formatWechatContextTokenStaleLogEntry({
336
+ context,
337
+ recipientId: senderId,
338
+ error: err,
339
+ }));
340
+ return false;
341
+ }
342
+ if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
343
+ const delayMs = computeWechatSendRetryDelayMs(attempt);
344
+ logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
345
+ stateStore.appendLog(formatWechatSendRetryLogEntry({
346
+ context,
347
+ recipientId: senderId,
348
+ attempt,
349
+ delayMs,
350
+ error: err,
351
+ }));
352
+ await delay(delayMs);
353
+ continue;
354
+ }
355
+ logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
356
+ stateStore.appendLog(formatWechatSendFailureLogEntry({
357
+ context,
358
+ recipientId: senderId,
359
+ error: err,
360
+ }));
361
+ return false;
362
+ }
363
+ }
364
+ return false;
365
+ });
366
+ };
367
+ const trackWechatForwardTask = (task) => {
368
+ const tracked = task
369
+ .catch((error) => {
370
+ logError(`WeChat forward task failed: ${describeWechatTransportError(error)}`);
371
+ stateStore.appendLog(`wechat_forward_failed: error=${truncatePreview(describeWechatTransportError(error), 400)}`);
372
+ })
373
+ .finally(() => {
374
+ pendingWechatForwardTasks.delete(tracked);
375
+ });
376
+ pendingWechatForwardTasks.add(tracked);
377
+ };
378
+ const waitForPendingWechatForwardTasks = async () => {
379
+ while (pendingWechatForwardTasks.size > 0) {
380
+ await Promise.allSettled([...pendingWechatForwardTasks]);
381
+ }
382
+ };
383
+ const outputBatcher = new OutputBatcher(async (text) => {
384
+ await queueWechatMessage(stateStore.getState().authorizedUserId, text);
385
+ });
386
+ const maybeDrainDeferredInboundMessages = async () => {
387
+ if (drainingDeferredInboundMessages || !ensureRuntimeOwnership()) {
388
+ return;
389
+ }
390
+ const adapterState = adapter.getState();
391
+ if (!canDrainDeferredCodexInboundQueue({
392
+ adapter: options.adapter,
393
+ deferredCount: deferredInboundMessages.length,
394
+ status: adapterState.status,
395
+ activeTurnId: adapterState.activeTurnId,
396
+ hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
397
+ hasPendingApproval: Boolean(adapterState.pendingApproval),
398
+ hasActiveTask: Boolean(activeTask),
399
+ })) {
400
+ return;
401
+ }
402
+ const nextDeferred = deferredInboundMessages.shift();
403
+ if (!nextDeferred) {
404
+ return;
405
+ }
406
+ drainingDeferredInboundMessages = true;
407
+ try {
408
+ stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
409
+ const nextTask = await dispatchInboundWechatText({
410
+ message: nextDeferred.message,
411
+ options,
412
+ stateStore,
413
+ adapter,
414
+ });
415
+ activeTask = nextTask;
416
+ lastHeartbeatAt = 0;
417
+ }
418
+ catch (err) {
419
+ const errorText = err instanceof Error ? err.message : String(err);
420
+ if (isRetryableDeferredCodexDrainError(errorText)) {
421
+ deferredInboundMessages.unshift(nextDeferred);
422
+ stateStore.appendLog(`deferred_inbound_blocked: ${truncatePreview(errorText, 400)}`);
423
+ return;
424
+ }
425
+ logError(errorText);
426
+ stateStore.appendLog(`deferred_inbound_error: ${errorText}`);
427
+ await queueWechatMessage(nextDeferred.message.senderId, formatUserFacingInboundError({
428
+ adapter: options.adapter,
429
+ cwd: options.cwd,
430
+ errorText,
431
+ isUserFacingShellRejection: false,
432
+ }), "inbound_error");
433
+ }
434
+ finally {
435
+ drainingDeferredInboundMessages = false;
436
+ }
437
+ };
438
+ const startupParentPid = process.ppid;
439
+ const attachedToTerminal = Boolean(process.stdin.isTTY || process.stdout.isTTY || process.stderr.isTTY);
440
+ let shutdownPromise = null;
441
+ let requestedExitCode = 0;
442
+ let stdinDetached = false;
443
+ const parentWatchTimer = shouldWatchParentProcess({
444
+ startupParentPid,
445
+ attachedToTerminal,
446
+ lifecycle: options.lifecycle,
447
+ })
448
+ ? setInterval(() => {
449
+ if (shutdownPromise || isPidAlive(startupParentPid)) {
450
+ return;
451
+ }
452
+ log(`Parent process ${startupParentPid} exited. Stopping bridge.`);
453
+ void shutdown(0);
454
+ }, PARENT_PROCESS_POLL_MS)
455
+ : null;
456
+ parentWatchTimer?.unref();
457
+ const cleanup = async () => {
458
+ if (parentWatchTimer) {
459
+ clearInterval(parentWatchTimer);
460
+ }
461
+ try {
462
+ await outputBatcher.flushNow();
463
+ await waitForPendingWechatForwardTasks();
464
+ }
465
+ catch {
466
+ // Best effort flush.
467
+ }
468
+ try {
469
+ await textSendChain;
470
+ await attachmentSendChain;
471
+ await waitForPendingWechatForwardTasks();
472
+ }
473
+ catch {
474
+ // Best effort flush.
475
+ }
476
+ try {
477
+ await adapter.dispose();
478
+ }
479
+ catch {
480
+ // Best effort shutdown.
481
+ }
482
+ controller.clearLocalClientEndpoint();
483
+ stateStore.releaseLock();
484
+ };
485
+ const shutdown = async (exitCode = 0) => {
486
+ requestedExitCode = exitCode;
487
+ if (!shutdownPromise) {
488
+ shutdownPromise = cleanup().catch((error) => {
489
+ logError(`Shutdown cleanup failed: ${describeWechatTransportError(error)}`);
490
+ });
491
+ }
492
+ await shutdownPromise;
493
+ };
494
+ const requestShutdown = (message, exitCode = 0) => {
495
+ if (shutdownPromise) {
496
+ return;
497
+ }
498
+ log(message);
499
+ void shutdown(exitCode).finally(() => process.exit(requestedExitCode));
500
+ };
501
+ process.once("SIGINT", () => {
502
+ requestShutdown("Received SIGINT. Stopping bridge.");
503
+ });
504
+ process.once("SIGTERM", () => {
505
+ requestShutdown("Received SIGTERM. Stopping bridge.");
506
+ });
507
+ process.once("SIGHUP", () => {
508
+ requestShutdown("Terminal session closed. Stopping bridge.");
509
+ });
510
+ if (process.platform === "win32") {
511
+ process.once("SIGBREAK", () => {
512
+ requestShutdown("Received SIGBREAK. Stopping bridge.");
513
+ });
514
+ }
515
+ if (attachedToTerminal) {
516
+ process.stdin.on("close", () => {
517
+ if (stdinDetached) {
518
+ return;
519
+ }
520
+ stdinDetached = true;
521
+ requestShutdown("Standard input closed. Stopping bridge.");
522
+ });
523
+ process.stdin.on("end", () => {
524
+ if (stdinDetached) {
525
+ return;
526
+ }
527
+ stdinDetached = true;
528
+ requestShutdown("Standard input ended. Stopping bridge.");
529
+ });
530
+ }
531
+ process.on("exit", () => {
532
+ if (parentWatchTimer) {
533
+ clearInterval(parentWatchTimer);
534
+ }
535
+ stateStore.releaseLock();
536
+ });
537
+ try {
538
+ wireAdapterEvents({
539
+ adapter,
540
+ options,
541
+ transport,
542
+ stateStore,
543
+ outputBatcher,
544
+ queueWechatAttachmentAction,
545
+ queueWechatMessage,
546
+ trackWechatForwardTask,
547
+ maybeDrainDeferredInboundMessages,
548
+ getActiveTask: () => activeTask,
549
+ clearActiveTask: () => {
550
+ activeTask = null;
551
+ lastHeartbeatAt = 0;
552
+ },
553
+ updateLastOutputAt: () => {
554
+ lastOutputAt = Date.now();
555
+ },
556
+ syncSharedSessionState: () => {
557
+ syncSharedSessionState(stateStore, adapter);
558
+ },
559
+ syncLocalClientEndpoint: () => {
560
+ controller.syncLocalClientEndpoint();
561
+ },
562
+ requestShutdown,
563
+ });
564
+ await adapter.start();
565
+ if (!ensureRuntimeOwnership()) {
566
+ return;
567
+ }
568
+ syncSharedSessionState(stateStore, adapter);
569
+ controller.syncLocalClientEndpoint();
570
+ stateStore.appendLog(`Bridge started with adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
571
+ log(`WeChat bridge is ready for adapter "${options.adapter}".`);
572
+ log(`Working directory: ${options.cwd}`);
573
+ if (options.profile) {
574
+ log(`Profile: ${options.profile}`);
575
+ }
576
+ log(`Authorized WeChat user: ${credentials.userId}`);
577
+ if (options.adapter === "codex") {
578
+ log('Start the visible Codex panel in a second terminal with: wechat-codex');
579
+ }
580
+ else if (options.adapter === "opencode") {
581
+ log('Start the visible OpenCode companion in a second terminal with: wechat-opencode');
582
+ }
583
+ else if (options.adapter === "claude") {
584
+ log('Start the visible Claude companion in a second terminal with: wechat-claude');
585
+ }
586
+ else if (options.adapter === "shell") {
587
+ log("Shell mode runs as a headless remote executor for non-interactive commands and scripts.");
588
+ }
589
+ while (true) {
590
+ if (!ensureRuntimeOwnership()) {
591
+ break;
592
+ }
593
+ let pollResult;
594
+ try {
595
+ pollResult = await transport.pollMessages({
596
+ timeoutMs: DEFAULT_LONG_POLL_TIMEOUT_MS,
597
+ minCreatedAtMs: stateStore.getState().bridgeStartedAtMs - MESSAGE_START_GRACE_MS,
598
+ });
599
+ }
600
+ catch (err) {
601
+ const classification = classifyWechatTransportError(err);
602
+ if (!classification.retryable) {
603
+ throw err;
604
+ }
605
+ consecutivePollFailures += 1;
606
+ const delayMs = computePollRetryDelayMs(consecutivePollFailures);
607
+ const errorText = describeWechatTransportError(err);
608
+ const statusDetails = typeof classification.statusCode === "number"
609
+ ? ` status=${classification.statusCode}`
610
+ : "";
611
+ logError(`WeChat long poll failed (${classification.kind}${statusDetails}, attempt ${consecutivePollFailures}). Retrying in ${formatDuration(delayMs)}. ${errorText}`);
612
+ stateStore.appendLog(`poll_retry: kind=${classification.kind}${statusDetails} attempt=${consecutivePollFailures} delay_ms=${delayMs} error=${truncatePreview(errorText, 400)}`);
613
+ await delay(delayMs);
614
+ continue;
615
+ }
616
+ if (!ensureRuntimeOwnership()) {
617
+ break;
618
+ }
619
+ if (consecutivePollFailures > 0) {
620
+ const recoveredFailures = consecutivePollFailures;
621
+ consecutivePollFailures = 0;
622
+ log(`WeChat long poll recovered after ${recoveredFailures} transient error(s).`);
623
+ stateStore.appendLog(`poll_recovered: failures=${recoveredFailures}`);
624
+ }
625
+ if (pollResult.ignoredBacklogCount > 0) {
626
+ stateStore.incrementIgnoredBacklog(pollResult.ignoredBacklogCount);
627
+ stateStore.appendLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
628
+ }
629
+ for (const message of pollResult.messages) {
630
+ if (!ensureRuntimeOwnership()) {
631
+ break;
632
+ }
633
+ stateStore.touchActivity(message.createdAt);
634
+ let nextTask = null;
635
+ try {
636
+ nextTask = await handleInboundMessage({
637
+ message,
638
+ options,
639
+ stateStore,
640
+ adapter,
641
+ queueWechatMessage,
642
+ outputBatcher,
643
+ deferInboundMessage: async (nextMessage) => {
644
+ deferredInboundMessages.push({
645
+ message: nextMessage,
646
+ });
647
+ stateStore.appendLog(`deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`);
648
+ await queueWechatMessage(nextMessage.senderId, formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length));
649
+ },
650
+ });
651
+ }
652
+ catch (err) {
653
+ const errorText = err instanceof Error ? err.message : String(err);
654
+ const isUserFacingShellRejection = err instanceof Error && err.name === "ShellCommandRejectedError";
655
+ logError(errorText);
656
+ stateStore.appendLog(`${isUserFacingShellRejection ? "inbound_rejected" : "inbound_error"}: ${errorText}`);
657
+ await queueWechatMessage(message.senderId, formatUserFacingInboundError({
658
+ adapter: options.adapter,
659
+ cwd: options.cwd,
660
+ errorText,
661
+ isUserFacingShellRejection,
662
+ }), "inbound_error");
663
+ }
664
+ if (nextTask) {
665
+ activeTask = nextTask;
666
+ lastHeartbeatAt = 0;
667
+ }
668
+ syncSharedSessionState(stateStore, adapter);
669
+ await maybeDrainDeferredInboundMessages();
670
+ }
671
+ const adapterState = adapter.getState();
672
+ const lastSignalAt = Math.max(lastHeartbeatAt, lastOutputAt || activeTask?.startedAt || 0);
673
+ if (activeTask &&
674
+ options.adapter === "shell" &&
675
+ adapterState.status === "busy" &&
676
+ Date.now() - lastSignalAt >= 30_000) {
677
+ lastHeartbeatAt = Date.now();
678
+ await queueWechatMessage(stateStore.getState().authorizedUserId, `${options.adapter} is still running. Waiting for more output...`);
679
+ }
680
+ }
681
+ }
682
+ finally {
683
+ await shutdown(requestedExitCode);
684
+ }
685
+ }
686
+ function syncSharedSessionState(stateStore, adapter) {
687
+ const persistedState = stateStore.getState();
688
+ const persistedSessionId = persistedState.sharedSessionId ?? persistedState.sharedThreadId;
689
+ const adapterState = adapter.getState();
690
+ const adapterSessionId = adapterState.sharedSessionId ?? adapterState.sharedThreadId;
691
+ if (adapterSessionId && adapterSessionId !== persistedSessionId) {
692
+ stateStore.setSharedSessionId(adapterSessionId);
693
+ }
694
+ else if (!adapterSessionId && persistedSessionId) {
695
+ stateStore.clearSharedSessionId();
696
+ }
697
+ if (persistedState.adapter !== "claude") {
698
+ return;
699
+ }
700
+ if (adapterState.resumeConversationId !== persistedState.resumeConversationId ||
701
+ adapterState.transcriptPath !== persistedState.transcriptPath) {
702
+ if (adapterState.resumeConversationId || adapterState.transcriptPath) {
703
+ stateStore.setClaudeResumeState(adapterState.resumeConversationId, adapterState.transcriptPath);
704
+ }
705
+ else {
706
+ stateStore.clearClaudeResumeState();
707
+ }
708
+ }
709
+ }
710
+ function wireAdapterEvents(params) {
711
+ const { adapter, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, updateLastOutputAt, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
712
+ adapter.setEventSink((event) => {
713
+ syncSharedSessionState();
714
+ syncLocalClientEndpoint();
715
+ const adapterState = adapter.getState();
716
+ const bridgeState = stateStore.getState();
717
+ if (bridgeState.pendingConfirmation && !adapterState.pendingApproval) {
718
+ stateStore.clearPendingConfirmation();
719
+ }
720
+ const authorizedUserId = stateStore.getState().authorizedUserId;
721
+ switch (event.type) {
722
+ case "stdout":
723
+ case "stderr":
724
+ updateLastOutputAt();
725
+ if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
726
+ outputBatcher.push(event.text);
727
+ }
728
+ break;
729
+ case "final_reply":
730
+ stateStore.appendLog(`final_reply: ${truncatePreview(event.text)}`);
731
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
732
+ await forwardWechatFinalReply({
733
+ adapter: options.adapter,
734
+ rawText: event.text,
735
+ onEmptyVisibleReply: ({ rawVisibleText }) => {
736
+ stateStore.appendLog(`empty_visible_final_reply: adapter=${options.adapter} raw=${truncatePreview(rawVisibleText)}`);
737
+ },
738
+ sender: {
739
+ sendText: async (text) => {
740
+ const sent = await queueWechatMessage(authorizedUserId, text, "final_reply");
741
+ if (sent) {
742
+ stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
743
+ }
744
+ return sent;
745
+ },
746
+ sendImage: (imagePath) => queueWechatAttachmentAction(() => transport.sendImage(imagePath, { recipientId: authorizedUserId })),
747
+ sendFile: (filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId: authorizedUserId })),
748
+ sendVoice: (voicePath) => queueWechatAttachmentAction(() => transport.sendVoice(voicePath, authorizedUserId)),
749
+ sendVideo: (videoPath) => queueWechatAttachmentAction(() => transport.sendVideo(videoPath, { recipientId: authorizedUserId })),
750
+ },
751
+ });
752
+ }));
753
+ break;
754
+ case "status":
755
+ if (event.message) {
756
+ log(`${event.status}: ${event.message}`);
757
+ stateStore.appendLog(`${event.status}: ${event.message}`);
758
+ }
759
+ void maybeDrainDeferredInboundMessages();
760
+ break;
761
+ case "notice":
762
+ updateLastOutputAt();
763
+ stateStore.appendLog(`${event.level}_notice: ${truncatePreview(event.text)}`);
764
+ if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
765
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
766
+ await queueWechatMessage(authorizedUserId, event.text, "notice");
767
+ }));
768
+ }
769
+ break;
770
+ case "approval_required":
771
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
772
+ const pending = toPendingApproval(event.request);
773
+ stateStore.setPendingConfirmation(pending);
774
+ stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
775
+ await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
776
+ }));
777
+ break;
778
+ case "mirrored_user_input":
779
+ stateStore.appendLog(`mirrored_local_input: ${truncatePreview(event.text)}`);
780
+ if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
781
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
782
+ await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, event.text), "mirrored_user_input");
783
+ }));
784
+ }
785
+ break;
786
+ case "session_switched":
787
+ stateStore.appendLog(`session_switched: ${event.sessionId} source=${event.source} reason=${event.reason}`);
788
+ if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
789
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
790
+ await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
791
+ adapter: options.adapter,
792
+ sessionId: event.sessionId,
793
+ source: event.source,
794
+ reason: event.reason,
795
+ }), "session_switched");
796
+ }));
797
+ }
798
+ break;
799
+ case "thread_switched":
800
+ stateStore.appendLog(`thread_switched: ${event.threadId} source=${event.source} reason=${event.reason}`);
801
+ if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
802
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
803
+ await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
804
+ adapter: options.adapter,
805
+ sessionId: event.threadId,
806
+ source: event.source,
807
+ reason: event.reason,
808
+ }), "thread_switched");
809
+ }));
810
+ }
811
+ void maybeDrainDeferredInboundMessages();
812
+ break;
813
+ case "task_complete":
814
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
815
+ stateStore.clearPendingConfirmation();
816
+ if (options.adapter === "shell") {
817
+ const summary = buildCompletionSummary({
818
+ adapter: options.adapter,
819
+ activeTask: getActiveTask(),
820
+ exitCode: event.exitCode,
821
+ recentOutput: outputBatcher.getRecentSummary(),
822
+ });
823
+ await queueWechatMessage(authorizedUserId, summary);
824
+ }
825
+ clearActiveTask();
826
+ await maybeDrainDeferredInboundMessages();
827
+ }));
828
+ break;
829
+ case "task_failed":
830
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
831
+ stateStore.clearPendingConfirmation();
832
+ clearActiveTask();
833
+ await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, event.message), "task_failed");
834
+ await maybeDrainDeferredInboundMessages();
835
+ }));
836
+ break;
837
+ case "fatal_error":
838
+ logError(event.message);
839
+ stateStore.appendLog(`fatal_error: ${event.message}`);
840
+ stateStore.clearPendingConfirmation();
841
+ clearActiveTask();
842
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
843
+ await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(event.message), "fatal_error");
844
+ await maybeDrainDeferredInboundMessages();
845
+ }));
846
+ break;
847
+ case "shutdown_requested":
848
+ stateStore.appendLog(`shutdown_requested: ${event.reason}`);
849
+ requestShutdown(event.message, event.exitCode ?? 0);
850
+ break;
851
+ }
852
+ });
853
+ }
854
+ function buildCompletionSummary(params) {
855
+ const lines = [`${params.adapter} task complete.`];
856
+ if (params.activeTask) {
857
+ lines.push(`duration: ${formatDuration(Date.now() - params.activeTask.startedAt)}`);
858
+ lines.push(`input: ${params.activeTask.inputPreview}`);
859
+ }
860
+ if (typeof params.exitCode === "number") {
861
+ lines.push(`exit_code: ${params.exitCode}`);
862
+ }
863
+ lines.push(`recent_output:\n${params.recentOutput}`);
864
+ return lines.join("\n");
865
+ }
866
+ function formatInboundMessagePreview(message) {
867
+ if (message.text.trim()) {
868
+ return message.text;
869
+ }
870
+ if (message.attachments.length > 0) {
871
+ return message.attachments
872
+ .map((attachment) => `${attachment.kind}: ${attachment.path}`)
873
+ .join("\n");
874
+ }
875
+ return "(empty)";
876
+ }
877
+ async function handleInboundMessage(params) {
878
+ const { message, options, stateStore, adapter, queueWechatMessage, outputBatcher, deferInboundMessage, } = params;
879
+ const state = stateStore.getState();
880
+ const systemCommand = parseWechatControlCommand(message.text, {
881
+ adapter: options.adapter,
882
+ hasPendingConfirmation: Boolean(state.pendingConfirmation),
883
+ hasPendingUserInput: Boolean(state.pendingUserInput),
884
+ });
885
+ if (message.senderId !== state.authorizedUserId) {
886
+ await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
887
+ return null;
888
+ }
889
+ switch (systemCommand?.type) {
890
+ case "status":
891
+ await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
892
+ return null;
893
+ case "resume": {
894
+ if (options.adapter === "codex") {
895
+ await queueWechatMessage(message.senderId, 'WeChat /resume is disabled in codex mode. Use /resume directly inside "wechat-codex"; WeChat will follow the active local thread.');
896
+ return null;
897
+ }
898
+ if (options.adapter === "claude") {
899
+ await queueWechatMessage(message.senderId, 'WeChat /resume is disabled in claude mode. Use /resume directly inside "wechat-claude"; WeChat will follow the active local session.');
900
+ return null;
901
+ }
902
+ if (options.adapter === "opencode") {
903
+ await queueWechatMessage(message.senderId, 'WeChat /resume is disabled in opencode mode. Use /resume directly inside "wechat-opencode"; WeChat will follow the active local session.');
904
+ return null;
905
+ }
906
+ await queueWechatMessage(message.senderId, `/resume is not available in ${options.adapter} mode.`);
907
+ return null;
908
+ }
909
+ case "new_session": {
910
+ if (!adapter.createSession) {
911
+ await queueWechatMessage(message.senderId, `/new is not available in ${options.adapter} mode.`);
912
+ return null;
913
+ }
914
+ await outputBatcher.flushNow();
915
+ outputBatcher.clear();
916
+ stateStore.clearPendingConfirmation();
917
+ stateStore.clearSharedSessionId();
918
+ await adapter.createSession();
919
+ stateStore.appendLog(`New ${options.adapter} session requested by owner.`);
920
+ return null;
921
+ }
922
+ case "stop": {
923
+ const interrupted = await adapter.interrupt();
924
+ await queueWechatMessage(message.senderId, interrupted
925
+ ? "Interrupt signal sent to the active worker."
926
+ : "No running worker was available to interrupt.");
927
+ return null;
928
+ }
929
+ case "reset":
930
+ await outputBatcher.flushNow();
931
+ outputBatcher.clear();
932
+ stateStore.clearPendingConfirmation();
933
+ stateStore.clearSharedSessionId();
934
+ await adapter.reset();
935
+ stateStore.appendLog("Worker reset by owner.");
936
+ await queueWechatMessage(message.senderId, "Worker session has been reset.");
937
+ return null;
938
+ case "confirm": {
939
+ const pending = state.pendingConfirmation;
940
+ if (!pending) {
941
+ await queueWechatMessage(message.senderId, "No pending approval request.");
942
+ return null;
943
+ }
944
+ if (options.adapter !== "claude" && pending.code !== systemCommand.code) {
945
+ await queueWechatMessage(message.senderId, "Confirmation code did not match.");
946
+ return null;
947
+ }
948
+ const confirmed = await adapter.resolveApproval("confirm");
949
+ if (!confirmed) {
950
+ await queueWechatMessage(message.senderId, "The worker could not apply this approval request.");
951
+ return null;
952
+ }
953
+ stateStore.clearPendingConfirmation();
954
+ stateStore.appendLog(`Approval confirmed: ${pending.commandPreview}`);
955
+ await queueWechatMessage(message.senderId, "Approval confirmed. Continuing...");
956
+ return {
957
+ startedAt: Date.now(),
958
+ inputPreview: pending.commandPreview,
959
+ };
960
+ }
961
+ case "deny": {
962
+ const pending = state.pendingConfirmation;
963
+ if (!pending) {
964
+ await queueWechatMessage(message.senderId, "No pending approval request.");
965
+ return null;
966
+ }
967
+ const denied = await adapter.resolveApproval("deny");
968
+ if (!denied) {
969
+ await queueWechatMessage(message.senderId, "The worker could not deny this approval request cleanly.");
970
+ return null;
971
+ }
972
+ stateStore.clearPendingConfirmation();
973
+ stateStore.appendLog(`Approval denied: ${pending.commandPreview}`);
974
+ await queueWechatMessage(message.senderId, "Approval denied.");
975
+ return null;
976
+ }
977
+ }
978
+ if (state.pendingConfirmation) {
979
+ await queueWechatMessage(message.senderId, formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()));
980
+ return null;
981
+ }
982
+ const adapterState = adapter.getState();
983
+ if (shouldDeferCodexInboundMessage({
984
+ adapter: options.adapter,
985
+ status: adapterState.status,
986
+ activeTurnOrigin: adapterState.activeTurnOrigin,
987
+ hasPendingConfirmation: Boolean(state.pendingConfirmation),
988
+ hasSystemCommand: Boolean(systemCommand),
989
+ })) {
990
+ await deferInboundMessage(message);
991
+ return null;
992
+ }
993
+ if (adapterState.status === "busy") {
994
+ if ((options.adapter === "codex" || options.adapter === "opencode") &&
995
+ adapterState.activeTurnOrigin === "local") {
996
+ await queueWechatMessage(message.senderId, `${options.adapter === "opencode" ? "OpenCode" : "codex"} is currently busy with a local terminal turn. Wait for it to finish or use /stop.`);
997
+ return null;
998
+ }
999
+ await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
1000
+ return null;
1001
+ }
1002
+ return dispatchInboundWechatText({
1003
+ message,
1004
+ options,
1005
+ stateStore,
1006
+ adapter,
1007
+ });
1008
+ }
1009
+ async function dispatchInboundWechatText(params) {
1010
+ const { message, options, stateStore, adapter } = params;
1011
+ const preview = formatInboundMessagePreview(message);
1012
+ const activeTask = {
1013
+ startedAt: Date.now(),
1014
+ inputPreview: truncatePreview(preview, 180),
1015
+ };
1016
+ stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
1017
+ await adapter.sendInput(buildWechatInboundPrompt(message.text, message.attachments));
1018
+ return activeTask;
1019
+ }
1020
+ const isDirectRun = Boolean(import.meta.main);
1021
+ if (isDirectRun) {
1022
+ main().catch((err) => {
1023
+ logError(describeWechatTransportError(err));
1024
+ process.exit(1);
1025
+ });
1026
+ }