@unlinearity/cli-wechat-bridge 1.0.2 → 1.0.4
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 +221 -223
- 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 +26 -4
- package/dist/bridge/bridge-adapters.core.js +10 -1
- package/dist/bridge/bridge-adapters.opencode.js +395 -43
- package/dist/bridge/bridge-adapters.shared.js +4 -97
- package/dist/bridge/bridge-final-reply.js +18 -3
- package/dist/bridge/bridge-state.js +9 -3
- package/dist/bridge/bridge-utils.js +62 -8
- package/dist/bridge/claude-hooks.js +2 -2
- package/dist/bridge/wechat-bridge.js +111 -27
- package/dist/companion/codex-remote-client.js +3 -0
- package/dist/companion/local-companion-start.js +6 -1
- package/dist/companion/local-companion.js +11 -0
- package/dist/runtime/legacy-adapter-runtime.js +6 -0
- package/dist/utils/version-checker.js +1 -1
- package/dist/wechat/wechat-transport.js +46 -2
- package/package.json +10 -2
|
@@ -8,6 +8,8 @@ import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLoc
|
|
|
8
8
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
9
9
|
import { buildClaudeFailureMessage, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeResumeConversationId, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
10
10
|
import { detectCliApproval, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
11
|
+
import { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
12
|
+
export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
11
13
|
export const DEFAULT_COLS = 120;
|
|
12
14
|
export const DEFAULT_ROWS = 30;
|
|
13
15
|
export const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -39,101 +41,6 @@ export const OPENCODE_SERVER_READY_TIMEOUT_MS = 10_000;
|
|
|
39
41
|
export const OPENCODE_SSE_RECONNECT_DELAY_MS = 2_000;
|
|
40
42
|
export const OPENCODE_SESSION_IDLE_SETTLE_MS = 1_500;
|
|
41
43
|
export const OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS = 12_000;
|
|
42
|
-
export function isRecord(value) {
|
|
43
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
-
}
|
|
45
|
-
export function getCodexRpcRequestId(value) {
|
|
46
|
-
return typeof value === "string" || typeof value === "number" ? value : null;
|
|
47
|
-
}
|
|
48
|
-
export function getNotificationThreadId(params) {
|
|
49
|
-
if (!isRecord(params)) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
if (typeof params.threadId === "string") {
|
|
53
|
-
return params.threadId;
|
|
54
|
-
}
|
|
55
|
-
if (isRecord(params.thread) && typeof params.thread.id === "string") {
|
|
56
|
-
return params.thread.id;
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
export function getNotificationTurnId(params) {
|
|
61
|
-
if (!isRecord(params)) {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
if (typeof params.turnId === "string") {
|
|
65
|
-
return params.turnId;
|
|
66
|
-
}
|
|
67
|
-
if (isRecord(params.turn) && typeof params.turn.id === "string") {
|
|
68
|
-
return params.turn.id;
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
export function describeUnknownError(error) {
|
|
73
|
-
if (error instanceof Error && error.message) {
|
|
74
|
-
return error.message;
|
|
75
|
-
}
|
|
76
|
-
return String(error);
|
|
77
|
-
}
|
|
78
|
-
export function normalizeCodexRpcError(error) {
|
|
79
|
-
if (isRecord(error)) {
|
|
80
|
-
const message = typeof error.message === "string"
|
|
81
|
-
? error.message
|
|
82
|
-
: typeof error.code === "number"
|
|
83
|
-
? `RPC error ${error.code}`
|
|
84
|
-
: "";
|
|
85
|
-
const data = typeof error.data === "string"
|
|
86
|
-
? error.data
|
|
87
|
-
: typeof error.details === "string"
|
|
88
|
-
? error.details
|
|
89
|
-
: "";
|
|
90
|
-
const combined = [message, data].filter(Boolean).join(": ");
|
|
91
|
-
if (combined) {
|
|
92
|
-
return combined;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return describeUnknownError(error);
|
|
96
|
-
}
|
|
97
|
-
export function getLocalCompanionCommandName(kind) {
|
|
98
|
-
switch (kind) {
|
|
99
|
-
case "codex":
|
|
100
|
-
return "wechat-codex";
|
|
101
|
-
case "claude":
|
|
102
|
-
return "wechat-claude";
|
|
103
|
-
case "opencode":
|
|
104
|
-
return "wechat-opencode";
|
|
105
|
-
default:
|
|
106
|
-
return "local companion";
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
export function getSharedSessionIdFromAdapterState(state) {
|
|
110
|
-
return state.sharedSessionId ?? state.sharedThreadId;
|
|
111
|
-
}
|
|
112
|
-
export function quoteWindowsCommandArg(value) {
|
|
113
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
114
|
-
}
|
|
115
|
-
export function quotePosixCommandArg(value) {
|
|
116
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
117
|
-
}
|
|
118
|
-
export function isRecentIsoTimestamp(timestamp, maxAgeMs) {
|
|
119
|
-
const parsedMs = Date.parse(timestamp);
|
|
120
|
-
if (!Number.isFinite(parsedMs)) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
return parsedMs >= Date.now() - maxAgeMs;
|
|
124
|
-
}
|
|
125
|
-
export function coerceWebSocketMessageData(data) {
|
|
126
|
-
if (typeof data === "string") {
|
|
127
|
-
return data;
|
|
128
|
-
}
|
|
129
|
-
if (data instanceof ArrayBuffer) {
|
|
130
|
-
return Buffer.from(data).toString("utf8");
|
|
131
|
-
}
|
|
132
|
-
if (ArrayBuffer.isView(data)) {
|
|
133
|
-
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
44
|
export function buildCodexCliArgs(remoteUrl, options = {}) {
|
|
138
45
|
assertNoReservedExtraCliArgs(options.extraCliArgs ?? [], ["--remote", "--remote-auth-token-env"], "Codex remote connection");
|
|
139
46
|
const args = [];
|
|
@@ -196,7 +103,7 @@ export function shouldIncludeClaudeNoAltScreen(command) {
|
|
|
196
103
|
if (cached !== undefined) {
|
|
197
104
|
return cached;
|
|
198
105
|
}
|
|
199
|
-
let supported
|
|
106
|
+
let supported;
|
|
200
107
|
try {
|
|
201
108
|
const probe = spawnSync(spawnTarget.file, [...spawnTarget.args, "--help"], {
|
|
202
109
|
cwd: process.cwd(),
|
|
@@ -660,7 +567,7 @@ export function escapePowerShellString(text) {
|
|
|
660
567
|
return text.replace(/`/g, "``").replace(/"/g, '`"');
|
|
661
568
|
}
|
|
662
569
|
export function escapePosixShellString(text) {
|
|
663
|
-
return `'${text.replace(/'/g, `'
|
|
570
|
+
return `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
664
571
|
}
|
|
665
572
|
export function buildShellProfileCommand(profilePath, family) {
|
|
666
573
|
const resolved = path.resolve(profilePath);
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { formatFinalReplyMessage, parseWechatFinalReply, sanitizeWechatFinalReplyText, } from "./bridge-utils.js";
|
|
2
|
+
export const OPENCODE_EMPTY_VISIBLE_REPLY_MESSAGE = "OpenCode 没有产生可发送到微信的可见回复。请查看本地终端输出,或重试这条消息。";
|
|
2
3
|
export async function forwardWechatFinalReply(params) {
|
|
3
|
-
const { adapter, rawText, sender } = params;
|
|
4
|
+
const { adapter, rawText, sender, onEmptyVisibleReply } = params;
|
|
4
5
|
const parsed = parseWechatFinalReply(rawText);
|
|
5
|
-
const
|
|
6
|
+
const sanitizedText = sanitizeWechatFinalReplyText(adapter, parsed.visibleText);
|
|
7
|
+
const visibleText = formatFinalReplyMessage(adapter, sanitizedText).trim();
|
|
6
8
|
if (visibleText) {
|
|
7
|
-
await sender.sendText(visibleText);
|
|
9
|
+
const sent = await sender.sendText(visibleText);
|
|
10
|
+
if (sent === false) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
else if (adapter === "opencode" && parsed.visibleText.trim()) {
|
|
15
|
+
onEmptyVisibleReply?.({
|
|
16
|
+
adapter,
|
|
17
|
+
rawVisibleText: parsed.visibleText,
|
|
18
|
+
});
|
|
19
|
+
const sent = await sender.sendText(OPENCODE_EMPTY_VISIBLE_REPLY_MESSAGE);
|
|
20
|
+
if (sent === false) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
8
23
|
}
|
|
9
24
|
for (const attachment of parsed.attachments) {
|
|
10
25
|
try {
|
|
@@ -3,6 +3,14 @@ import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, ensureWorkspaceChannelDir, ensureCha
|
|
|
3
3
|
import { buildInstanceId } from "./bridge-utils.js";
|
|
4
4
|
const ORPHAN_LOCK_RECLAIM_TIMEOUT_MS = 2_000;
|
|
5
5
|
const ORPHAN_LOCK_RECLAIM_POLL_MS = 100;
|
|
6
|
+
export function resolveRestorableSharedSessionId(persisted, options) {
|
|
7
|
+
if (!persisted ||
|
|
8
|
+
persisted.cwd !== options.cwd ||
|
|
9
|
+
persisted.adapter !== options.adapter) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return persisted.sharedSessionId ?? persisted.sharedThreadId;
|
|
13
|
+
}
|
|
6
14
|
function cloneState(state) {
|
|
7
15
|
return JSON.parse(JSON.stringify(state));
|
|
8
16
|
}
|
|
@@ -162,9 +170,7 @@ export class BridgeStateStore {
|
|
|
162
170
|
};
|
|
163
171
|
this.acquireLock();
|
|
164
172
|
const persisted = this.readStateFile();
|
|
165
|
-
const persistedSharedSessionId = persisted
|
|
166
|
-
? persisted.sharedSessionId ?? persisted.sharedThreadId
|
|
167
|
-
: undefined;
|
|
173
|
+
const persistedSharedSessionId = resolveRestorableSharedSessionId(persisted, options);
|
|
168
174
|
const persistedResumeConversationId = options.adapter === "claude" &&
|
|
169
175
|
persisted?.cwd === options.cwd &&
|
|
170
176
|
typeof persisted.resumeConversationId === "string"
|
|
@@ -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) {
|
|
@@ -139,6 +143,9 @@ export function parseSystemCommand(text) {
|
|
|
139
143
|
return { type: "status" };
|
|
140
144
|
case "/resume":
|
|
141
145
|
return argument ? { type: "resume", target: argument } : { type: "resume" };
|
|
146
|
+
case "/new":
|
|
147
|
+
case "/new-session":
|
|
148
|
+
return { type: "new_session" };
|
|
142
149
|
case "/stop":
|
|
143
150
|
return { type: "stop" };
|
|
144
151
|
case "/reset":
|
|
@@ -224,7 +231,11 @@ export function parseWechatFinalReply(text) {
|
|
|
224
231
|
return extractInlineWechatAttachments(normalized);
|
|
225
232
|
}
|
|
226
233
|
const attachments = [];
|
|
227
|
-
const
|
|
234
|
+
const attachmentBlock = match[1];
|
|
235
|
+
if (attachmentBlock === undefined) {
|
|
236
|
+
return extractInlineWechatAttachments(normalized);
|
|
237
|
+
}
|
|
238
|
+
const lines = attachmentBlock
|
|
228
239
|
.split("\n")
|
|
229
240
|
.map((line) => line.trim())
|
|
230
241
|
.filter(Boolean);
|
|
@@ -237,7 +248,11 @@ export function parseWechatFinalReply(text) {
|
|
|
237
248
|
return extractInlineWechatAttachments(normalized);
|
|
238
249
|
}
|
|
239
250
|
const kind = parsed[1];
|
|
240
|
-
const
|
|
251
|
+
const rawPath = parsed[2];
|
|
252
|
+
if (!rawPath) {
|
|
253
|
+
return extractInlineWechatAttachments(normalized);
|
|
254
|
+
}
|
|
255
|
+
const attachmentPath = resolveWechatAttachmentPath(rawPath);
|
|
241
256
|
if (!attachmentPath) {
|
|
242
257
|
return extractInlineWechatAttachments(normalized);
|
|
243
258
|
}
|
|
@@ -404,7 +419,11 @@ export function getInteractiveShellCommandRejectionMessage(command) {
|
|
|
404
419
|
if (!tokens.length) {
|
|
405
420
|
return null;
|
|
406
421
|
}
|
|
407
|
-
const
|
|
422
|
+
const firstToken = tokens[0];
|
|
423
|
+
if (!firstToken) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const executable = normalizeShellExecutableToken(firstToken);
|
|
408
427
|
const args = tokens.slice(1);
|
|
409
428
|
const lowerArgs = args.map((arg) => arg.toLowerCase());
|
|
410
429
|
if (!executable) {
|
|
@@ -416,7 +435,7 @@ export function getInteractiveShellCommandRejectionMessage(command) {
|
|
|
416
435
|
switch (executable) {
|
|
417
436
|
case "python":
|
|
418
437
|
case "python3":
|
|
419
|
-
case "py":
|
|
438
|
+
case "py": {
|
|
420
439
|
if (!args.length) {
|
|
421
440
|
return buildInteractiveShellCommandMessage(executable, 'Try "python script.py" or "python -c \\"...\\"" instead.');
|
|
422
441
|
}
|
|
@@ -435,6 +454,7 @@ export function getInteractiveShellCommandRejectionMessage(command) {
|
|
|
435
454
|
return hasScriptLikeArg(args)
|
|
436
455
|
? null
|
|
437
456
|
: buildInteractiveShellCommandMessage(executable, 'Try "python script.py" or "python -c \\"...\\"" instead.');
|
|
457
|
+
}
|
|
438
458
|
case "node":
|
|
439
459
|
if (!args.length || lowerArgs.includes("-i") || lowerArgs.includes("--interactive")) {
|
|
440
460
|
return buildInteractiveShellCommandMessage(executable, 'Try "node script.js" or "node -e \\"...\\"" instead.');
|
|
@@ -694,6 +714,7 @@ const OPENCODE_REASONING_LINE_RES = [
|
|
|
694
714
|
/\bI need to (?:respond|reply|answer|tell the user)\b/i,
|
|
695
715
|
/\bWe need to (?:respond|reply|answer)\b/i,
|
|
696
716
|
/\bI should\b/i,
|
|
717
|
+
/\bI(?:'ll| will) (?:respond|reply|answer|tell the user|provide)\b/i,
|
|
697
718
|
/\bI'll provide\b/i,
|
|
698
719
|
/^Let me (?:directly )?(?:answer|respond)\b/i,
|
|
699
720
|
/根据系统提示/i,
|
|
@@ -704,8 +725,30 @@ const OPENCODE_REASONING_LINE_RES = [
|
|
|
704
725
|
/^我要直接(?:回答|回复)/,
|
|
705
726
|
/^用户(?:说|问)了/,
|
|
706
727
|
];
|
|
728
|
+
const OPENCODE_INLINE_REASONING_MARKER_RE = /\b(?:The user\b|I need to\b|I should\b|I(?:'ll| will)\b|We need to\b|Let me\b)/i;
|
|
729
|
+
const OPENCODE_INLINE_REASONING_SENTENCE_RE = /^(?:The user\b|I need to\b|I should\b|I(?:'ll| will)\b|We need to\b|Let me\b)[^.!?\n]*(?:[.!?]+)\s*/i;
|
|
730
|
+
function stripInlineOpenCodeReasoningPrefix(text) {
|
|
731
|
+
let current = text.trim();
|
|
732
|
+
const markerIndex = current.search(OPENCODE_INLINE_REASONING_MARKER_RE);
|
|
733
|
+
if (markerIndex > 0 && markerIndex <= 80) {
|
|
734
|
+
current = current.slice(markerIndex).trimStart();
|
|
735
|
+
}
|
|
736
|
+
for (let index = 0; index < 6; index += 1) {
|
|
737
|
+
const match = current.match(OPENCODE_INLINE_REASONING_SENTENCE_RE);
|
|
738
|
+
if (!match) {
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
current = current.slice(match[0].length).trimStart();
|
|
742
|
+
}
|
|
743
|
+
return current;
|
|
744
|
+
}
|
|
745
|
+
function isOpenCodeReasoningResidue(text) {
|
|
746
|
+
return !text.replace(/[\s"'“”‘’`.,!?;:()[\]{}<>…。!?;:()【】《》、,-]/g, "");
|
|
747
|
+
}
|
|
707
748
|
export function sanitizeWechatFinalReplyText(adapter, text) {
|
|
708
|
-
const normalized =
|
|
749
|
+
const normalized = adapter === "opencode"
|
|
750
|
+
? cleanupVisibleWechatReplyText(stripInlineOpenCodeReasoningPrefix(text))
|
|
751
|
+
: cleanupVisibleWechatReplyText(text);
|
|
709
752
|
if (!normalized || adapter !== "opencode") {
|
|
710
753
|
return normalized;
|
|
711
754
|
}
|
|
@@ -752,7 +795,8 @@ export function sanitizeWechatFinalReplyText(adapter, text) {
|
|
|
752
795
|
return cleaned;
|
|
753
796
|
}
|
|
754
797
|
const tail = cleanupVisibleWechatReplyText(keptLines.slice(tailStartIndex).join("\n"));
|
|
755
|
-
|
|
798
|
+
const resolved = tail || cleaned;
|
|
799
|
+
return isOpenCodeReasoningResidue(resolved) ? "" : resolved;
|
|
756
800
|
}
|
|
757
801
|
function extractInlineWechatAttachments(text) {
|
|
758
802
|
const sanitized = text
|
|
@@ -923,7 +967,7 @@ function parseSingleUserInputAnswer(question, rawValue) {
|
|
|
923
967
|
};
|
|
924
968
|
}
|
|
925
969
|
const separatorIndex = trimmed.indexOf("|");
|
|
926
|
-
|
|
970
|
+
const selection = separatorIndex >= 0 ? trimmed.slice(0, separatorIndex).trim() : trimmed;
|
|
927
971
|
let note = separatorIndex >= 0 ? trimmed.slice(separatorIndex + 1).trim() : "";
|
|
928
972
|
const answers = [];
|
|
929
973
|
if (selection) {
|
|
@@ -933,7 +977,6 @@ function parseSingleUserInputAnswer(question, rawValue) {
|
|
|
933
977
|
}
|
|
934
978
|
else if (question.isOther) {
|
|
935
979
|
note = note ? `${selection}; ${note}` : selection;
|
|
936
|
-
selection = "";
|
|
937
980
|
}
|
|
938
981
|
else {
|
|
939
982
|
return {
|
|
@@ -963,6 +1006,11 @@ export function parsePendingUserInputAnswerCommand(raw, pending) {
|
|
|
963
1006
|
const answers = {};
|
|
964
1007
|
if (pending.questions.length === 1) {
|
|
965
1008
|
const question = pending.questions[0];
|
|
1009
|
+
if (!question) {
|
|
1010
|
+
return {
|
|
1011
|
+
error: "No pending user input question was found.",
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
966
1014
|
const parsed = parseSingleUserInputAnswer(question, input);
|
|
967
1015
|
if ("error" in parsed) {
|
|
968
1016
|
return parsed;
|
|
@@ -1045,6 +1093,9 @@ export function formatUserInputRequestMessage(pending, adapterState) {
|
|
|
1045
1093
|
lines.push("");
|
|
1046
1094
|
if (pending.questions.length === 1) {
|
|
1047
1095
|
const question = pending.questions[0];
|
|
1096
|
+
if (!question) {
|
|
1097
|
+
return lines.join("\n");
|
|
1098
|
+
}
|
|
1048
1099
|
if (question.options?.length) {
|
|
1049
1100
|
lines.push("Reply with /answer <option number or exact label>.");
|
|
1050
1101
|
if (question.isOther) {
|
|
@@ -1067,6 +1118,9 @@ export function formatUserInputRequestMessage(pending, adapterState) {
|
|
|
1067
1118
|
export function formatPendingUserInputReminder(pending) {
|
|
1068
1119
|
if (pending.questions.length === 1) {
|
|
1069
1120
|
const question = pending.questions[0];
|
|
1121
|
+
if (!question) {
|
|
1122
|
+
return "Codex is waiting for user input. Reply with /answer and your response, or use /stop to interrupt.";
|
|
1123
|
+
}
|
|
1070
1124
|
return `Codex is waiting for user input for ${question.header}. Reply with /answer and your response, or use /stop to interrupt.`;
|
|
1071
1125
|
}
|
|
1072
1126
|
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
|
}
|
|
@@ -14,6 +14,8 @@ import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.j
|
|
|
14
14
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
15
15
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
16
16
|
const PARENT_PROCESS_POLL_MS = 5_000;
|
|
17
|
+
const WECHAT_SEND_MAX_ATTEMPTS = 3;
|
|
18
|
+
const WECHAT_SEND_RETRY_BASE_MS = 750;
|
|
17
19
|
function log(message) {
|
|
18
20
|
process.stderr.write(`[wechat-bridge] ${message}\n`);
|
|
19
21
|
}
|
|
@@ -73,6 +75,21 @@ export function formatUserFacingInboundError(params) {
|
|
|
73
75
|
export function formatWechatSendFailureLogEntry(params) {
|
|
74
76
|
return `wechat_send_failed: context=${params.context} recipient=${params.recipientId} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
75
77
|
}
|
|
78
|
+
function formatWechatSendRetryLogEntry(params) {
|
|
79
|
+
return `wechat_send_retry: context=${params.context} recipient=${params.recipientId} attempt=${params.attempt} delay_ms=${params.delayMs} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
80
|
+
}
|
|
81
|
+
function isRetryableWechatSendError(error) {
|
|
82
|
+
const classification = classifyWechatTransportError(error);
|
|
83
|
+
if (classification.retryable) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const details = describeWechatTransportError(error);
|
|
87
|
+
return /^Error: sendmessage failed:/i.test(details) &&
|
|
88
|
+
!/errcode=-14\b.*session timeout/i.test(details);
|
|
89
|
+
}
|
|
90
|
+
function computeWechatSendRetryDelayMs(attempt) {
|
|
91
|
+
return WECHAT_SEND_RETRY_BASE_MS * attempt;
|
|
92
|
+
}
|
|
76
93
|
export function shouldWatchParentProcess(options) {
|
|
77
94
|
return (options.startupParentPid > 1 &&
|
|
78
95
|
(options.attachedToTerminal || options.lifecycle === "companion_bound"));
|
|
@@ -271,6 +288,7 @@ async function main() {
|
|
|
271
288
|
stateStore.appendLog(`Cleared stale companion endpoint for ${options.cwd} before adapter start.`);
|
|
272
289
|
let textSendChain = Promise.resolve();
|
|
273
290
|
let attachmentSendChain = Promise.resolve();
|
|
291
|
+
const pendingWechatForwardTasks = new Set();
|
|
274
292
|
let activeTask = null;
|
|
275
293
|
const deferredInboundMessages = [];
|
|
276
294
|
let drainingDeferredInboundMessages = false;
|
|
@@ -288,14 +306,53 @@ async function main() {
|
|
|
288
306
|
return run;
|
|
289
307
|
};
|
|
290
308
|
const queueWechatMessage = (senderId, text, context = "message") => {
|
|
291
|
-
return queueWechatTextAction(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
309
|
+
return queueWechatTextAction(async () => {
|
|
310
|
+
for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
|
|
311
|
+
try {
|
|
312
|
+
await transport.sendText(senderId, text);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
317
|
+
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
318
|
+
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
319
|
+
stateStore.appendLog(formatWechatSendRetryLogEntry({
|
|
320
|
+
context,
|
|
321
|
+
recipientId: senderId,
|
|
322
|
+
attempt,
|
|
323
|
+
delayMs,
|
|
324
|
+
error: err,
|
|
325
|
+
}));
|
|
326
|
+
await delay(delayMs);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
|
|
330
|
+
stateStore.appendLog(formatWechatSendFailureLogEntry({
|
|
331
|
+
context,
|
|
332
|
+
recipientId: senderId,
|
|
333
|
+
error: err,
|
|
334
|
+
}));
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
const trackWechatForwardTask = (task) => {
|
|
342
|
+
const tracked = task
|
|
343
|
+
.catch((error) => {
|
|
344
|
+
logError(`WeChat forward task failed: ${describeWechatTransportError(error)}`);
|
|
345
|
+
stateStore.appendLog(`wechat_forward_failed: error=${truncatePreview(describeWechatTransportError(error), 400)}`);
|
|
346
|
+
})
|
|
347
|
+
.finally(() => {
|
|
348
|
+
pendingWechatForwardTasks.delete(tracked);
|
|
298
349
|
});
|
|
350
|
+
pendingWechatForwardTasks.add(tracked);
|
|
351
|
+
};
|
|
352
|
+
const waitForPendingWechatForwardTasks = async () => {
|
|
353
|
+
while (pendingWechatForwardTasks.size > 0) {
|
|
354
|
+
await Promise.allSettled([...pendingWechatForwardTasks]);
|
|
355
|
+
}
|
|
299
356
|
};
|
|
300
357
|
const outputBatcher = new OutputBatcher(async (text) => {
|
|
301
358
|
await queueWechatMessage(stateStore.getState().authorizedUserId, text);
|
|
@@ -377,6 +434,7 @@ async function main() {
|
|
|
377
434
|
}
|
|
378
435
|
try {
|
|
379
436
|
await outputBatcher.flushNow();
|
|
437
|
+
await waitForPendingWechatForwardTasks();
|
|
380
438
|
}
|
|
381
439
|
catch {
|
|
382
440
|
// Best effort flush.
|
|
@@ -384,6 +442,7 @@ async function main() {
|
|
|
384
442
|
try {
|
|
385
443
|
await textSendChain;
|
|
386
444
|
await attachmentSendChain;
|
|
445
|
+
await waitForPendingWechatForwardTasks();
|
|
387
446
|
}
|
|
388
447
|
catch {
|
|
389
448
|
// Best effort flush.
|
|
@@ -458,6 +517,7 @@ async function main() {
|
|
|
458
517
|
outputBatcher,
|
|
459
518
|
queueWechatAttachmentAction,
|
|
460
519
|
queueWechatMessage,
|
|
520
|
+
trackWechatForwardTask,
|
|
461
521
|
maybeDrainDeferredInboundMessages,
|
|
462
522
|
getActiveTask: () => activeTask,
|
|
463
523
|
clearActiveTask: () => {
|
|
@@ -622,7 +682,7 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
622
682
|
}
|
|
623
683
|
}
|
|
624
684
|
function wireAdapterEvents(params) {
|
|
625
|
-
const { adapter, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, updateLastOutputAt, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
685
|
+
const { adapter, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, updateLastOutputAt, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
|
|
626
686
|
adapter.setEventSink((event) => {
|
|
627
687
|
syncSharedSessionState();
|
|
628
688
|
syncLocalClientEndpoint();
|
|
@@ -641,19 +701,29 @@ function wireAdapterEvents(params) {
|
|
|
641
701
|
}
|
|
642
702
|
break;
|
|
643
703
|
case "final_reply":
|
|
644
|
-
|
|
704
|
+
stateStore.appendLog(`final_reply: ${truncatePreview(event.text)}`);
|
|
705
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
645
706
|
await forwardWechatFinalReply({
|
|
646
707
|
adapter: options.adapter,
|
|
647
708
|
rawText: event.text,
|
|
709
|
+
onEmptyVisibleReply: ({ rawVisibleText }) => {
|
|
710
|
+
stateStore.appendLog(`empty_visible_final_reply: adapter=${options.adapter} raw=${truncatePreview(rawVisibleText)}`);
|
|
711
|
+
},
|
|
648
712
|
sender: {
|
|
649
|
-
sendText: (text) =>
|
|
713
|
+
sendText: async (text) => {
|
|
714
|
+
const sent = await queueWechatMessage(authorizedUserId, text, "final_reply");
|
|
715
|
+
if (sent) {
|
|
716
|
+
stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
|
|
717
|
+
}
|
|
718
|
+
return sent;
|
|
719
|
+
},
|
|
650
720
|
sendImage: (imagePath) => queueWechatAttachmentAction(() => transport.sendImage(imagePath, { recipientId: authorizedUserId })),
|
|
651
721
|
sendFile: (filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId: authorizedUserId })),
|
|
652
722
|
sendVoice: (voicePath) => queueWechatAttachmentAction(() => transport.sendVoice(voicePath, authorizedUserId)),
|
|
653
723
|
sendVideo: (videoPath) => queueWechatAttachmentAction(() => transport.sendVideo(videoPath, { recipientId: authorizedUserId })),
|
|
654
724
|
},
|
|
655
725
|
});
|
|
656
|
-
});
|
|
726
|
+
}));
|
|
657
727
|
break;
|
|
658
728
|
case "status":
|
|
659
729
|
if (event.message) {
|
|
@@ -666,56 +736,56 @@ function wireAdapterEvents(params) {
|
|
|
666
736
|
updateLastOutputAt();
|
|
667
737
|
stateStore.appendLog(`${event.level}_notice: ${truncatePreview(event.text)}`);
|
|
668
738
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
|
|
669
|
-
|
|
739
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
670
740
|
await queueWechatMessage(authorizedUserId, event.text, "notice");
|
|
671
|
-
});
|
|
741
|
+
}));
|
|
672
742
|
}
|
|
673
743
|
break;
|
|
674
744
|
case "approval_required":
|
|
675
|
-
|
|
745
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
676
746
|
const pending = toPendingApproval(event.request);
|
|
677
747
|
stateStore.setPendingConfirmation(pending);
|
|
678
748
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
679
749
|
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
680
|
-
});
|
|
750
|
+
}));
|
|
681
751
|
break;
|
|
682
752
|
case "mirrored_user_input":
|
|
683
753
|
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(event.text)}`);
|
|
684
754
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
|
|
685
|
-
|
|
755
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
686
756
|
await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, event.text), "mirrored_user_input");
|
|
687
|
-
});
|
|
757
|
+
}));
|
|
688
758
|
}
|
|
689
759
|
break;
|
|
690
760
|
case "session_switched":
|
|
691
761
|
stateStore.appendLog(`session_switched: ${event.sessionId} source=${event.source} reason=${event.reason}`);
|
|
692
762
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
693
|
-
|
|
763
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
694
764
|
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
695
765
|
adapter: options.adapter,
|
|
696
766
|
sessionId: event.sessionId,
|
|
697
767
|
source: event.source,
|
|
698
768
|
reason: event.reason,
|
|
699
769
|
}), "session_switched");
|
|
700
|
-
});
|
|
770
|
+
}));
|
|
701
771
|
}
|
|
702
772
|
break;
|
|
703
773
|
case "thread_switched":
|
|
704
774
|
stateStore.appendLog(`thread_switched: ${event.threadId} source=${event.source} reason=${event.reason}`);
|
|
705
775
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
|
|
706
|
-
|
|
776
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
707
777
|
await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
|
|
708
778
|
adapter: options.adapter,
|
|
709
779
|
sessionId: event.threadId,
|
|
710
780
|
source: event.source,
|
|
711
781
|
reason: event.reason,
|
|
712
782
|
}), "thread_switched");
|
|
713
|
-
});
|
|
783
|
+
}));
|
|
714
784
|
}
|
|
715
785
|
void maybeDrainDeferredInboundMessages();
|
|
716
786
|
break;
|
|
717
787
|
case "task_complete":
|
|
718
|
-
|
|
788
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
719
789
|
stateStore.clearPendingConfirmation();
|
|
720
790
|
if (options.adapter === "shell") {
|
|
721
791
|
const summary = buildCompletionSummary({
|
|
@@ -728,25 +798,25 @@ function wireAdapterEvents(params) {
|
|
|
728
798
|
}
|
|
729
799
|
clearActiveTask();
|
|
730
800
|
await maybeDrainDeferredInboundMessages();
|
|
731
|
-
});
|
|
801
|
+
}));
|
|
732
802
|
break;
|
|
733
803
|
case "task_failed":
|
|
734
|
-
|
|
804
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
735
805
|
stateStore.clearPendingConfirmation();
|
|
736
806
|
clearActiveTask();
|
|
737
807
|
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, event.message), "task_failed");
|
|
738
808
|
await maybeDrainDeferredInboundMessages();
|
|
739
|
-
});
|
|
809
|
+
}));
|
|
740
810
|
break;
|
|
741
811
|
case "fatal_error":
|
|
742
812
|
logError(event.message);
|
|
743
813
|
stateStore.appendLog(`fatal_error: ${event.message}`);
|
|
744
814
|
stateStore.clearPendingConfirmation();
|
|
745
815
|
clearActiveTask();
|
|
746
|
-
|
|
816
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
747
817
|
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(event.message), "fatal_error");
|
|
748
818
|
await maybeDrainDeferredInboundMessages();
|
|
749
|
-
});
|
|
819
|
+
}));
|
|
750
820
|
break;
|
|
751
821
|
case "shutdown_requested":
|
|
752
822
|
stateStore.appendLog(`shutdown_requested: ${event.reason}`);
|
|
@@ -773,6 +843,7 @@ async function handleInboundMessage(params) {
|
|
|
773
843
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
774
844
|
adapter: options.adapter,
|
|
775
845
|
hasPendingConfirmation: Boolean(state.pendingConfirmation),
|
|
846
|
+
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
776
847
|
});
|
|
777
848
|
if (message.senderId !== state.authorizedUserId) {
|
|
778
849
|
await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
@@ -798,6 +869,19 @@ async function handleInboundMessage(params) {
|
|
|
798
869
|
await queueWechatMessage(message.senderId, `/resume is not available in ${options.adapter} mode.`);
|
|
799
870
|
return null;
|
|
800
871
|
}
|
|
872
|
+
case "new_session": {
|
|
873
|
+
if (!adapter.createSession) {
|
|
874
|
+
await queueWechatMessage(message.senderId, `/new is not available in ${options.adapter} mode.`);
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
await outputBatcher.flushNow();
|
|
878
|
+
outputBatcher.clear();
|
|
879
|
+
stateStore.clearPendingConfirmation();
|
|
880
|
+
stateStore.clearSharedSessionId();
|
|
881
|
+
await adapter.createSession();
|
|
882
|
+
stateStore.appendLog(`New ${options.adapter} session requested by owner.`);
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
801
885
|
case "stop": {
|
|
802
886
|
const interrupted = await adapter.interrupt();
|
|
803
887
|
await queueWechatMessage(message.senderId, interrupted
|