@unlinearity/cli-wechat-bridge 1.0.3 → 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.
- package/README.md +325 -221
- package/bin/wechat-daemon.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +95 -0
- package/dist/bridge/bridge-adapters.claude.js +1 -1
- package/dist/bridge/bridge-adapters.codex.js +18 -3
- package/dist/bridge/bridge-adapters.core.js +51 -7
- package/dist/bridge/bridge-adapters.opencode.js +220 -22
- package/dist/bridge/bridge-adapters.shared.js +4 -97
- package/dist/bridge/bridge-controller.js +6 -2
- package/dist/bridge/bridge-final-reply.js +18 -3
- package/dist/bridge/bridge-state.js +9 -3
- package/dist/bridge/bridge-utils.js +81 -14
- package/dist/bridge/claude-hooks.js +2 -2
- package/dist/bridge/wechat-bridge.js +139 -31
- package/dist/companion/codex-remote-client.js +11 -4
- package/dist/companion/local-companion-link.js +56 -26
- package/dist/companion/local-companion-start.js +54 -6
- package/dist/companion/local-companion.js +6 -1
- package/dist/daemon/daemon-link.js +175 -0
- package/dist/daemon/wechat-daemon.js +1202 -0
- package/dist/utils/version-checker.js +1 -1
- package/dist/wechat/channel-config.js +126 -33
- package/dist/wechat/standalone-bot.js +5 -6
- package/dist/wechat/wechat-channel.js +21 -10
- package/dist/wechat/wechat-transport.js +336 -10
- package/package.json +12 -2
|
@@ -101,6 +101,7 @@ export function stripAnsi(text) {
|
|
|
101
101
|
}
|
|
102
102
|
export function normalizeOutput(text) {
|
|
103
103
|
return stripAnsi(text)
|
|
104
|
+
// eslint-disable-next-line no-control-regex
|
|
104
105
|
.replace(/\u0000/g, "")
|
|
105
106
|
.replace(/\r\n/g, "\n")
|
|
106
107
|
.replace(/\r/g, "\n");
|
|
@@ -132,6 +133,9 @@ export function parseSystemCommand(text) {
|
|
|
132
133
|
return null;
|
|
133
134
|
}
|
|
134
135
|
const [rawCommand, ...rest] = trimmed.split(/\s+/);
|
|
136
|
+
if (!rawCommand) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
135
139
|
const command = rawCommand.toLowerCase();
|
|
136
140
|
const argument = rest.join(" ").trim();
|
|
137
141
|
switch (command) {
|
|
@@ -207,15 +211,51 @@ export function shouldInjectWechatAttachmentPrompt(text) {
|
|
|
207
211
|
mentionsLocalPath ||
|
|
208
212
|
looksLikeShortSendCommand);
|
|
209
213
|
}
|
|
210
|
-
|
|
211
|
-
if (!
|
|
212
|
-
return
|
|
214
|
+
function formatPromptByteSize(bytes) {
|
|
215
|
+
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {
|
|
216
|
+
return null;
|
|
213
217
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
if (bytes >= 1024 * 1024) {
|
|
219
|
+
return `${(bytes / (1024 * 1024)).toFixed(bytes >= 100 * 1024 * 1024 ? 0 : 1)} MB`;
|
|
220
|
+
}
|
|
221
|
+
if (bytes >= 1024) {
|
|
222
|
+
return `${(bytes / 1024).toFixed(bytes >= 100 * 1024 ? 0 : 1)} KB`;
|
|
217
223
|
}
|
|
218
|
-
return `${
|
|
224
|
+
return `${bytes} B`;
|
|
225
|
+
}
|
|
226
|
+
function formatWechatInboundAttachmentPrompt(attachments) {
|
|
227
|
+
const lines = [
|
|
228
|
+
"[WeChat inbound attachments]",
|
|
229
|
+
"The user sent attachment files through WeChat. They have been saved locally. Read the local paths directly if the user's request requires inspecting them.",
|
|
230
|
+
];
|
|
231
|
+
attachments.forEach((attachment, index) => {
|
|
232
|
+
const sizeText = formatPromptByteSize(attachment.sizeBytes);
|
|
233
|
+
const metadata = [
|
|
234
|
+
`kind=${attachment.kind}`,
|
|
235
|
+
attachment.fileName ? `name=${attachment.fileName}` : "",
|
|
236
|
+
sizeText ? `size=${sizeText}` : "",
|
|
237
|
+
].filter(Boolean);
|
|
238
|
+
lines.push(`${index + 1}. ${metadata.join(" ")} path=${attachment.path}`);
|
|
239
|
+
});
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
export function buildWechatInboundPrompt(text, attachments = []) {
|
|
243
|
+
const trimmedAttachments = attachments.filter((attachment) => attachment.path.trim());
|
|
244
|
+
if (!trimmedAttachments.length) {
|
|
245
|
+
if (!shouldInjectWechatAttachmentPrompt(text)) {
|
|
246
|
+
return text;
|
|
247
|
+
}
|
|
248
|
+
const normalized = normalizeOutput(text).trim();
|
|
249
|
+
if (!normalized) {
|
|
250
|
+
return text;
|
|
251
|
+
}
|
|
252
|
+
return `${WECHAT_ATTACHMENT_PROMPT_PREFIX}\n${normalized}`;
|
|
253
|
+
}
|
|
254
|
+
const baseText = normalizeOutput(text).trim() || "Received WeChat attachment(s).";
|
|
255
|
+
const userPrompt = shouldInjectWechatAttachmentPrompt(baseText)
|
|
256
|
+
? `${WECHAT_ATTACHMENT_PROMPT_PREFIX}\n${baseText.trim()}`
|
|
257
|
+
: baseText;
|
|
258
|
+
return `${userPrompt.trim()}\n\n${formatWechatInboundAttachmentPrompt(trimmedAttachments)}`;
|
|
219
259
|
}
|
|
220
260
|
export function parseWechatFinalReply(text) {
|
|
221
261
|
const normalized = normalizeOutput(text);
|
|
@@ -227,7 +267,11 @@ export function parseWechatFinalReply(text) {
|
|
|
227
267
|
return extractInlineWechatAttachments(normalized);
|
|
228
268
|
}
|
|
229
269
|
const attachments = [];
|
|
230
|
-
const
|
|
270
|
+
const attachmentBlock = match[1];
|
|
271
|
+
if (attachmentBlock === undefined) {
|
|
272
|
+
return extractInlineWechatAttachments(normalized);
|
|
273
|
+
}
|
|
274
|
+
const lines = attachmentBlock
|
|
231
275
|
.split("\n")
|
|
232
276
|
.map((line) => line.trim())
|
|
233
277
|
.filter(Boolean);
|
|
@@ -240,7 +284,11 @@ export function parseWechatFinalReply(text) {
|
|
|
240
284
|
return extractInlineWechatAttachments(normalized);
|
|
241
285
|
}
|
|
242
286
|
const kind = parsed[1];
|
|
243
|
-
const
|
|
287
|
+
const rawPath = parsed[2];
|
|
288
|
+
if (!rawPath) {
|
|
289
|
+
return extractInlineWechatAttachments(normalized);
|
|
290
|
+
}
|
|
291
|
+
const attachmentPath = resolveWechatAttachmentPath(rawPath);
|
|
244
292
|
if (!attachmentPath) {
|
|
245
293
|
return extractInlineWechatAttachments(normalized);
|
|
246
294
|
}
|
|
@@ -407,7 +455,11 @@ export function getInteractiveShellCommandRejectionMessage(command) {
|
|
|
407
455
|
if (!tokens.length) {
|
|
408
456
|
return null;
|
|
409
457
|
}
|
|
410
|
-
const
|
|
458
|
+
const firstToken = tokens[0];
|
|
459
|
+
if (!firstToken) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const executable = normalizeShellExecutableToken(firstToken);
|
|
411
463
|
const args = tokens.slice(1);
|
|
412
464
|
const lowerArgs = args.map((arg) => arg.toLowerCase());
|
|
413
465
|
if (!executable) {
|
|
@@ -419,7 +471,7 @@ export function getInteractiveShellCommandRejectionMessage(command) {
|
|
|
419
471
|
switch (executable) {
|
|
420
472
|
case "python":
|
|
421
473
|
case "python3":
|
|
422
|
-
case "py":
|
|
474
|
+
case "py": {
|
|
423
475
|
if (!args.length) {
|
|
424
476
|
return buildInteractiveShellCommandMessage(executable, 'Try "python script.py" or "python -c \\"...\\"" instead.');
|
|
425
477
|
}
|
|
@@ -438,6 +490,7 @@ export function getInteractiveShellCommandRejectionMessage(command) {
|
|
|
438
490
|
return hasScriptLikeArg(args)
|
|
439
491
|
? null
|
|
440
492
|
: buildInteractiveShellCommandMessage(executable, 'Try "python script.py" or "python -c \\"...\\"" instead.');
|
|
493
|
+
}
|
|
441
494
|
case "node":
|
|
442
495
|
if (!args.length || lowerArgs.includes("-i") || lowerArgs.includes("--interactive")) {
|
|
443
496
|
return buildInteractiveShellCommandMessage(executable, 'Try "node script.js" or "node -e \\"...\\"" instead.');
|
|
@@ -725,6 +778,9 @@ function stripInlineOpenCodeReasoningPrefix(text) {
|
|
|
725
778
|
}
|
|
726
779
|
return current;
|
|
727
780
|
}
|
|
781
|
+
function isOpenCodeReasoningResidue(text) {
|
|
782
|
+
return !text.replace(/[\s"'“”‘’`.,!?;:()[\]{}<>…。!?;:()【】《》、,-]/g, "");
|
|
783
|
+
}
|
|
728
784
|
export function sanitizeWechatFinalReplyText(adapter, text) {
|
|
729
785
|
const normalized = adapter === "opencode"
|
|
730
786
|
? cleanupVisibleWechatReplyText(stripInlineOpenCodeReasoningPrefix(text))
|
|
@@ -775,7 +831,8 @@ export function sanitizeWechatFinalReplyText(adapter, text) {
|
|
|
775
831
|
return cleaned;
|
|
776
832
|
}
|
|
777
833
|
const tail = cleanupVisibleWechatReplyText(keptLines.slice(tailStartIndex).join("\n"));
|
|
778
|
-
|
|
834
|
+
const resolved = tail || cleaned;
|
|
835
|
+
return isOpenCodeReasoningResidue(resolved) ? "" : resolved;
|
|
779
836
|
}
|
|
780
837
|
function extractInlineWechatAttachments(text) {
|
|
781
838
|
const sanitized = text
|
|
@@ -946,7 +1003,7 @@ function parseSingleUserInputAnswer(question, rawValue) {
|
|
|
946
1003
|
};
|
|
947
1004
|
}
|
|
948
1005
|
const separatorIndex = trimmed.indexOf("|");
|
|
949
|
-
|
|
1006
|
+
const selection = separatorIndex >= 0 ? trimmed.slice(0, separatorIndex).trim() : trimmed;
|
|
950
1007
|
let note = separatorIndex >= 0 ? trimmed.slice(separatorIndex + 1).trim() : "";
|
|
951
1008
|
const answers = [];
|
|
952
1009
|
if (selection) {
|
|
@@ -956,7 +1013,6 @@ function parseSingleUserInputAnswer(question, rawValue) {
|
|
|
956
1013
|
}
|
|
957
1014
|
else if (question.isOther) {
|
|
958
1015
|
note = note ? `${selection}; ${note}` : selection;
|
|
959
|
-
selection = "";
|
|
960
1016
|
}
|
|
961
1017
|
else {
|
|
962
1018
|
return {
|
|
@@ -986,6 +1042,11 @@ export function parsePendingUserInputAnswerCommand(raw, pending) {
|
|
|
986
1042
|
const answers = {};
|
|
987
1043
|
if (pending.questions.length === 1) {
|
|
988
1044
|
const question = pending.questions[0];
|
|
1045
|
+
if (!question) {
|
|
1046
|
+
return {
|
|
1047
|
+
error: "No pending user input question was found.",
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
989
1050
|
const parsed = parseSingleUserInputAnswer(question, input);
|
|
990
1051
|
if ("error" in parsed) {
|
|
991
1052
|
return parsed;
|
|
@@ -1068,6 +1129,9 @@ export function formatUserInputRequestMessage(pending, adapterState) {
|
|
|
1068
1129
|
lines.push("");
|
|
1069
1130
|
if (pending.questions.length === 1) {
|
|
1070
1131
|
const question = pending.questions[0];
|
|
1132
|
+
if (!question) {
|
|
1133
|
+
return lines.join("\n");
|
|
1134
|
+
}
|
|
1071
1135
|
if (question.options?.length) {
|
|
1072
1136
|
lines.push("Reply with /answer <option number or exact label>.");
|
|
1073
1137
|
if (question.isOther) {
|
|
@@ -1090,6 +1154,9 @@ export function formatUserInputRequestMessage(pending, adapterState) {
|
|
|
1090
1154
|
export function formatPendingUserInputReminder(pending) {
|
|
1091
1155
|
if (pending.questions.length === 1) {
|
|
1092
1156
|
const question = pending.questions[0];
|
|
1157
|
+
if (!question) {
|
|
1158
|
+
return "Codex is waiting for user input. Reply with /answer and your response, or use /stop to interrupt.";
|
|
1159
|
+
}
|
|
1093
1160
|
return `Codex is waiting for user input for ${question.header}. Reply with /answer and your response, or use /stop to interrupt.`;
|
|
1094
1161
|
}
|
|
1095
1162
|
return `Codex is waiting for answers to ${pending.questions.length} questions. Reply with /answer questionId=value; questionId2=value, or use /stop to interrupt.`;
|
|
@@ -108,7 +108,7 @@ function summarizeClaudePlan(plan) {
|
|
|
108
108
|
const description = lines.find((line) => !/^#+\s+/.test(line) &&
|
|
109
109
|
!/^[-*]\s+/.test(line) &&
|
|
110
110
|
!/^\d+\.\s+/.test(line));
|
|
111
|
-
return truncatePreview([heading, description].filter(Boolean).join(" - ") || lines[0], 180);
|
|
111
|
+
return truncatePreview([heading, description].filter(Boolean).join(" - ") || lines[0] || "(empty plan)", 180);
|
|
112
112
|
}
|
|
113
113
|
function summarizeClaudeToolInput(toolName, toolInput) {
|
|
114
114
|
if (!toolInput) {
|
|
@@ -214,7 +214,7 @@ export function extractClaudeTranscriptFinalReply(rawTranscript) {
|
|
|
214
214
|
if (!line) {
|
|
215
215
|
continue;
|
|
216
216
|
}
|
|
217
|
-
let parsed
|
|
217
|
+
let parsed;
|
|
218
218
|
try {
|
|
219
219
|
parsed = JSON.parse(line);
|
|
220
220
|
}
|
|
@@ -9,11 +9,14 @@ import { BridgeStateStore } from "./bridge-state.js";
|
|
|
9
9
|
import { reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses } from "./bridge-process-reaper.js";
|
|
10
10
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
11
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, } from "../wechat/wechat-transport.js";
|
|
12
|
+
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
|
|
13
13
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
14
|
+
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
14
15
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
15
16
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
16
17
|
const PARENT_PROCESS_POLL_MS = 5_000;
|
|
18
|
+
const WECHAT_SEND_MAX_ATTEMPTS = 3;
|
|
19
|
+
const WECHAT_SEND_RETRY_BASE_MS = 750;
|
|
17
20
|
function log(message) {
|
|
18
21
|
process.stderr.write(`[wechat-bridge] ${message}\n`);
|
|
19
22
|
}
|
|
@@ -73,6 +76,27 @@ export function formatUserFacingInboundError(params) {
|
|
|
73
76
|
export function formatWechatSendFailureLogEntry(params) {
|
|
74
77
|
return `wechat_send_failed: context=${params.context} recipient=${params.recipientId} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
75
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
|
+
}
|
|
76
100
|
export function shouldWatchParentProcess(options) {
|
|
77
101
|
return (options.startupParentPid > 1 &&
|
|
78
102
|
(options.attachedToTerminal || options.lifecycle === "companion_bound"));
|
|
@@ -197,6 +221,14 @@ function printUsageAndExit() {
|
|
|
197
221
|
}
|
|
198
222
|
async function main() {
|
|
199
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
|
+
}
|
|
200
232
|
const credentials = await ensureWechatCredentials({
|
|
201
233
|
requireUserId: true,
|
|
202
234
|
validateExisting: true,
|
|
@@ -271,6 +303,7 @@ async function main() {
|
|
|
271
303
|
stateStore.appendLog(`Cleared stale companion endpoint for ${options.cwd} before adapter start.`);
|
|
272
304
|
let textSendChain = Promise.resolve();
|
|
273
305
|
let attachmentSendChain = Promise.resolve();
|
|
306
|
+
const pendingWechatForwardTasks = new Set();
|
|
274
307
|
let activeTask = null;
|
|
275
308
|
const deferredInboundMessages = [];
|
|
276
309
|
let drainingDeferredInboundMessages = false;
|
|
@@ -288,15 +321,65 @@ async function main() {
|
|
|
288
321
|
return run;
|
|
289
322
|
};
|
|
290
323
|
const queueWechatMessage = (senderId, text, context = "message") => {
|
|
291
|
-
return queueWechatTextAction(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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;
|
|
298
365
|
});
|
|
299
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
|
+
};
|
|
300
383
|
const outputBatcher = new OutputBatcher(async (text) => {
|
|
301
384
|
await queueWechatMessage(stateStore.getState().authorizedUserId, text);
|
|
302
385
|
});
|
|
@@ -377,6 +460,7 @@ async function main() {
|
|
|
377
460
|
}
|
|
378
461
|
try {
|
|
379
462
|
await outputBatcher.flushNow();
|
|
463
|
+
await waitForPendingWechatForwardTasks();
|
|
380
464
|
}
|
|
381
465
|
catch {
|
|
382
466
|
// Best effort flush.
|
|
@@ -384,6 +468,7 @@ async function main() {
|
|
|
384
468
|
try {
|
|
385
469
|
await textSendChain;
|
|
386
470
|
await attachmentSendChain;
|
|
471
|
+
await waitForPendingWechatForwardTasks();
|
|
387
472
|
}
|
|
388
473
|
catch {
|
|
389
474
|
// Best effort flush.
|
|
@@ -458,6 +543,7 @@ async function main() {
|
|
|
458
543
|
outputBatcher,
|
|
459
544
|
queueWechatAttachmentAction,
|
|
460
545
|
queueWechatMessage,
|
|
546
|
+
trackWechatForwardTask,
|
|
461
547
|
maybeDrainDeferredInboundMessages,
|
|
462
548
|
getActiveTask: () => activeTask,
|
|
463
549
|
clearActiveTask: () => {
|
|
@@ -622,7 +708,7 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
622
708
|
}
|
|
623
709
|
}
|
|
624
710
|
function wireAdapterEvents(params) {
|
|
625
|
-
const { adapter, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, updateLastOutputAt, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
711
|
+
const { adapter, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, updateLastOutputAt, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
626
712
|
adapter.setEventSink((event) => {
|
|
627
713
|
syncSharedSessionState();
|
|
628
714
|
syncLocalClientEndpoint();
|
|
@@ -642,19 +728,28 @@ function wireAdapterEvents(params) {
|
|
|
642
728
|
break;
|
|
643
729
|
case "final_reply":
|
|
644
730
|
stateStore.appendLog(`final_reply: ${truncatePreview(event.text)}`);
|
|
645
|
-
|
|
731
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
646
732
|
await forwardWechatFinalReply({
|
|
647
733
|
adapter: options.adapter,
|
|
648
734
|
rawText: event.text,
|
|
735
|
+
onEmptyVisibleReply: ({ rawVisibleText }) => {
|
|
736
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${options.adapter} raw=${truncatePreview(rawVisibleText)}`);
|
|
737
|
+
},
|
|
649
738
|
sender: {
|
|
650
|
-
sendText: (text) =>
|
|
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
|
+
},
|
|
651
746
|
sendImage: (imagePath) => queueWechatAttachmentAction(() => transport.sendImage(imagePath, { recipientId: authorizedUserId })),
|
|
652
747
|
sendFile: (filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId: authorizedUserId })),
|
|
653
748
|
sendVoice: (voicePath) => queueWechatAttachmentAction(() => transport.sendVoice(voicePath, authorizedUserId)),
|
|
654
749
|
sendVideo: (videoPath) => queueWechatAttachmentAction(() => transport.sendVideo(videoPath, { recipientId: authorizedUserId })),
|
|
655
750
|
},
|
|
656
751
|
});
|
|
657
|
-
});
|
|
752
|
+
}));
|
|
658
753
|
break;
|
|
659
754
|
case "status":
|
|
660
755
|
if (event.message) {
|
|
@@ -667,56 +762,56 @@ function wireAdapterEvents(params) {
|
|
|
667
762
|
updateLastOutputAt();
|
|
668
763
|
stateStore.appendLog(`${event.level}_notice: ${truncatePreview(event.text)}`);
|
|
669
764
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
|
|
670
|
-
|
|
765
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
671
766
|
await queueWechatMessage(authorizedUserId, event.text, "notice");
|
|
672
|
-
});
|
|
767
|
+
}));
|
|
673
768
|
}
|
|
674
769
|
break;
|
|
675
770
|
case "approval_required":
|
|
676
|
-
|
|
771
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
677
772
|
const pending = toPendingApproval(event.request);
|
|
678
773
|
stateStore.setPendingConfirmation(pending);
|
|
679
774
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
680
775
|
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
681
|
-
});
|
|
776
|
+
}));
|
|
682
777
|
break;
|
|
683
778
|
case "mirrored_user_input":
|
|
684
779
|
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(event.text)}`);
|
|
685
780
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
|
|
686
|
-
|
|
781
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
687
782
|
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, event.text), "mirrored_user_input");
|
|
688
|
-
});
|
|
783
|
+
}));
|
|
689
784
|
}
|
|
690
785
|
break;
|
|
691
786
|
case "session_switched":
|
|
692
787
|
stateStore.appendLog(`session_switched: ${event.sessionId} source=${event.source} reason=${event.reason}`);
|
|
693
788
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
694
|
-
|
|
789
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
695
790
|
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
696
791
|
adapter: options.adapter,
|
|
697
792
|
sessionId: event.sessionId,
|
|
698
793
|
source: event.source,
|
|
699
794
|
reason: event.reason,
|
|
700
795
|
}), "session_switched");
|
|
701
|
-
});
|
|
796
|
+
}));
|
|
702
797
|
}
|
|
703
798
|
break;
|
|
704
799
|
case "thread_switched":
|
|
705
800
|
stateStore.appendLog(`thread_switched: ${event.threadId} source=${event.source} reason=${event.reason}`);
|
|
706
801
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
707
|
-
|
|
802
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
708
803
|
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
709
804
|
adapter: options.adapter,
|
|
710
805
|
sessionId: event.threadId,
|
|
711
806
|
source: event.source,
|
|
712
807
|
reason: event.reason,
|
|
713
808
|
}), "thread_switched");
|
|
714
|
-
});
|
|
809
|
+
}));
|
|
715
810
|
}
|
|
716
811
|
void maybeDrainDeferredInboundMessages();
|
|
717
812
|
break;
|
|
718
813
|
case "task_complete":
|
|
719
|
-
|
|
814
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
720
815
|
stateStore.clearPendingConfirmation();
|
|
721
816
|
if (options.adapter === "shell") {
|
|
722
817
|
const summary = buildCompletionSummary({
|
|
@@ -729,25 +824,25 @@ function wireAdapterEvents(params) {
|
|
|
729
824
|
}
|
|
730
825
|
clearActiveTask();
|
|
731
826
|
await maybeDrainDeferredInboundMessages();
|
|
732
|
-
});
|
|
827
|
+
}));
|
|
733
828
|
break;
|
|
734
829
|
case "task_failed":
|
|
735
|
-
|
|
830
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
736
831
|
stateStore.clearPendingConfirmation();
|
|
737
832
|
clearActiveTask();
|
|
738
833
|
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, event.message), "task_failed");
|
|
739
834
|
await maybeDrainDeferredInboundMessages();
|
|
740
|
-
});
|
|
835
|
+
}));
|
|
741
836
|
break;
|
|
742
837
|
case "fatal_error":
|
|
743
838
|
logError(event.message);
|
|
744
839
|
stateStore.appendLog(`fatal_error: ${event.message}`);
|
|
745
840
|
stateStore.clearPendingConfirmation();
|
|
746
841
|
clearActiveTask();
|
|
747
|
-
|
|
842
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
748
843
|
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(event.message), "fatal_error");
|
|
749
844
|
await maybeDrainDeferredInboundMessages();
|
|
750
|
-
});
|
|
845
|
+
}));
|
|
751
846
|
break;
|
|
752
847
|
case "shutdown_requested":
|
|
753
848
|
stateStore.appendLog(`shutdown_requested: ${event.reason}`);
|
|
@@ -768,12 +863,24 @@ function buildCompletionSummary(params) {
|
|
|
768
863
|
lines.push(`recent_output:\n${params.recentOutput}`);
|
|
769
864
|
return lines.join("\n");
|
|
770
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
|
+
}
|
|
771
877
|
async function handleInboundMessage(params) {
|
|
772
878
|
const { message, options, stateStore, adapter, queueWechatMessage, outputBatcher, deferInboundMessage, } = params;
|
|
773
879
|
const state = stateStore.getState();
|
|
774
880
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
775
881
|
adapter: options.adapter,
|
|
776
882
|
hasPendingConfirmation: Boolean(state.pendingConfirmation),
|
|
883
|
+
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
777
884
|
});
|
|
778
885
|
if (message.senderId !== state.authorizedUserId) {
|
|
779
886
|
await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
@@ -901,12 +1008,13 @@ async function handleInboundMessage(params) {
|
|
|
901
1008
|
}
|
|
902
1009
|
async function dispatchInboundWechatText(params) {
|
|
903
1010
|
const { message, options, stateStore, adapter } = params;
|
|
1011
|
+
const preview = formatInboundMessagePreview(message);
|
|
904
1012
|
const activeTask = {
|
|
905
1013
|
startedAt: Date.now(),
|
|
906
|
-
inputPreview: truncatePreview(
|
|
1014
|
+
inputPreview: truncatePreview(preview, 180),
|
|
907
1015
|
};
|
|
908
|
-
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(
|
|
909
|
-
await adapter.sendInput(buildWechatInboundPrompt(message.text));
|
|
1016
|
+
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
|
|
1017
|
+
await adapter.sendInput(buildWechatInboundPrompt(message.text, message.attachments));
|
|
910
1018
|
return activeTask;
|
|
911
1019
|
}
|
|
912
1020
|
const isDirectRun = Boolean(import.meta.main);
|
|
@@ -13,6 +13,9 @@ export function parseCliArgs(argv) {
|
|
|
13
13
|
const cliArgs = [];
|
|
14
14
|
for (let i = 0; i < argv.length; i += 1) {
|
|
15
15
|
const arg = argv[i];
|
|
16
|
+
if (!arg) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
16
19
|
const next = argv[i + 1];
|
|
17
20
|
if (arg === "--help" || arg === "-h") {
|
|
18
21
|
process.stdout.write([
|
|
@@ -37,7 +40,7 @@ export function parseCliArgs(argv) {
|
|
|
37
40
|
return { cwd, cliArgs };
|
|
38
41
|
}
|
|
39
42
|
export function readCodexRuntimeEndpoint(cwd) {
|
|
40
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
43
|
+
const endpoint = readLocalCompanionEndpoint(cwd, { adapter: "codex" });
|
|
41
44
|
if (!endpoint || endpoint.kind !== "codex") {
|
|
42
45
|
throw new Error(`No active Codex bridge endpoint was found for ${cwd}. Start "wechat-bridge-codex" in that directory first.`);
|
|
43
46
|
}
|
|
@@ -80,14 +83,18 @@ export async function runCodexRemoteClientFromEndpoint(endpoint, options = {}) {
|
|
|
80
83
|
updateLocalCompanionOccupancy(endpoint.cwd, {
|
|
81
84
|
companionPid: child.pid,
|
|
82
85
|
companionConnectedAt: new Date().toISOString(),
|
|
83
|
-
}, endpoint.instanceId);
|
|
86
|
+
}, endpoint.instanceId, { adapter: "codex" });
|
|
84
87
|
}
|
|
85
88
|
child.once("error", (error) => {
|
|
86
|
-
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId
|
|
89
|
+
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId, {
|
|
90
|
+
adapter: "codex",
|
|
91
|
+
});
|
|
87
92
|
reject(error);
|
|
88
93
|
});
|
|
89
94
|
child.once("exit", (code, signal) => {
|
|
90
|
-
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId
|
|
95
|
+
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId, {
|
|
96
|
+
adapter: "codex",
|
|
97
|
+
});
|
|
91
98
|
if (signal) {
|
|
92
99
|
process.kill(process.pid, signal);
|
|
93
100
|
return;
|