cli-wechat-bridge 1.0.9 → 1.1.1
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 +30 -0
- package/dist/bridge/bridge-adapters.claude.js +186 -5
- package/dist/bridge/bridge-adapters.codex.js +58 -24
- package/dist/bridge/bridge-adapters.core.js +39 -5
- package/dist/bridge/bridge-adapters.opencode.js +6 -2
- package/dist/bridge/bridge-adapters.shared.js +104 -1
- package/dist/bridge/bridge-adapters.shell.js +1 -1
- package/dist/bridge/bridge-utils.js +36 -12
- package/dist/bridge/claude-hooks.js +9 -16
- package/dist/bridge/wechat-bridge.js +64 -10
- package/dist/companion/local-companion.js +1 -0
- package/dist/daemon/emoji-bindings.js +31 -0
- package/dist/daemon/wechat-daemon.js +61 -75
- package/dist/i18n/index.js +32 -0
- package/dist/i18n/messages-en.js +125 -0
- package/dist/i18n/messages-zh.js +125 -0
- package/dist/runtime/legacy-adapter-runtime.js +3 -0
- package/dist/utils/doctor.js +540 -0
- package/dist/wechat/channel-config.js +2 -0
- package/package.json +1 -1
|
@@ -110,7 +110,7 @@ export class OpenCodeServerAdapter {
|
|
|
110
110
|
throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
|
|
111
111
|
}
|
|
112
112
|
if (this.pendingPermission) {
|
|
113
|
-
throw new Error("An OpenCode approval request is pending. Reply with /confirm
|
|
113
|
+
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
114
114
|
}
|
|
115
115
|
const normalized = normalizeOutput(text).trim();
|
|
116
116
|
if (!normalized) {
|
|
@@ -163,7 +163,7 @@ export class OpenCodeServerAdapter {
|
|
|
163
163
|
throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
|
|
164
164
|
}
|
|
165
165
|
if (this.pendingPermission) {
|
|
166
|
-
throw new Error("An OpenCode approval request is pending. Reply with /confirm
|
|
166
|
+
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
167
167
|
}
|
|
168
168
|
this.outputBatcher.clear();
|
|
169
169
|
this.clearStreamedPartState();
|
|
@@ -254,6 +254,10 @@ export class OpenCodeServerAdapter {
|
|
|
254
254
|
this.setStatus("busy");
|
|
255
255
|
return true;
|
|
256
256
|
}
|
|
257
|
+
async resolveAllApprovals(action) {
|
|
258
|
+
const ok = await this.resolveApproval(action);
|
|
259
|
+
return ok ? 1 : 0;
|
|
260
|
+
}
|
|
257
261
|
async submitUserInput(_answers) {
|
|
258
262
|
return false;
|
|
259
263
|
}
|
|
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
6
7
|
import { spawn as spawnPty } from "node-pty";
|
|
7
8
|
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
|
|
8
9
|
import { getNotificationThreadId, isRecord, } from "./bridge-adapter-common.js";
|
|
@@ -1214,3 +1215,105 @@ export function resolveSpawnTarget(command, kind, options = {}) {
|
|
|
1214
1215
|
}
|
|
1215
1216
|
return { file: resolved, args: [...forwardArgs] };
|
|
1216
1217
|
}
|
|
1218
|
+
export function spawnFallbackProcess(file, args, options) {
|
|
1219
|
+
const child = spawn(file, args, {
|
|
1220
|
+
cwd: options.cwd,
|
|
1221
|
+
env: { ...options.env, TERM: "dumb" },
|
|
1222
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1223
|
+
windowsHide: false,
|
|
1224
|
+
shell: false,
|
|
1225
|
+
});
|
|
1226
|
+
if (!child.pid) {
|
|
1227
|
+
throw new Error(`Failed to spawn fallback process: ${file}`);
|
|
1228
|
+
}
|
|
1229
|
+
const dataListeners = [];
|
|
1230
|
+
const exitListeners = [];
|
|
1231
|
+
child.stdout?.on("data", (chunk) => {
|
|
1232
|
+
const text = chunk.toString("utf8");
|
|
1233
|
+
for (const listener of dataListeners) {
|
|
1234
|
+
listener(text);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
child.stderr?.on("data", (chunk) => {
|
|
1238
|
+
const text = chunk.toString("utf8");
|
|
1239
|
+
for (const listener of dataListeners) {
|
|
1240
|
+
listener(text);
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
child.on("exit", (code, signal) => {
|
|
1244
|
+
const exitCode = code ?? (signal ? 128 : 1);
|
|
1245
|
+
for (const listener of exitListeners) {
|
|
1246
|
+
listener({ exitCode });
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
return {
|
|
1250
|
+
pid: child.pid,
|
|
1251
|
+
write(data) {
|
|
1252
|
+
child.stdin?.write(data);
|
|
1253
|
+
},
|
|
1254
|
+
kill(signal) {
|
|
1255
|
+
try {
|
|
1256
|
+
if (process.platform === "win32") {
|
|
1257
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
1258
|
+
stdio: "ignore",
|
|
1259
|
+
windowsHide: true,
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
child.kill(signal);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
// Best effort.
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
onData(callback) {
|
|
1271
|
+
dataListeners.push(callback);
|
|
1272
|
+
return {
|
|
1273
|
+
dispose() {
|
|
1274
|
+
const index = dataListeners.indexOf(callback);
|
|
1275
|
+
if (index >= 0)
|
|
1276
|
+
dataListeners.splice(index, 1);
|
|
1277
|
+
},
|
|
1278
|
+
};
|
|
1279
|
+
},
|
|
1280
|
+
onExit(callback) {
|
|
1281
|
+
exitListeners.push(callback);
|
|
1282
|
+
return {
|
|
1283
|
+
dispose() {
|
|
1284
|
+
const index = exitListeners.indexOf(callback);
|
|
1285
|
+
if (index >= 0)
|
|
1286
|
+
exitListeners.splice(index, 1);
|
|
1287
|
+
},
|
|
1288
|
+
};
|
|
1289
|
+
},
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
export function buildSpawnDiagnostic(error, spawnTarget, platform = process.platform) {
|
|
1293
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1294
|
+
const target = spawnTarget ? spawnTarget.file : "(unknown)";
|
|
1295
|
+
const lines = [
|
|
1296
|
+
t("spawn.diagnostic.title", { target, error: errorMessage }),
|
|
1297
|
+
t("spawn.diagnostic.fixesHeader"),
|
|
1298
|
+
];
|
|
1299
|
+
if (errorMessage.includes("posix_spawnp") || errorMessage.includes("node-pty")) {
|
|
1300
|
+
lines.push(t("spawn.diagnostic.nodePty"));
|
|
1301
|
+
if (platform === "darwin") {
|
|
1302
|
+
lines.push(t("spawn.diagnostic.xcode"));
|
|
1303
|
+
}
|
|
1304
|
+
if (platform === "linux") {
|
|
1305
|
+
lines.push(t("spawn.diagnostic.linuxBuildTools"));
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
else if (errorMessage.includes("ENOENT") || errorMessage.includes("spawn")) {
|
|
1309
|
+
lines.push(t("spawn.diagnostic.notFound", { target }));
|
|
1310
|
+
}
|
|
1311
|
+
else {
|
|
1312
|
+
lines.push(t("spawn.diagnostic.generic"));
|
|
1313
|
+
}
|
|
1314
|
+
lines.push(t("spawn.diagnostic.nodeVersion"));
|
|
1315
|
+
if (platform === "win32") {
|
|
1316
|
+
lines.push(t("spawn.diagnostic.winFull"));
|
|
1317
|
+
}
|
|
1318
|
+
return lines.join("\n");
|
|
1319
|
+
}
|
|
@@ -67,7 +67,7 @@ export class ShellAdapter extends AbstractPtyAdapter {
|
|
|
67
67
|
throw new Error("shell is still working. Wait for the current reply or use /stop.");
|
|
68
68
|
}
|
|
69
69
|
if (this.pendingApproval || this.state.status === "awaiting_approval") {
|
|
70
|
-
throw new Error("A shell approval request is pending. Reply with /confirm
|
|
70
|
+
throw new Error("A shell approval request is pending. Reply with /confirm or /deny.");
|
|
71
71
|
}
|
|
72
72
|
const rejectionMessage = getInteractiveShellCommandRejectionMessage(text);
|
|
73
73
|
if (rejectionMessage) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
const ANSI_ESCAPE_RE =
|
|
@@ -122,6 +123,34 @@ export function truncatePreview(text, maxLength = 140) {
|
|
|
122
123
|
}
|
|
123
124
|
return `${normalized.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
124
125
|
}
|
|
126
|
+
export function isThinkingForwardEnabled() {
|
|
127
|
+
if (process.env.CLI_BRIDGE_THINKING_FORWARD === "1") {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const accountPath = process.env.CLI_BRIDGE_DATA_DIR
|
|
132
|
+
? path.join(process.env.CLI_BRIDGE_DATA_DIR, "account.json")
|
|
133
|
+
: path.join(os.homedir(), ".cli-bridge", "account.json");
|
|
134
|
+
if (!fs.existsSync(accountPath))
|
|
135
|
+
return false;
|
|
136
|
+
const raw = fs.readFileSync(accountPath, "utf8");
|
|
137
|
+
const data = JSON.parse(raw);
|
|
138
|
+
return data.enableThinkingForward === true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function formatThinkingForWechat(text, maxLength = 500) {
|
|
145
|
+
const normalized = normalizeOutput(text).trim();
|
|
146
|
+
if (!normalized) {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
if (normalized.length <= maxLength) {
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
153
|
+
}
|
|
125
154
|
export function buildOneTimeCode(length = 6) {
|
|
126
155
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
127
156
|
let code = "";
|
|
@@ -157,8 +186,10 @@ export function parseSystemCommand(text) {
|
|
|
157
186
|
case "/reset":
|
|
158
187
|
return { type: "reset" };
|
|
159
188
|
case "/confirm":
|
|
160
|
-
|
|
189
|
+
case "/yes":
|
|
190
|
+
return { type: "confirm" };
|
|
161
191
|
case "/deny":
|
|
192
|
+
case "/no":
|
|
162
193
|
return { type: "deny" };
|
|
163
194
|
case "/answer":
|
|
164
195
|
return argument ? { type: "answer", raw: argument } : null;
|
|
@@ -171,19 +202,13 @@ export function parseWechatControlCommand(text, options) {
|
|
|
171
202
|
if (systemCommand) {
|
|
172
203
|
return systemCommand;
|
|
173
204
|
}
|
|
174
|
-
if (options.
|
|
205
|
+
if (!options.hasPendingConfirmation) {
|
|
175
206
|
return null;
|
|
176
207
|
}
|
|
177
208
|
const normalized = text.trim().toLowerCase();
|
|
178
209
|
if (!normalized) {
|
|
179
210
|
return null;
|
|
180
211
|
}
|
|
181
|
-
if (normalized === "/confirm") {
|
|
182
|
-
return { type: "confirm" };
|
|
183
|
-
}
|
|
184
|
-
if (!options.hasPendingConfirmation) {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
212
|
switch (normalized) {
|
|
188
213
|
case "confirm":
|
|
189
214
|
case "yes":
|
|
@@ -385,7 +410,7 @@ const HIGH_RISK_PATTERNS = [
|
|
|
385
410
|
/\brmdir\b/i,
|
|
386
411
|
/\bdel\b/i,
|
|
387
412
|
/\berase\b/i,
|
|
388
|
-
/\bformat\b/i,
|
|
413
|
+
/\bformat\b(?!-)/i,
|
|
389
414
|
/\bshutdown\b/i,
|
|
390
415
|
/\bstop-computer\b/i,
|
|
391
416
|
/\brestart-computer\b/i,
|
|
@@ -997,10 +1022,9 @@ export function formatApprovalMessage(pending, adapterState) {
|
|
|
997
1022
|
return [
|
|
998
1023
|
`${pending.source === "shell" ? "Shell" : "CLI"} approval is required.`,
|
|
999
1024
|
`adapter: ${adapterState.kind}`,
|
|
1000
|
-
`code: ${pending.code}`,
|
|
1001
1025
|
`summary: ${pending.summary}`,
|
|
1002
1026
|
`target: ${pending.commandPreview}`,
|
|
1003
|
-
"Reply with /confirm
|
|
1027
|
+
"Reply with /confirm or /yes to continue, /deny or /no to reject.",
|
|
1004
1028
|
].join("\n");
|
|
1005
1029
|
}
|
|
1006
1030
|
export function formatPendingApprovalReminder(pending, adapterState) {
|
|
@@ -1010,7 +1034,7 @@ export function formatPendingApprovalReminder(pending, adapterState) {
|
|
|
1010
1034
|
: pending.commandPreview;
|
|
1011
1035
|
return `Approval is pending for ${truncatePreview(target, 140)}. Reply with /confirm, confirm, or yes to continue, or /deny, deny, or no to reject.`;
|
|
1012
1036
|
}
|
|
1013
|
-
return `Approval is pending for ${pending.commandPreview}. Reply with /confirm
|
|
1037
|
+
return `Approval is pending for ${pending.commandPreview}. Reply with /confirm or /deny.`;
|
|
1014
1038
|
}
|
|
1015
1039
|
function formatUserInputQuestionLabel(question, index) {
|
|
1016
1040
|
return `${index + 1}. ${question.header} [id: ${question.id}]`;
|
|
@@ -6,14 +6,6 @@ const FILE_MUTATION_TOOL_NAMES = new Set([
|
|
|
6
6
|
"MultiEdit",
|
|
7
7
|
"NotebookEdit",
|
|
8
8
|
]);
|
|
9
|
-
const LOW_RISK_READ_TOOL_NAMES = new Set([
|
|
10
|
-
"Glob",
|
|
11
|
-
"Grep",
|
|
12
|
-
"LS",
|
|
13
|
-
"NotebookRead",
|
|
14
|
-
"Read",
|
|
15
|
-
"TodoRead",
|
|
16
|
-
]);
|
|
17
9
|
function quoteWindowsCommandArg(value) {
|
|
18
10
|
return `"${value.replace(/"/g, '""')}"`;
|
|
19
11
|
}
|
|
@@ -90,13 +82,15 @@ export function buildClaudeHookScript(params) {
|
|
|
90
82
|
params.runtimeExecPath,
|
|
91
83
|
...runtimeArgs,
|
|
92
84
|
].map(quoteWindowsCommandArg).join(" ");
|
|
85
|
+
const stderrRedirect = params.hookErrorLogPath
|
|
86
|
+
? `2>>${quoteWindowsCommandArg(params.hookErrorLogPath)}`
|
|
87
|
+
: "2>nul";
|
|
93
88
|
return [
|
|
94
89
|
"@echo off",
|
|
95
90
|
"setlocal",
|
|
96
91
|
`set "CLAUDE_WECHAT_HOOK_PORT=${params.hookPort}"`,
|
|
97
92
|
`set "CLAUDE_WECHAT_HOOK_TOKEN=${params.hookToken}"`,
|
|
98
|
-
|
|
99
|
-
`${command} 2>nul`,
|
|
93
|
+
`${command} ${stderrRedirect}`,
|
|
100
94
|
"exit /b 0",
|
|
101
95
|
].join("\r\n");
|
|
102
96
|
}
|
|
@@ -104,12 +98,14 @@ export function buildClaudeHookScript(params) {
|
|
|
104
98
|
params.runtimeExecPath,
|
|
105
99
|
...runtimeArgs,
|
|
106
100
|
].map(quotePosixCommandArg).join(" ");
|
|
101
|
+
const stderrRedirect = params.hookErrorLogPath
|
|
102
|
+
? `2>>${quotePosixCommandArg(params.hookErrorLogPath)}`
|
|
103
|
+
: "2>/dev/null";
|
|
107
104
|
return [
|
|
108
105
|
"#!/bin/sh",
|
|
109
106
|
`export CLAUDE_WECHAT_HOOK_PORT=${quotePosixCommandArg(String(params.hookPort))}`,
|
|
110
107
|
`export CLAUDE_WECHAT_HOOK_TOKEN=${quotePosixCommandArg(params.hookToken)}`,
|
|
111
|
-
|
|
112
|
-
`${command} 2>/dev/null || true`,
|
|
108
|
+
`${command} ${stderrRedirect} || true`,
|
|
113
109
|
"exit 0",
|
|
114
110
|
].join("\n");
|
|
115
111
|
}
|
|
@@ -214,15 +210,12 @@ export function getClaudePermissionAutoResponse(payload) {
|
|
|
214
210
|
if (!toolName) {
|
|
215
211
|
return null;
|
|
216
212
|
}
|
|
217
|
-
if (
|
|
213
|
+
if (toolName !== "Bash") {
|
|
218
214
|
return {
|
|
219
215
|
action: "confirm",
|
|
220
216
|
reason: `low-risk ${toolName} permission`,
|
|
221
217
|
};
|
|
222
218
|
}
|
|
223
|
-
if (toolName !== "Bash") {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
219
|
const command = getClaudeToolInputString(payload, "command").trim();
|
|
227
220
|
if (!command || isHighRiskShellCommand(command)) {
|
|
228
221
|
return null;
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
|
|
4
4
|
import { delay } from "./bridge-adapters.shared.js";
|
|
5
|
+
import { t } from "../i18n/index.js";
|
|
5
6
|
import { BridgeController } from "./bridge-controller.js";
|
|
6
7
|
import { forwardWechatFinalReply } from "./bridge-final-reply.js";
|
|
7
8
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
8
9
|
import { BridgeStateStore } from "./bridge-state.js";
|
|
9
10
|
import { reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses } from "./bridge-process-reaper.js";
|
|
10
11
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
11
|
-
import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
|
|
12
|
+
import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, formatThinkingForWechat, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
|
|
12
13
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
|
|
13
14
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
14
15
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
16
|
+
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "../daemon/emoji-bindings.js";
|
|
15
17
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
16
18
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
17
19
|
const PARENT_PROCESS_POLL_MS = 5_000;
|
|
@@ -240,6 +242,11 @@ function printUsageAndExit() {
|
|
|
240
242
|
process.exit(0);
|
|
241
243
|
}
|
|
242
244
|
async function main() {
|
|
245
|
+
if (process.argv.includes("--doctor")) {
|
|
246
|
+
const { runDoctorCheck } = await import("../utils/doctor.js");
|
|
247
|
+
await runDoctorCheck(process.argv.slice(2), { mode: "bridge" });
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
243
250
|
const options = parseCliArgs(process.argv.slice(2));
|
|
244
251
|
const daemonEndpoint = readDaemonEndpoint();
|
|
245
252
|
if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
|
|
@@ -608,6 +615,13 @@ async function main() {
|
|
|
608
615
|
else if (options.adapter === "shell") {
|
|
609
616
|
log("Shell mode runs as a headless remote executor for non-interactive commands and scripts.");
|
|
610
617
|
}
|
|
618
|
+
loadEmojiBindings();
|
|
619
|
+
const welcomeText = t("bridge.welcome", {
|
|
620
|
+
adapter: options.adapter,
|
|
621
|
+
cwd: options.cwd,
|
|
622
|
+
bindings: formatBindingsListMessage(listBindings()),
|
|
623
|
+
});
|
|
624
|
+
await queueWechatMessage(credentials.userId, welcomeText);
|
|
611
625
|
while (true) {
|
|
612
626
|
if (!ensureRuntimeOwnership()) {
|
|
613
627
|
break;
|
|
@@ -792,6 +806,18 @@ function wireAdapterEvents(params) {
|
|
|
792
806
|
}));
|
|
793
807
|
}
|
|
794
808
|
break;
|
|
809
|
+
case "thinking":
|
|
810
|
+
updateLastOutputAt();
|
|
811
|
+
if (event.text) {
|
|
812
|
+
const thinkingPreview = formatThinkingForWechat(event.text, 500);
|
|
813
|
+
if (thinkingPreview) {
|
|
814
|
+
stateStore.appendLog(`thinking: ${thinkingPreview}`);
|
|
815
|
+
trackWechatForwardTask((async () => {
|
|
816
|
+
await queueWechatMessage(authorizedUserId, `思考: ${thinkingPreview}`, "thinking");
|
|
817
|
+
})());
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
break;
|
|
795
821
|
case "approval_required":
|
|
796
822
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
797
823
|
const pending = toPendingApproval(event.request);
|
|
@@ -911,17 +937,49 @@ function formatInboundMessagePreview(message) {
|
|
|
911
937
|
return "(empty)";
|
|
912
938
|
}
|
|
913
939
|
async function handleInboundMessage(params) {
|
|
914
|
-
|
|
940
|
+
let { message, } = params;
|
|
941
|
+
const { options, stateStore, adapter, queueWechatMessage, outputBatcher, deferInboundMessage, } = params;
|
|
915
942
|
const state = stateStore.getState();
|
|
943
|
+
if (message.senderId !== state.authorizedUserId) {
|
|
944
|
+
await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
// Emoji resolution: rewrite message text if it starts with a bound emoji
|
|
948
|
+
const emojiMatch = resolveEmojiCommand(message.text);
|
|
949
|
+
if (emojiMatch) {
|
|
950
|
+
const rewritten = emojiMatch.remainder
|
|
951
|
+
? `${emojiMatch.command} ${emojiMatch.remainder}`
|
|
952
|
+
: emojiMatch.command;
|
|
953
|
+
message = { ...message, text: rewritten };
|
|
954
|
+
}
|
|
955
|
+
// Emoji binding management commands
|
|
956
|
+
const bindingsCmd = parseEmojiBindingsCommand(message.text);
|
|
957
|
+
if (bindingsCmd) {
|
|
958
|
+
switch (bindingsCmd.type) {
|
|
959
|
+
case "list":
|
|
960
|
+
await queueWechatMessage(message.senderId, formatBindingsListMessage(listBindings()));
|
|
961
|
+
break;
|
|
962
|
+
case "bind":
|
|
963
|
+
setBinding(bindingsCmd.emoji, bindingsCmd.command);
|
|
964
|
+
await queueWechatMessage(message.senderId, `Bound ${bindingsCmd.emoji} → ${bindingsCmd.command}`);
|
|
965
|
+
break;
|
|
966
|
+
case "unbind": {
|
|
967
|
+
const removed = removeBinding(bindingsCmd.emoji);
|
|
968
|
+
await queueWechatMessage(message.senderId, removed ? `Unbound ${bindingsCmd.emoji}` : `No binding found for ${bindingsCmd.emoji}`);
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
if (isBindCommandPrefix(message.text)) {
|
|
975
|
+
await queueWechatMessage(message.senderId, formatBindCommandUsage());
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
916
978
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
917
979
|
adapter: options.adapter,
|
|
918
980
|
hasPendingConfirmation: Boolean(state.pendingConfirmation),
|
|
919
981
|
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
920
982
|
});
|
|
921
|
-
if (message.senderId !== state.authorizedUserId) {
|
|
922
|
-
await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
|
|
923
|
-
return null;
|
|
924
|
-
}
|
|
925
983
|
switch (systemCommand?.type) {
|
|
926
984
|
case "status":
|
|
927
985
|
await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
|
|
@@ -979,10 +1037,6 @@ async function handleInboundMessage(params) {
|
|
|
979
1037
|
await queueWechatMessage(message.senderId, "No pending approval request.");
|
|
980
1038
|
return null;
|
|
981
1039
|
}
|
|
982
|
-
if (options.adapter !== "claude" && pending.code !== systemCommand.code) {
|
|
983
|
-
await queueWechatMessage(message.senderId, "Confirmation code did not match.");
|
|
984
|
-
return null;
|
|
985
|
-
}
|
|
986
1040
|
const confirmed = await adapter.resolveApproval("confirm");
|
|
987
1041
|
if (!confirmed) {
|
|
988
1042
|
await queueWechatMessage(message.senderId, "The worker could not apply this approval request.");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { EMOJI_BINDINGS_FILE } from "../wechat/channel-config.js";
|
|
3
|
+
import { t } from "../i18n/index.js";
|
|
3
4
|
export const DEFAULT_EMOJI_BINDINGS = {
|
|
4
5
|
"[OK]": "/confirm",
|
|
5
6
|
"[闭嘴]": "/stop",
|
|
@@ -78,3 +79,33 @@ export function resolveEmojiCommand(text) {
|
|
|
78
79
|
}
|
|
79
80
|
return null;
|
|
80
81
|
}
|
|
82
|
+
export function parseEmojiBindingsCommand(text) {
|
|
83
|
+
const trimmed = text.trim();
|
|
84
|
+
const lower = trimmed.toLowerCase();
|
|
85
|
+
if (lower === "/bindings") {
|
|
86
|
+
return { type: "list" };
|
|
87
|
+
}
|
|
88
|
+
const bindMatch = trimmed.match(/^\/bind\s+(\[[^\]]+\])\s+(.+)$/i);
|
|
89
|
+
if (bindMatch) {
|
|
90
|
+
return { type: "bind", emoji: bindMatch[1], command: bindMatch[2].trim() };
|
|
91
|
+
}
|
|
92
|
+
const unbindMatch = trimmed.match(/^\/unbind\s+(\[[^\]]+\])$/i);
|
|
93
|
+
if (unbindMatch) {
|
|
94
|
+
return { type: "unbind", emoji: unbindMatch[1] };
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
export function isBindCommandPrefix(text) {
|
|
99
|
+
const lower = text.trim().toLowerCase();
|
|
100
|
+
return lower.startsWith("/bind") || lower.startsWith("/unbind");
|
|
101
|
+
}
|
|
102
|
+
export function formatBindCommandUsage() {
|
|
103
|
+
return t("binding.usage");
|
|
104
|
+
}
|
|
105
|
+
export function formatBindingsListMessage(map) {
|
|
106
|
+
if (map.size === 0) {
|
|
107
|
+
return t("binding.listEmpty");
|
|
108
|
+
}
|
|
109
|
+
const lines = Array.from(map.entries()).map(([emoji, command]) => `${emoji} → ${command}`);
|
|
110
|
+
return `${t("binding.listHeader")}\n${lines.join("\n")}`;
|
|
111
|
+
}
|