weacpx 0.1.3 → 0.1.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 +75 -10
- package/dist/bridge/bridge-main.js +75 -24
- package/dist/cli.js +2817 -252
- package/package.json +8 -3
package/dist/cli.js
CHANGED
|
@@ -1046,120 +1046,2267 @@ var require_main = __commonJS((exports, module) => {
|
|
|
1046
1046
|
};
|
|
1047
1047
|
});
|
|
1048
1048
|
|
|
1049
|
-
// src/weixin-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1049
|
+
// src/weixin/storage/state-dir.ts
|
|
1050
|
+
import os from "node:os";
|
|
1051
|
+
import path from "node:path";
|
|
1052
|
+
function resolveStateDir() {
|
|
1053
|
+
return process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
|
|
1054
|
+
}
|
|
1055
|
+
var init_state_dir = () => {};
|
|
1056
|
+
|
|
1057
|
+
// src/weixin/auth/accounts.ts
|
|
1058
|
+
import fs from "node:fs";
|
|
1059
|
+
import path2 from "node:path";
|
|
1060
|
+
function normalizeAccountId(raw) {
|
|
1061
|
+
return raw.trim().toLowerCase().replace(/[@.]/g, "-");
|
|
1062
|
+
}
|
|
1063
|
+
function deriveRawAccountId(normalizedId) {
|
|
1064
|
+
if (normalizedId.endsWith("-im-bot")) {
|
|
1065
|
+
return `${normalizedId.slice(0, -7)}@im.bot`;
|
|
1066
|
+
}
|
|
1067
|
+
if (normalizedId.endsWith("-im-wechat")) {
|
|
1068
|
+
return `${normalizedId.slice(0, -10)}@im.wechat`;
|
|
1069
|
+
}
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
function resolveWeixinStateDir() {
|
|
1073
|
+
return path2.join(resolveStateDir(), "openclaw-weixin");
|
|
1074
|
+
}
|
|
1075
|
+
function resolveAccountIndexPath() {
|
|
1076
|
+
return path2.join(resolveWeixinStateDir(), "accounts.json");
|
|
1077
|
+
}
|
|
1078
|
+
function listIndexedWeixinAccountIds() {
|
|
1079
|
+
const filePath = resolveAccountIndexPath();
|
|
1080
|
+
try {
|
|
1081
|
+
if (!fs.existsSync(filePath))
|
|
1082
|
+
return [];
|
|
1083
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1084
|
+
const parsed = JSON.parse(raw);
|
|
1085
|
+
if (!Array.isArray(parsed))
|
|
1086
|
+
return [];
|
|
1087
|
+
return parsed.filter((id) => typeof id === "string" && id.trim() !== "");
|
|
1088
|
+
} catch {
|
|
1089
|
+
return [];
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function registerWeixinAccountId(accountId) {
|
|
1093
|
+
const dir = resolveWeixinStateDir();
|
|
1094
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1095
|
+
fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify([accountId], null, 2), "utf-8");
|
|
1096
|
+
}
|
|
1097
|
+
function resolveAccountsDir() {
|
|
1098
|
+
return path2.join(resolveWeixinStateDir(), "accounts");
|
|
1099
|
+
}
|
|
1100
|
+
function resolveAccountPath(accountId) {
|
|
1101
|
+
return path2.join(resolveAccountsDir(), `${accountId}.json`);
|
|
1102
|
+
}
|
|
1103
|
+
function loadLegacyToken() {
|
|
1104
|
+
const legacyPath = path2.join(resolveStateDir(), "credentials", "openclaw-weixin", "credentials.json");
|
|
1105
|
+
try {
|
|
1106
|
+
if (!fs.existsSync(legacyPath))
|
|
1107
|
+
return;
|
|
1108
|
+
const raw = fs.readFileSync(legacyPath, "utf-8");
|
|
1109
|
+
const parsed = JSON.parse(raw);
|
|
1110
|
+
return typeof parsed.token === "string" ? parsed.token : undefined;
|
|
1111
|
+
} catch {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
function readAccountFile(filePath) {
|
|
1116
|
+
try {
|
|
1117
|
+
if (fs.existsSync(filePath)) {
|
|
1118
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1119
|
+
}
|
|
1120
|
+
} catch {}
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
function loadWeixinAccount(accountId) {
|
|
1124
|
+
const primary = readAccountFile(resolveAccountPath(accountId));
|
|
1125
|
+
if (primary)
|
|
1126
|
+
return primary;
|
|
1127
|
+
const rawId = deriveRawAccountId(accountId);
|
|
1128
|
+
if (rawId) {
|
|
1129
|
+
const compat = readAccountFile(resolveAccountPath(rawId));
|
|
1130
|
+
if (compat)
|
|
1131
|
+
return compat;
|
|
1132
|
+
}
|
|
1133
|
+
const token = loadLegacyToken();
|
|
1134
|
+
if (token)
|
|
1135
|
+
return { token };
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
function saveWeixinAccount(accountId, update) {
|
|
1139
|
+
const dir = resolveAccountsDir();
|
|
1140
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1141
|
+
const existing = loadWeixinAccount(accountId) ?? {};
|
|
1142
|
+
const token = update.token?.trim() || existing.token;
|
|
1143
|
+
const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
|
|
1144
|
+
const userId = update.userId !== undefined ? update.userId.trim() || undefined : existing.userId?.trim() || undefined;
|
|
1145
|
+
const data = {
|
|
1146
|
+
...token ? { token, savedAt: new Date().toISOString() } : {},
|
|
1147
|
+
...baseUrl ? { baseUrl } : {},
|
|
1148
|
+
...userId ? { userId } : {}
|
|
1149
|
+
};
|
|
1150
|
+
const filePath = resolveAccountPath(accountId);
|
|
1151
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
1152
|
+
try {
|
|
1153
|
+
fs.chmodSync(filePath, 384);
|
|
1154
|
+
} catch {}
|
|
1155
|
+
}
|
|
1156
|
+
function clearWeixinAccount(accountId) {
|
|
1157
|
+
try {
|
|
1158
|
+
fs.unlinkSync(resolveAccountPath(accountId));
|
|
1159
|
+
} catch {}
|
|
1160
|
+
}
|
|
1161
|
+
function clearAllWeixinAccounts() {
|
|
1162
|
+
const ids = listIndexedWeixinAccountIds();
|
|
1163
|
+
for (const id of ids) {
|
|
1164
|
+
clearWeixinAccount(id);
|
|
1165
|
+
}
|
|
1166
|
+
try {
|
|
1167
|
+
fs.writeFileSync(resolveAccountIndexPath(), "[]", "utf-8");
|
|
1168
|
+
} catch {}
|
|
1169
|
+
}
|
|
1170
|
+
function resolveConfigPath() {
|
|
1171
|
+
const envPath = process.env.OPENCLAW_CONFIG?.trim();
|
|
1172
|
+
if (envPath)
|
|
1173
|
+
return envPath;
|
|
1174
|
+
return path2.join(resolveStateDir(), "openclaw.json");
|
|
1175
|
+
}
|
|
1176
|
+
function loadConfigRouteTag(accountId) {
|
|
1177
|
+
try {
|
|
1178
|
+
const configPath = resolveConfigPath();
|
|
1179
|
+
if (!fs.existsSync(configPath))
|
|
1180
|
+
return;
|
|
1181
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
1182
|
+
const cfg = JSON.parse(raw);
|
|
1183
|
+
const channels = cfg.channels;
|
|
1184
|
+
const section = channels?.["openclaw-weixin"];
|
|
1185
|
+
if (!section)
|
|
1186
|
+
return;
|
|
1187
|
+
if (accountId) {
|
|
1188
|
+
const accounts = section.accounts;
|
|
1189
|
+
const tag = accounts?.[accountId]?.routeTag;
|
|
1190
|
+
if (typeof tag === "number")
|
|
1191
|
+
return String(tag);
|
|
1192
|
+
if (typeof tag === "string" && tag.trim())
|
|
1193
|
+
return tag.trim();
|
|
1194
|
+
}
|
|
1195
|
+
if (typeof section.routeTag === "number")
|
|
1196
|
+
return String(section.routeTag);
|
|
1197
|
+
return typeof section.routeTag === "string" && section.routeTag.trim() ? section.routeTag.trim() : undefined;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function listWeixinAccountIds() {
|
|
1203
|
+
return listIndexedWeixinAccountIds();
|
|
1204
|
+
}
|
|
1205
|
+
function resolveWeixinAccount(accountId) {
|
|
1206
|
+
const raw = accountId?.trim();
|
|
1207
|
+
if (!raw) {
|
|
1208
|
+
throw new Error("weixin: accountId is required (no default account)");
|
|
1209
|
+
}
|
|
1210
|
+
const id = normalizeAccountId(raw);
|
|
1211
|
+
const accountData = loadWeixinAccount(id);
|
|
1212
|
+
const token = accountData?.token?.trim() || undefined;
|
|
1213
|
+
const stateBaseUrl = accountData?.baseUrl?.trim() || "";
|
|
1214
|
+
return {
|
|
1215
|
+
accountId: id,
|
|
1216
|
+
baseUrl: stateBaseUrl || DEFAULT_BASE_URL,
|
|
1217
|
+
cdnBaseUrl: CDN_BASE_URL,
|
|
1218
|
+
token,
|
|
1219
|
+
enabled: true,
|
|
1220
|
+
configured: Boolean(token)
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com", CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
1224
|
+
var init_accounts = __esm(() => {
|
|
1225
|
+
init_state_dir();
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// src/weixin/util/logger.ts
|
|
1229
|
+
import fs2 from "node:fs";
|
|
1230
|
+
import os2 from "node:os";
|
|
1231
|
+
import path3 from "node:path";
|
|
1232
|
+
function resolveMinLevel() {
|
|
1233
|
+
const env = process.env.OPENCLAW_LOG_LEVEL?.toUpperCase();
|
|
1234
|
+
if (env && env in LEVEL_IDS)
|
|
1235
|
+
return LEVEL_IDS[env] ?? LEVEL_IDS[DEFAULT_LOG_LEVEL] ?? 3;
|
|
1236
|
+
return LEVEL_IDS[DEFAULT_LOG_LEVEL] ?? 3;
|
|
1237
|
+
}
|
|
1238
|
+
function toLocalISO(now) {
|
|
1239
|
+
const offsetMs = -now.getTimezoneOffset() * 60000;
|
|
1240
|
+
const sign = offsetMs >= 0 ? "+" : "-";
|
|
1241
|
+
const abs = Math.abs(now.getTimezoneOffset());
|
|
1242
|
+
const offStr = `${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
|
|
1243
|
+
return new Date(now.getTime() + offsetMs).toISOString().replace("Z", offStr);
|
|
1244
|
+
}
|
|
1245
|
+
function localDateKey(now) {
|
|
1246
|
+
return toLocalISO(now).slice(0, 10);
|
|
1247
|
+
}
|
|
1248
|
+
function resolveMainLogPath() {
|
|
1249
|
+
const dateKey = localDateKey(new Date);
|
|
1250
|
+
return path3.join(MAIN_LOG_DIR, `openclaw-${dateKey}.log`);
|
|
1251
|
+
}
|
|
1252
|
+
function buildLoggerName(accountId) {
|
|
1253
|
+
return accountId ? `${SUBSYSTEM}/${accountId}` : SUBSYSTEM;
|
|
1254
|
+
}
|
|
1255
|
+
function writeLog(level, message, accountId) {
|
|
1256
|
+
const levelId = LEVEL_IDS[level] ?? LEVEL_IDS.INFO ?? 3;
|
|
1257
|
+
if (levelId < minLevelId)
|
|
1258
|
+
return;
|
|
1259
|
+
const now = new Date;
|
|
1260
|
+
const loggerName = buildLoggerName(accountId);
|
|
1261
|
+
const prefixedMessage = accountId ? `[${accountId}] ${message}` : message;
|
|
1262
|
+
const entry = JSON.stringify({
|
|
1263
|
+
"0": loggerName,
|
|
1264
|
+
"1": prefixedMessage,
|
|
1265
|
+
_meta: {
|
|
1266
|
+
runtime: RUNTIME,
|
|
1267
|
+
runtimeVersion: RUNTIME_VERSION,
|
|
1268
|
+
hostname: HOSTNAME,
|
|
1269
|
+
name: loggerName,
|
|
1270
|
+
parentNames: PARENT_NAMES,
|
|
1271
|
+
date: now.toISOString(),
|
|
1272
|
+
logLevelId: levelId,
|
|
1273
|
+
logLevelName: level
|
|
1274
|
+
},
|
|
1275
|
+
time: toLocalISO(now)
|
|
1276
|
+
});
|
|
1277
|
+
try {
|
|
1278
|
+
if (!logDirEnsured) {
|
|
1279
|
+
fs2.mkdirSync(MAIN_LOG_DIR, { recursive: true });
|
|
1280
|
+
logDirEnsured = true;
|
|
1281
|
+
}
|
|
1282
|
+
fs2.appendFileSync(resolveMainLogPath(), `${entry}
|
|
1283
|
+
`, "utf-8");
|
|
1284
|
+
} catch {}
|
|
1285
|
+
}
|
|
1286
|
+
function createLogger(accountId) {
|
|
1287
|
+
return {
|
|
1288
|
+
info(message) {
|
|
1289
|
+
writeLog("INFO", message, accountId);
|
|
1290
|
+
},
|
|
1291
|
+
debug(message) {
|
|
1292
|
+
writeLog("DEBUG", message, accountId);
|
|
1293
|
+
},
|
|
1294
|
+
warn(message) {
|
|
1295
|
+
writeLog("WARN", message, accountId);
|
|
1296
|
+
},
|
|
1297
|
+
error(message) {
|
|
1298
|
+
writeLog("ERROR", message, accountId);
|
|
1299
|
+
},
|
|
1300
|
+
withAccount(id) {
|
|
1301
|
+
return createLogger(id);
|
|
1302
|
+
},
|
|
1303
|
+
getLogFilePath() {
|
|
1304
|
+
return resolveMainLogPath();
|
|
1305
|
+
},
|
|
1306
|
+
close() {}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
var MAIN_LOG_DIR, SUBSYSTEM = "gateway/channels/openclaw-weixin", RUNTIME = "node", RUNTIME_VERSION, HOSTNAME, PARENT_NAMES, LEVEL_IDS, DEFAULT_LOG_LEVEL = "INFO", minLevelId, logDirEnsured = false, logger;
|
|
1310
|
+
var init_logger = __esm(() => {
|
|
1311
|
+
MAIN_LOG_DIR = path3.join("/tmp", "openclaw");
|
|
1312
|
+
RUNTIME_VERSION = process.versions.node;
|
|
1313
|
+
HOSTNAME = os2.hostname() || "unknown";
|
|
1314
|
+
PARENT_NAMES = ["openclaw"];
|
|
1315
|
+
LEVEL_IDS = {
|
|
1316
|
+
TRACE: 1,
|
|
1317
|
+
DEBUG: 2,
|
|
1318
|
+
INFO: 3,
|
|
1319
|
+
WARN: 4,
|
|
1320
|
+
ERROR: 5,
|
|
1321
|
+
FATAL: 6
|
|
1322
|
+
};
|
|
1323
|
+
minLevelId = resolveMinLevel();
|
|
1324
|
+
logger = createLogger();
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
// src/weixin/util/redact.ts
|
|
1328
|
+
function truncate(s, max) {
|
|
1329
|
+
if (!s)
|
|
1330
|
+
return "";
|
|
1331
|
+
if (s.length <= max)
|
|
1332
|
+
return s;
|
|
1333
|
+
return `${s.slice(0, max)}…(len=${s.length})`;
|
|
1334
|
+
}
|
|
1335
|
+
function redactToken(token, prefixLen = DEFAULT_TOKEN_PREFIX_LEN) {
|
|
1336
|
+
if (!token)
|
|
1337
|
+
return "(none)";
|
|
1338
|
+
if (token.length <= prefixLen)
|
|
1339
|
+
return `****(len=${token.length})`;
|
|
1340
|
+
return `${token.slice(0, prefixLen)}…(len=${token.length})`;
|
|
1341
|
+
}
|
|
1342
|
+
function redactBody(body, maxLen = DEFAULT_BODY_MAX_LEN) {
|
|
1343
|
+
if (!body)
|
|
1344
|
+
return "(empty)";
|
|
1345
|
+
if (body.length <= maxLen)
|
|
1346
|
+
return body;
|
|
1347
|
+
return `${body.slice(0, maxLen)}…(truncated, totalLen=${body.length})`;
|
|
1348
|
+
}
|
|
1349
|
+
function redactUrl(rawUrl) {
|
|
1350
|
+
try {
|
|
1351
|
+
const u = new URL(rawUrl);
|
|
1352
|
+
const base = `${u.origin}${u.pathname}`;
|
|
1353
|
+
return u.search ? `${base}?<redacted>` : base;
|
|
1354
|
+
} catch {
|
|
1355
|
+
return truncate(rawUrl, 80);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
var DEFAULT_BODY_MAX_LEN = 200, DEFAULT_TOKEN_PREFIX_LEN = 6;
|
|
1359
|
+
|
|
1360
|
+
// src/weixin/api/api.ts
|
|
1361
|
+
import crypto from "node:crypto";
|
|
1362
|
+
import fs3 from "node:fs";
|
|
1363
|
+
import path4 from "node:path";
|
|
1364
|
+
import { fileURLToPath } from "node:url";
|
|
1365
|
+
function readChannelVersion() {
|
|
1366
|
+
try {
|
|
1367
|
+
const dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1368
|
+
const pkgPath = path4.resolve(dir, "..", "..", "package.json");
|
|
1369
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
1370
|
+
return pkg.version ?? "unknown";
|
|
1371
|
+
} catch {
|
|
1372
|
+
return "unknown";
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function buildBaseInfo() {
|
|
1376
|
+
return { channel_version: CHANNEL_VERSION };
|
|
1377
|
+
}
|
|
1378
|
+
function ensureTrailingSlash(url) {
|
|
1379
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
1380
|
+
}
|
|
1381
|
+
function randomWechatUin() {
|
|
1382
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
1383
|
+
return Buffer.from(String(uint32), "utf-8").toString("base64");
|
|
1384
|
+
}
|
|
1385
|
+
function buildCommonHeaders() {
|
|
1386
|
+
const headers = {};
|
|
1387
|
+
const routeTag = loadConfigRouteTag();
|
|
1388
|
+
if (routeTag) {
|
|
1389
|
+
headers.SKRouteTag = routeTag;
|
|
1390
|
+
}
|
|
1391
|
+
return headers;
|
|
1392
|
+
}
|
|
1393
|
+
function buildHeaders(opts) {
|
|
1394
|
+
const headers = {
|
|
1395
|
+
"Content-Type": "application/json",
|
|
1396
|
+
AuthorizationType: "ilink_bot_token",
|
|
1397
|
+
"Content-Length": String(Buffer.byteLength(opts.body, "utf-8")),
|
|
1398
|
+
"X-WECHAT-UIN": randomWechatUin(),
|
|
1399
|
+
...buildCommonHeaders()
|
|
1400
|
+
};
|
|
1401
|
+
if (opts.token?.trim()) {
|
|
1402
|
+
headers.Authorization = `Bearer ${opts.token.trim()}`;
|
|
1403
|
+
}
|
|
1404
|
+
logger.debug(`requestHeaders: ${JSON.stringify({ ...headers, Authorization: headers.Authorization ? "Bearer ***" : undefined })}`);
|
|
1405
|
+
return headers;
|
|
1406
|
+
}
|
|
1407
|
+
async function apiGetFetch(params) {
|
|
1408
|
+
const base = ensureTrailingSlash(params.baseUrl);
|
|
1409
|
+
const url = new URL(params.endpoint, base);
|
|
1410
|
+
const hdrs = buildCommonHeaders();
|
|
1411
|
+
logger.debug(`GET ${redactUrl(url.toString())}`);
|
|
1412
|
+
const controller = new AbortController;
|
|
1413
|
+
const t = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
1414
|
+
try {
|
|
1415
|
+
const res = await fetch(url.toString(), {
|
|
1416
|
+
method: "GET",
|
|
1417
|
+
headers: hdrs,
|
|
1418
|
+
signal: controller.signal
|
|
1419
|
+
});
|
|
1420
|
+
clearTimeout(t);
|
|
1421
|
+
const rawText = await res.text();
|
|
1422
|
+
logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
|
|
1423
|
+
if (!res.ok) {
|
|
1424
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
1425
|
+
}
|
|
1426
|
+
return rawText;
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
clearTimeout(t);
|
|
1429
|
+
throw err;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
async function apiFetch(params) {
|
|
1433
|
+
const base = ensureTrailingSlash(params.baseUrl);
|
|
1434
|
+
const url = new URL(params.endpoint, base);
|
|
1435
|
+
const hdrs = buildHeaders({ token: params.token, body: params.body });
|
|
1436
|
+
logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
|
|
1437
|
+
const controller = new AbortController;
|
|
1438
|
+
const t = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
1439
|
+
const onAbort = () => controller.abort();
|
|
1440
|
+
params.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1441
|
+
try {
|
|
1442
|
+
const res = await fetch(url.toString(), {
|
|
1443
|
+
method: "POST",
|
|
1444
|
+
headers: hdrs,
|
|
1445
|
+
body: params.body,
|
|
1446
|
+
signal: controller.signal
|
|
1447
|
+
});
|
|
1448
|
+
clearTimeout(t);
|
|
1449
|
+
const rawText = await res.text();
|
|
1450
|
+
logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
|
|
1451
|
+
if (!res.ok) {
|
|
1452
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
1453
|
+
}
|
|
1454
|
+
return rawText;
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
clearTimeout(t);
|
|
1457
|
+
throw err;
|
|
1458
|
+
} finally {
|
|
1459
|
+
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
async function getUpdates(params) {
|
|
1463
|
+
const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
1464
|
+
try {
|
|
1465
|
+
const rawText = await apiFetch({
|
|
1466
|
+
baseUrl: params.baseUrl,
|
|
1467
|
+
endpoint: "ilink/bot/getupdates",
|
|
1468
|
+
body: JSON.stringify({
|
|
1469
|
+
get_updates_buf: params.get_updates_buf ?? "",
|
|
1470
|
+
base_info: buildBaseInfo()
|
|
1471
|
+
}),
|
|
1472
|
+
token: params.token,
|
|
1473
|
+
timeoutMs: timeout,
|
|
1474
|
+
label: "getUpdates",
|
|
1475
|
+
abortSignal: params.abortSignal
|
|
1476
|
+
});
|
|
1477
|
+
const resp = JSON.parse(rawText);
|
|
1478
|
+
return resp;
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1481
|
+
logger.debug(`getUpdates: client-side timeout after ${timeout}ms, returning empty response`);
|
|
1482
|
+
return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
|
|
1483
|
+
}
|
|
1484
|
+
throw err;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async function getUploadUrl(params) {
|
|
1488
|
+
const rawText = await apiFetch({
|
|
1489
|
+
baseUrl: params.baseUrl,
|
|
1490
|
+
endpoint: "ilink/bot/getuploadurl",
|
|
1491
|
+
body: JSON.stringify({
|
|
1492
|
+
filekey: params.filekey,
|
|
1493
|
+
media_type: params.media_type,
|
|
1494
|
+
to_user_id: params.to_user_id,
|
|
1495
|
+
rawsize: params.rawsize,
|
|
1496
|
+
rawfilemd5: params.rawfilemd5,
|
|
1497
|
+
filesize: params.filesize,
|
|
1498
|
+
thumb_rawsize: params.thumb_rawsize,
|
|
1499
|
+
thumb_rawfilemd5: params.thumb_rawfilemd5,
|
|
1500
|
+
thumb_filesize: params.thumb_filesize,
|
|
1501
|
+
no_need_thumb: params.no_need_thumb,
|
|
1502
|
+
aeskey: params.aeskey,
|
|
1503
|
+
base_info: buildBaseInfo()
|
|
1504
|
+
}),
|
|
1505
|
+
token: params.token,
|
|
1506
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
1507
|
+
label: "getUploadUrl"
|
|
1508
|
+
});
|
|
1509
|
+
const resp = JSON.parse(rawText);
|
|
1510
|
+
return resp;
|
|
1511
|
+
}
|
|
1512
|
+
async function sendMessage(params) {
|
|
1513
|
+
await apiFetch({
|
|
1514
|
+
baseUrl: params.baseUrl,
|
|
1515
|
+
endpoint: "ilink/bot/sendmessage",
|
|
1516
|
+
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
|
|
1517
|
+
token: params.token,
|
|
1518
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
1519
|
+
label: "sendMessage"
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
async function getConfig(params) {
|
|
1523
|
+
const rawText = await apiFetch({
|
|
1524
|
+
baseUrl: params.baseUrl,
|
|
1525
|
+
endpoint: "ilink/bot/getconfig",
|
|
1526
|
+
body: JSON.stringify({
|
|
1527
|
+
ilink_user_id: params.ilinkUserId,
|
|
1528
|
+
context_token: params.contextToken,
|
|
1529
|
+
base_info: buildBaseInfo()
|
|
1530
|
+
}),
|
|
1531
|
+
token: params.token,
|
|
1532
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
|
|
1533
|
+
label: "getConfig"
|
|
1534
|
+
});
|
|
1535
|
+
const resp = JSON.parse(rawText);
|
|
1536
|
+
return resp;
|
|
1537
|
+
}
|
|
1538
|
+
async function sendTyping(params) {
|
|
1539
|
+
await apiFetch({
|
|
1540
|
+
baseUrl: params.baseUrl,
|
|
1541
|
+
endpoint: "ilink/bot/sendtyping",
|
|
1542
|
+
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
|
|
1543
|
+
token: params.token,
|
|
1544
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
|
|
1545
|
+
label: "sendTyping"
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
var CHANNEL_VERSION, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
|
|
1549
|
+
var init_api = __esm(() => {
|
|
1550
|
+
init_accounts();
|
|
1551
|
+
init_logger();
|
|
1552
|
+
CHANNEL_VERSION = readChannelVersion();
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
// src/weixin/auth/login-qr.ts
|
|
1556
|
+
import { randomUUID } from "node:crypto";
|
|
1557
|
+
function isLoginFresh(login) {
|
|
1558
|
+
return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
|
|
1559
|
+
}
|
|
1560
|
+
function purgeExpiredLogins() {
|
|
1561
|
+
for (const [id, login] of activeLogins) {
|
|
1562
|
+
if (!isLoginFresh(login)) {
|
|
1563
|
+
activeLogins.delete(id);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async function fetchQRCode(apiBaseUrl, botType) {
|
|
1568
|
+
logger.info(`Fetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
|
|
1569
|
+
const rawText = await apiGetFetch({
|
|
1570
|
+
baseUrl: apiBaseUrl,
|
|
1571
|
+
endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
|
|
1572
|
+
timeoutMs: GET_QRCODE_TIMEOUT_MS,
|
|
1573
|
+
label: "fetchQRCode"
|
|
1574
|
+
});
|
|
1575
|
+
return JSON.parse(rawText);
|
|
1576
|
+
}
|
|
1577
|
+
async function pollQRStatus(apiBaseUrl, qrcode) {
|
|
1578
|
+
logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=***`);
|
|
1579
|
+
try {
|
|
1580
|
+
const rawText = await apiGetFetch({
|
|
1581
|
+
baseUrl: apiBaseUrl,
|
|
1582
|
+
endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
|
|
1583
|
+
timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
|
|
1584
|
+
label: "pollQRStatus"
|
|
1585
|
+
});
|
|
1586
|
+
logger.debug(`pollQRStatus: body=${rawText.substring(0, 200)}`);
|
|
1587
|
+
return JSON.parse(rawText);
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1590
|
+
logger.debug(`pollQRStatus: client-side timeout after ${QR_LONG_POLL_TIMEOUT_MS}ms, returning wait`);
|
|
1591
|
+
return { status: "wait" };
|
|
1592
|
+
}
|
|
1593
|
+
logger.warn(`pollQRStatus: network/gateway error, will retry: ${String(err)}`);
|
|
1594
|
+
return { status: "wait" };
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
async function startWeixinLoginWithQr(opts) {
|
|
1598
|
+
const sessionKey = opts.accountId || randomUUID();
|
|
1599
|
+
purgeExpiredLogins();
|
|
1600
|
+
const existing = activeLogins.get(sessionKey);
|
|
1601
|
+
if (!opts.force && existing && isLoginFresh(existing) && existing.qrcodeUrl) {
|
|
1602
|
+
return {
|
|
1603
|
+
qrcodeUrl: existing.qrcodeUrl,
|
|
1604
|
+
message: "二维码已就绪,请使用微信扫描。",
|
|
1605
|
+
sessionKey
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
try {
|
|
1609
|
+
const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
|
|
1610
|
+
logger.info(`Starting Weixin login with bot_type=${botType}`);
|
|
1611
|
+
const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
|
|
1612
|
+
logger.info(`QR code received, qrcode=${redactToken(qrResponse.qrcode)} imgContentLen=${qrResponse.qrcode_img_content?.length ?? 0}`);
|
|
1613
|
+
logger.info(`二维码链接: ${qrResponse.qrcode_img_content}`);
|
|
1614
|
+
const login = {
|
|
1615
|
+
sessionKey,
|
|
1616
|
+
id: randomUUID(),
|
|
1617
|
+
qrcode: qrResponse.qrcode,
|
|
1618
|
+
qrcodeUrl: qrResponse.qrcode_img_content,
|
|
1619
|
+
startedAt: Date.now()
|
|
1620
|
+
};
|
|
1621
|
+
activeLogins.set(sessionKey, login);
|
|
1622
|
+
return {
|
|
1623
|
+
qrcodeUrl: qrResponse.qrcode_img_content,
|
|
1624
|
+
message: "使用微信扫描以下二维码,以完成连接。",
|
|
1625
|
+
sessionKey
|
|
1626
|
+
};
|
|
1627
|
+
} catch (err) {
|
|
1628
|
+
logger.error(`Failed to start Weixin login: ${String(err)}`);
|
|
1629
|
+
return {
|
|
1630
|
+
message: `Failed to start login: ${String(err)}`,
|
|
1631
|
+
sessionKey
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
async function waitForWeixinLogin(opts) {
|
|
1636
|
+
let activeLogin = activeLogins.get(opts.sessionKey);
|
|
1637
|
+
if (!activeLogin) {
|
|
1638
|
+
logger.warn(`waitForWeixinLogin: no active login sessionKey=${opts.sessionKey}`);
|
|
1639
|
+
return {
|
|
1640
|
+
connected: false,
|
|
1641
|
+
message: "当前没有进行中的登录,请先发起登录。"
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
if (!isLoginFresh(activeLogin)) {
|
|
1645
|
+
logger.warn(`waitForWeixinLogin: login QR expired sessionKey=${opts.sessionKey}`);
|
|
1646
|
+
activeLogins.delete(opts.sessionKey);
|
|
1647
|
+
return {
|
|
1648
|
+
connected: false,
|
|
1649
|
+
message: "二维码已过期,请重新生成。"
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
const timeoutMs = Math.max(opts.timeoutMs ?? 480000, 1000);
|
|
1653
|
+
const deadline = Date.now() + timeoutMs;
|
|
1654
|
+
let scannedPrinted = false;
|
|
1655
|
+
let qrRefreshCount = 1;
|
|
1656
|
+
activeLogin.currentApiBaseUrl = FIXED_BASE_URL;
|
|
1657
|
+
logger.info("Starting to poll QR code status...");
|
|
1658
|
+
while (Date.now() < deadline) {
|
|
1659
|
+
try {
|
|
1660
|
+
const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
|
|
1661
|
+
const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode);
|
|
1662
|
+
logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
|
|
1663
|
+
activeLogin.status = statusResponse.status;
|
|
1664
|
+
switch (statusResponse.status) {
|
|
1665
|
+
case "wait":
|
|
1666
|
+
if (opts.verbose) {
|
|
1667
|
+
process.stdout.write(".");
|
|
1668
|
+
}
|
|
1669
|
+
break;
|
|
1670
|
+
case "scaned":
|
|
1671
|
+
if (!scannedPrinted) {
|
|
1672
|
+
process.stdout.write(`
|
|
1673
|
+
\uD83D\uDC40 已扫码,在微信继续操作...
|
|
1674
|
+
`);
|
|
1675
|
+
scannedPrinted = true;
|
|
1676
|
+
}
|
|
1677
|
+
break;
|
|
1678
|
+
case "expired": {
|
|
1679
|
+
qrRefreshCount++;
|
|
1680
|
+
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
|
1681
|
+
logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
|
|
1682
|
+
activeLogins.delete(opts.sessionKey);
|
|
1683
|
+
return {
|
|
1684
|
+
connected: false,
|
|
1685
|
+
message: "登录超时:二维码多次过期,请重新开始登录流程。"
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
process.stdout.write(`
|
|
1689
|
+
⏳ 二维码已过期,正在刷新...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})
|
|
1690
|
+
`);
|
|
1691
|
+
logger.info(`waitForWeixinLogin: QR expired, refreshing (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})`);
|
|
1692
|
+
try {
|
|
1693
|
+
const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
|
|
1694
|
+
const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
|
|
1695
|
+
activeLogin.qrcode = qrResponse.qrcode;
|
|
1696
|
+
activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
|
|
1697
|
+
activeLogin.startedAt = Date.now();
|
|
1698
|
+
scannedPrinted = false;
|
|
1699
|
+
logger.info(`waitForWeixinLogin: new QR code obtained qrcode=${redactToken(qrResponse.qrcode)}`);
|
|
1700
|
+
process.stdout.write(`\uD83D\uDD04 新二维码已生成,请重新扫描
|
|
1701
|
+
|
|
1702
|
+
`);
|
|
1703
|
+
try {
|
|
1704
|
+
const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
|
|
1705
|
+
qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
|
|
1706
|
+
process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
|
|
1707
|
+
`);
|
|
1708
|
+
process.stdout.write(`${qrResponse.qrcode_img_content}
|
|
1709
|
+
`);
|
|
1710
|
+
} catch {
|
|
1711
|
+
process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
|
|
1712
|
+
`);
|
|
1713
|
+
process.stdout.write(`${qrResponse.qrcode_img_content}
|
|
1714
|
+
`);
|
|
1715
|
+
}
|
|
1716
|
+
} catch (refreshErr) {
|
|
1717
|
+
logger.error(`waitForWeixinLogin: failed to refresh QR code: ${String(refreshErr)}`);
|
|
1718
|
+
activeLogins.delete(opts.sessionKey);
|
|
1719
|
+
return {
|
|
1720
|
+
connected: false,
|
|
1721
|
+
message: `刷新二维码失败: ${String(refreshErr)}`
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
break;
|
|
1725
|
+
}
|
|
1726
|
+
case "scaned_but_redirect": {
|
|
1727
|
+
const redirectHost = statusResponse.redirect_host;
|
|
1728
|
+
if (redirectHost) {
|
|
1729
|
+
const newBaseUrl = `https://${redirectHost}`;
|
|
1730
|
+
activeLogin.currentApiBaseUrl = newBaseUrl;
|
|
1731
|
+
logger.info(`waitForWeixinLogin: IDC redirect, switching polling host to ${redirectHost}`);
|
|
1732
|
+
} else {
|
|
1733
|
+
logger.warn(`waitForWeixinLogin: received scaned_but_redirect but redirect_host is missing, continuing with current host`);
|
|
1734
|
+
}
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1737
|
+
case "confirmed": {
|
|
1738
|
+
if (!statusResponse.ilink_bot_id) {
|
|
1739
|
+
activeLogins.delete(opts.sessionKey);
|
|
1740
|
+
logger.error("Login confirmed but ilink_bot_id missing from response");
|
|
1741
|
+
return {
|
|
1742
|
+
connected: false,
|
|
1743
|
+
message: "登录失败:服务器未返回 ilink_bot_id。"
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
activeLogin.botToken = statusResponse.bot_token;
|
|
1747
|
+
activeLogins.delete(opts.sessionKey);
|
|
1748
|
+
logger.info(`✅ Login confirmed! ilink_bot_id=${statusResponse.ilink_bot_id} ilink_user_id=${redactToken(statusResponse.ilink_user_id)}`);
|
|
1749
|
+
return {
|
|
1750
|
+
connected: true,
|
|
1751
|
+
botToken: statusResponse.bot_token,
|
|
1752
|
+
accountId: statusResponse.ilink_bot_id,
|
|
1753
|
+
baseUrl: statusResponse.baseurl,
|
|
1754
|
+
userId: statusResponse.ilink_user_id,
|
|
1755
|
+
message: "✅ 与微信连接成功!"
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
logger.error(`Error polling QR status: ${String(err)}`);
|
|
1761
|
+
activeLogins.delete(opts.sessionKey);
|
|
1762
|
+
return {
|
|
1763
|
+
connected: false,
|
|
1764
|
+
message: `Login failed: ${String(err)}`
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
1768
|
+
}
|
|
1769
|
+
logger.warn(`waitForWeixinLogin: timed out waiting for QR scan sessionKey=${opts.sessionKey} timeoutMs=${timeoutMs}`);
|
|
1770
|
+
activeLogins.delete(opts.sessionKey);
|
|
1771
|
+
return {
|
|
1772
|
+
connected: false,
|
|
1773
|
+
message: "登录超时,请重试。"
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
var ACTIVE_LOGIN_TTL_MS, GET_QRCODE_TIMEOUT_MS = 5000, QR_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_ILINK_BOT_TYPE = "3", FIXED_BASE_URL = "https://ilinkai.weixin.qq.com", activeLogins, MAX_QR_REFRESH_COUNT = 3;
|
|
1777
|
+
var init_login_qr = __esm(() => {
|
|
1778
|
+
init_api();
|
|
1779
|
+
init_logger();
|
|
1780
|
+
ACTIVE_LOGIN_TTL_MS = 5 * 60000;
|
|
1781
|
+
activeLogins = new Map;
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
// src/login.ts
|
|
1785
|
+
var exports_login = {};
|
|
1786
|
+
__export(exports_login, {
|
|
1787
|
+
main: () => main,
|
|
1788
|
+
loginWithQrRendering: () => loginWithQrRendering
|
|
1789
|
+
});
|
|
1790
|
+
async function loginWithQrRendering() {
|
|
1791
|
+
const apiBaseUrl = DEFAULT_BASE_URL;
|
|
1792
|
+
console.log("正在启动微信扫码登录...");
|
|
1793
|
+
const startResult = await startWeixinLoginWithQr({
|
|
1794
|
+
apiBaseUrl,
|
|
1795
|
+
botType: DEFAULT_ILINK_BOT_TYPE
|
|
1796
|
+
});
|
|
1797
|
+
if (!startResult.qrcodeUrl) {
|
|
1798
|
+
throw new Error(startResult.message);
|
|
1799
|
+
}
|
|
1800
|
+
console.log(`
|
|
1801
|
+
使用微信扫描以下二维码,以完成连接:
|
|
1802
|
+
`);
|
|
1803
|
+
await new Promise((resolve) => {
|
|
1804
|
+
import_qrcode_terminal.default.generate(startResult.qrcodeUrl, { small: true }, (qr) => {
|
|
1805
|
+
console.log(qr);
|
|
1806
|
+
resolve();
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
console.log(`
|
|
1810
|
+
等待扫码...
|
|
1811
|
+
`);
|
|
1812
|
+
const waitResult = await waitForWeixinLogin({
|
|
1813
|
+
sessionKey: startResult.sessionKey,
|
|
1814
|
+
apiBaseUrl,
|
|
1815
|
+
timeoutMs: 480000,
|
|
1816
|
+
botType: DEFAULT_ILINK_BOT_TYPE
|
|
1817
|
+
});
|
|
1818
|
+
if (!waitResult.connected || !waitResult.botToken || !waitResult.accountId) {
|
|
1819
|
+
throw new Error(waitResult.message);
|
|
1820
|
+
}
|
|
1821
|
+
const normalizedId = normalizeAccountId(waitResult.accountId);
|
|
1822
|
+
saveWeixinAccount(normalizedId, {
|
|
1823
|
+
token: waitResult.botToken,
|
|
1824
|
+
baseUrl: waitResult.baseUrl,
|
|
1825
|
+
userId: waitResult.userId
|
|
1826
|
+
});
|
|
1827
|
+
registerWeixinAccountId(normalizedId);
|
|
1828
|
+
console.log(`
|
|
1829
|
+
✅ 与微信连接成功!`);
|
|
1830
|
+
}
|
|
1831
|
+
async function main() {
|
|
1832
|
+
await loginWithQrRendering();
|
|
1833
|
+
}
|
|
1834
|
+
var import_qrcode_terminal;
|
|
1835
|
+
var init_login = __esm(async () => {
|
|
1836
|
+
init_accounts();
|
|
1837
|
+
init_login_qr();
|
|
1838
|
+
import_qrcode_terminal = __toESM(require_main(), 1);
|
|
1839
|
+
if (false) {}
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
// src/weixin/api/config-cache.ts
|
|
1843
|
+
class WeixinConfigManager {
|
|
1844
|
+
apiOpts;
|
|
1845
|
+
log;
|
|
1846
|
+
cache = new Map;
|
|
1847
|
+
constructor(apiOpts, log) {
|
|
1848
|
+
this.apiOpts = apiOpts;
|
|
1849
|
+
this.log = log;
|
|
1850
|
+
}
|
|
1851
|
+
async getForUser(userId, contextToken) {
|
|
1852
|
+
const now = Date.now();
|
|
1853
|
+
const entry = this.cache.get(userId);
|
|
1854
|
+
const shouldFetch = !entry || now >= entry.nextFetchAt;
|
|
1855
|
+
if (shouldFetch) {
|
|
1856
|
+
let fetchOk = false;
|
|
1857
|
+
try {
|
|
1858
|
+
const resp = await getConfig({
|
|
1859
|
+
baseUrl: this.apiOpts.baseUrl,
|
|
1860
|
+
token: this.apiOpts.token,
|
|
1861
|
+
ilinkUserId: userId,
|
|
1862
|
+
contextToken
|
|
1863
|
+
});
|
|
1864
|
+
if (resp.ret === 0) {
|
|
1865
|
+
this.cache.set(userId, {
|
|
1866
|
+
config: { typingTicket: resp.typing_ticket ?? "" },
|
|
1867
|
+
everSucceeded: true,
|
|
1868
|
+
nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
|
|
1869
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
|
|
1870
|
+
});
|
|
1871
|
+
this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
|
|
1872
|
+
fetchOk = true;
|
|
1873
|
+
}
|
|
1874
|
+
} catch (err) {
|
|
1875
|
+
this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
|
|
1876
|
+
}
|
|
1877
|
+
if (!fetchOk) {
|
|
1878
|
+
const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
|
|
1879
|
+
const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
|
|
1880
|
+
if (entry) {
|
|
1881
|
+
entry.nextFetchAt = now + nextDelay;
|
|
1882
|
+
entry.retryDelayMs = nextDelay;
|
|
1883
|
+
} else {
|
|
1884
|
+
this.cache.set(userId, {
|
|
1885
|
+
config: { typingTicket: "" },
|
|
1886
|
+
everSucceeded: false,
|
|
1887
|
+
nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
1888
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return this.cache.get(userId)?.config ?? { typingTicket: "" };
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
var CONFIG_CACHE_TTL_MS, CONFIG_CACHE_INITIAL_RETRY_MS = 2000, CONFIG_CACHE_MAX_RETRY_MS;
|
|
1897
|
+
var init_config_cache = __esm(() => {
|
|
1898
|
+
init_api();
|
|
1899
|
+
CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1900
|
+
CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
// src/weixin/api/session-guard.ts
|
|
1904
|
+
function pauseSession(accountId) {
|
|
1905
|
+
const until = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
1906
|
+
pauseUntilMap.set(accountId, until);
|
|
1907
|
+
logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
|
|
1908
|
+
}
|
|
1909
|
+
function getRemainingPauseMs(accountId) {
|
|
1910
|
+
const until = pauseUntilMap.get(accountId);
|
|
1911
|
+
if (until === undefined)
|
|
1912
|
+
return 0;
|
|
1913
|
+
const remaining = until - Date.now();
|
|
1914
|
+
if (remaining <= 0) {
|
|
1915
|
+
pauseUntilMap.delete(accountId);
|
|
1916
|
+
return 0;
|
|
1917
|
+
}
|
|
1918
|
+
return remaining;
|
|
1919
|
+
}
|
|
1920
|
+
var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
|
|
1921
|
+
var init_session_guard = __esm(() => {
|
|
1922
|
+
init_logger();
|
|
1923
|
+
SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
|
|
1924
|
+
pauseUntilMap = new Map;
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
// src/weixin/api/types.ts
|
|
1928
|
+
var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
|
|
1929
|
+
var init_types = __esm(() => {
|
|
1930
|
+
UploadMediaType = {
|
|
1931
|
+
IMAGE: 1,
|
|
1932
|
+
VIDEO: 2,
|
|
1933
|
+
FILE: 3,
|
|
1934
|
+
VOICE: 4
|
|
1935
|
+
};
|
|
1936
|
+
MessageType = {
|
|
1937
|
+
NONE: 0,
|
|
1938
|
+
USER: 1,
|
|
1939
|
+
BOT: 2
|
|
1940
|
+
};
|
|
1941
|
+
MessageItemType = {
|
|
1942
|
+
NONE: 0,
|
|
1943
|
+
TEXT: 1,
|
|
1944
|
+
IMAGE: 2,
|
|
1945
|
+
VOICE: 3,
|
|
1946
|
+
FILE: 4,
|
|
1947
|
+
VIDEO: 5
|
|
1948
|
+
};
|
|
1949
|
+
MessageState = {
|
|
1950
|
+
NEW: 0,
|
|
1951
|
+
GENERATING: 1,
|
|
1952
|
+
FINISH: 2
|
|
1953
|
+
};
|
|
1954
|
+
TypingStatus = {
|
|
1955
|
+
TYPING: 1,
|
|
1956
|
+
CANCEL: 2
|
|
1957
|
+
};
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
// src/weixin/cdn/aes-ecb.ts
|
|
1961
|
+
import { createCipheriv, createDecipheriv } from "node:crypto";
|
|
1962
|
+
function encryptAesEcb(plaintext, key) {
|
|
1963
|
+
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
1964
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1965
|
+
}
|
|
1966
|
+
function decryptAesEcb(ciphertext, key) {
|
|
1967
|
+
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
1968
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1969
|
+
}
|
|
1970
|
+
function aesEcbPaddedSize(plaintextSize) {
|
|
1971
|
+
return Math.ceil((plaintextSize + 1) / 16) * 16;
|
|
1972
|
+
}
|
|
1973
|
+
var init_aes_ecb = () => {};
|
|
1974
|
+
|
|
1975
|
+
// src/weixin/cdn/cdn-url.ts
|
|
1976
|
+
function buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl) {
|
|
1977
|
+
return `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`;
|
|
1978
|
+
}
|
|
1979
|
+
function buildCdnUploadUrl(params) {
|
|
1980
|
+
return `${params.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(params.uploadParam)}&filekey=${encodeURIComponent(params.filekey)}`;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/weixin/cdn/cdn-upload.ts
|
|
1984
|
+
async function uploadBufferToCdn(params) {
|
|
1985
|
+
const { buf, uploadFullUrl, uploadParam, filekey, cdnBaseUrl, label, aeskey } = params;
|
|
1986
|
+
const ciphertext = encryptAesEcb(buf, aeskey);
|
|
1987
|
+
const trimmedFull = uploadFullUrl?.trim();
|
|
1988
|
+
let cdnUrl;
|
|
1989
|
+
if (trimmedFull) {
|
|
1990
|
+
cdnUrl = trimmedFull;
|
|
1991
|
+
} else if (uploadParam) {
|
|
1992
|
+
cdnUrl = buildCdnUploadUrl({ cdnBaseUrl, uploadParam, filekey });
|
|
1993
|
+
} else {
|
|
1994
|
+
throw new Error(`${label}: CDN upload URL missing (need upload_full_url or upload_param)`);
|
|
1995
|
+
}
|
|
1996
|
+
logger.debug(`${label}: CDN POST url=${redactUrl(cdnUrl)} ciphertextSize=${ciphertext.length}`);
|
|
1997
|
+
let downloadParam;
|
|
1998
|
+
let lastError;
|
|
1999
|
+
for (let attempt = 1;attempt <= UPLOAD_MAX_RETRIES; attempt++) {
|
|
2000
|
+
try {
|
|
2001
|
+
const res = await fetch(cdnUrl, {
|
|
2002
|
+
method: "POST",
|
|
2003
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
2004
|
+
body: new Uint8Array(ciphertext)
|
|
2005
|
+
});
|
|
2006
|
+
if (res.status >= 400 && res.status < 500) {
|
|
2007
|
+
const errMsg = res.headers.get("x-error-message") ?? await res.text();
|
|
2008
|
+
logger.error(`${label}: CDN client error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
|
|
2009
|
+
throw new Error(`CDN upload client error ${res.status}: ${errMsg}`);
|
|
2010
|
+
}
|
|
2011
|
+
if (res.status !== 200) {
|
|
2012
|
+
const errMsg = res.headers.get("x-error-message") ?? `status ${res.status}`;
|
|
2013
|
+
logger.error(`${label}: CDN server error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
|
|
2014
|
+
throw new Error(`CDN upload server error: ${errMsg}`);
|
|
2015
|
+
}
|
|
2016
|
+
downloadParam = res.headers.get("x-encrypted-param") ?? undefined;
|
|
2017
|
+
if (!downloadParam) {
|
|
2018
|
+
logger.error(`${label}: CDN response missing x-encrypted-param header attempt=${attempt}`);
|
|
2019
|
+
throw new Error("CDN upload response missing x-encrypted-param header");
|
|
2020
|
+
}
|
|
2021
|
+
logger.debug(`${label}: CDN upload success attempt=${attempt}`);
|
|
2022
|
+
break;
|
|
2023
|
+
} catch (err) {
|
|
2024
|
+
lastError = err;
|
|
2025
|
+
if (err instanceof Error && err.message.includes("client error"))
|
|
2026
|
+
throw err;
|
|
2027
|
+
if (attempt < UPLOAD_MAX_RETRIES) {
|
|
2028
|
+
logger.error(`${label}: attempt ${attempt} failed, retrying... err=${String(err)}`);
|
|
2029
|
+
} else {
|
|
2030
|
+
logger.error(`${label}: all ${UPLOAD_MAX_RETRIES} attempts failed err=${String(err)}`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (!downloadParam) {
|
|
2035
|
+
throw lastError instanceof Error ? lastError : new Error(`CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts`);
|
|
2036
|
+
}
|
|
2037
|
+
return { downloadParam };
|
|
2038
|
+
}
|
|
2039
|
+
var UPLOAD_MAX_RETRIES = 3;
|
|
2040
|
+
var init_cdn_upload = __esm(() => {
|
|
2041
|
+
init_aes_ecb();
|
|
2042
|
+
init_logger();
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
// src/weixin/media/mime.ts
|
|
2046
|
+
import path5 from "node:path";
|
|
2047
|
+
function getMimeFromFilename(filename) {
|
|
2048
|
+
const ext = path5.extname(filename).toLowerCase();
|
|
2049
|
+
return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
|
|
2050
|
+
}
|
|
2051
|
+
function getExtensionFromMime(mimeType) {
|
|
2052
|
+
const ct = (mimeType.split(";")[0] ?? "").trim().toLowerCase();
|
|
2053
|
+
return MIME_TO_EXTENSION[ct] ?? ".bin";
|
|
2054
|
+
}
|
|
2055
|
+
function getExtensionFromContentTypeOrUrl(contentType, url) {
|
|
2056
|
+
if (contentType) {
|
|
2057
|
+
const ext2 = getExtensionFromMime(contentType);
|
|
2058
|
+
if (ext2 !== ".bin")
|
|
2059
|
+
return ext2;
|
|
2060
|
+
}
|
|
2061
|
+
const ext = path5.extname(new URL(url).pathname).toLowerCase();
|
|
2062
|
+
const knownExts = new Set(Object.keys(EXTENSION_TO_MIME));
|
|
2063
|
+
return knownExts.has(ext) ? ext : ".bin";
|
|
2064
|
+
}
|
|
2065
|
+
var EXTENSION_TO_MIME, MIME_TO_EXTENSION;
|
|
2066
|
+
var init_mime = __esm(() => {
|
|
2067
|
+
EXTENSION_TO_MIME = {
|
|
2068
|
+
".pdf": "application/pdf",
|
|
2069
|
+
".doc": "application/msword",
|
|
2070
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2071
|
+
".xls": "application/vnd.ms-excel",
|
|
2072
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2073
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
2074
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
2075
|
+
".txt": "text/plain",
|
|
2076
|
+
".csv": "text/csv",
|
|
2077
|
+
".zip": "application/zip",
|
|
2078
|
+
".tar": "application/x-tar",
|
|
2079
|
+
".gz": "application/gzip",
|
|
2080
|
+
".mp3": "audio/mpeg",
|
|
2081
|
+
".ogg": "audio/ogg",
|
|
2082
|
+
".wav": "audio/wav",
|
|
2083
|
+
".mp4": "video/mp4",
|
|
2084
|
+
".mov": "video/quicktime",
|
|
2085
|
+
".webm": "video/webm",
|
|
2086
|
+
".mkv": "video/x-matroska",
|
|
2087
|
+
".avi": "video/x-msvideo",
|
|
2088
|
+
".png": "image/png",
|
|
2089
|
+
".jpg": "image/jpeg",
|
|
2090
|
+
".jpeg": "image/jpeg",
|
|
2091
|
+
".gif": "image/gif",
|
|
2092
|
+
".webp": "image/webp",
|
|
2093
|
+
".bmp": "image/bmp"
|
|
2094
|
+
};
|
|
2095
|
+
MIME_TO_EXTENSION = {
|
|
2096
|
+
"image/jpeg": ".jpg",
|
|
2097
|
+
"image/jpg": ".jpg",
|
|
2098
|
+
"image/png": ".png",
|
|
2099
|
+
"image/gif": ".gif",
|
|
2100
|
+
"image/webp": ".webp",
|
|
2101
|
+
"image/bmp": ".bmp",
|
|
2102
|
+
"video/mp4": ".mp4",
|
|
2103
|
+
"video/quicktime": ".mov",
|
|
2104
|
+
"video/webm": ".webm",
|
|
2105
|
+
"video/x-matroska": ".mkv",
|
|
2106
|
+
"video/x-msvideo": ".avi",
|
|
2107
|
+
"audio/mpeg": ".mp3",
|
|
2108
|
+
"audio/ogg": ".ogg",
|
|
2109
|
+
"audio/wav": ".wav",
|
|
2110
|
+
"application/pdf": ".pdf",
|
|
2111
|
+
"application/zip": ".zip",
|
|
2112
|
+
"application/x-tar": ".tar",
|
|
2113
|
+
"application/gzip": ".gz",
|
|
2114
|
+
"text/plain": ".txt",
|
|
2115
|
+
"text/csv": ".csv"
|
|
2116
|
+
};
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
// src/weixin/util/random.ts
|
|
2120
|
+
import crypto2 from "node:crypto";
|
|
2121
|
+
function generateId(prefix) {
|
|
2122
|
+
return `${prefix}:${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
|
|
2123
|
+
}
|
|
2124
|
+
function tempFileName(prefix, ext) {
|
|
2125
|
+
return `${prefix}-${Date.now()}-${crypto2.randomBytes(4).toString("hex")}${ext}`;
|
|
2126
|
+
}
|
|
2127
|
+
var init_random = () => {};
|
|
2128
|
+
|
|
2129
|
+
// src/weixin/cdn/upload.ts
|
|
2130
|
+
import crypto3 from "node:crypto";
|
|
2131
|
+
import fs4 from "node:fs/promises";
|
|
2132
|
+
import path6 from "node:path";
|
|
2133
|
+
async function downloadRemoteImageToTemp(url, destDir) {
|
|
2134
|
+
logger.debug(`downloadRemoteImageToTemp: fetching url=${url}`);
|
|
2135
|
+
const res = await fetch(url);
|
|
2136
|
+
if (!res.ok) {
|
|
2137
|
+
const msg = `remote media download failed: ${res.status} ${res.statusText} url=${url}`;
|
|
2138
|
+
logger.error(`downloadRemoteImageToTemp: ${msg}`);
|
|
2139
|
+
throw new Error(msg);
|
|
2140
|
+
}
|
|
2141
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
2142
|
+
logger.debug(`downloadRemoteImageToTemp: downloaded ${buf.length} bytes`);
|
|
2143
|
+
await fs4.mkdir(destDir, { recursive: true });
|
|
2144
|
+
const ext = getExtensionFromContentTypeOrUrl(res.headers.get("content-type"), url);
|
|
2145
|
+
const name = tempFileName("weixin-remote", ext);
|
|
2146
|
+
const filePath = path6.join(destDir, name);
|
|
2147
|
+
await fs4.writeFile(filePath, buf);
|
|
2148
|
+
logger.debug(`downloadRemoteImageToTemp: saved to ${filePath} ext=${ext}`);
|
|
2149
|
+
return filePath;
|
|
2150
|
+
}
|
|
2151
|
+
async function uploadMediaToCdn(params) {
|
|
2152
|
+
const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
|
|
2153
|
+
const plaintext = await fs4.readFile(filePath);
|
|
2154
|
+
const rawsize = plaintext.length;
|
|
2155
|
+
const rawfilemd5 = crypto3.createHash("md5").update(plaintext).digest("hex");
|
|
2156
|
+
const filesize = aesEcbPaddedSize(rawsize);
|
|
2157
|
+
const filekey = crypto3.randomBytes(16).toString("hex");
|
|
2158
|
+
const aeskey = crypto3.randomBytes(16);
|
|
2159
|
+
logger.debug(`${label}: file=${filePath} rawsize=${rawsize} filesize=${filesize} md5=${rawfilemd5} filekey=${filekey}`);
|
|
2160
|
+
const uploadUrlResp = await getUploadUrl({
|
|
2161
|
+
...opts,
|
|
2162
|
+
filekey,
|
|
2163
|
+
media_type: mediaType,
|
|
2164
|
+
to_user_id: toUserId,
|
|
2165
|
+
rawsize,
|
|
2166
|
+
rawfilemd5,
|
|
2167
|
+
filesize,
|
|
2168
|
+
no_need_thumb: true,
|
|
2169
|
+
aeskey: aeskey.toString("hex")
|
|
2170
|
+
});
|
|
2171
|
+
const uploadFullUrl = uploadUrlResp.upload_full_url?.trim();
|
|
2172
|
+
const uploadParam = uploadUrlResp.upload_param;
|
|
2173
|
+
if (!uploadFullUrl && !uploadParam) {
|
|
2174
|
+
logger.error(`${label}: getUploadUrl returned no upload URL (need upload_full_url or upload_param), resp=${JSON.stringify(uploadUrlResp)}`);
|
|
2175
|
+
throw new Error(`${label}: getUploadUrl returned no upload URL`);
|
|
2176
|
+
}
|
|
2177
|
+
const { downloadParam: downloadEncryptedQueryParam } = await uploadBufferToCdn({
|
|
2178
|
+
buf: plaintext,
|
|
2179
|
+
uploadFullUrl: uploadFullUrl || undefined,
|
|
2180
|
+
uploadParam: uploadParam ?? undefined,
|
|
2181
|
+
filekey,
|
|
2182
|
+
cdnBaseUrl,
|
|
2183
|
+
aeskey,
|
|
2184
|
+
label: `${label}[orig filekey=${filekey}]`
|
|
2185
|
+
});
|
|
2186
|
+
return {
|
|
2187
|
+
filekey,
|
|
2188
|
+
downloadEncryptedQueryParam,
|
|
2189
|
+
aeskey: aeskey.toString("hex"),
|
|
2190
|
+
fileSize: rawsize,
|
|
2191
|
+
fileSizeCiphertext: filesize
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
async function uploadFileToWeixin(params) {
|
|
2195
|
+
return uploadMediaToCdn({
|
|
2196
|
+
...params,
|
|
2197
|
+
mediaType: UploadMediaType.IMAGE,
|
|
2198
|
+
label: "uploadFileToWeixin"
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
async function uploadVideoToWeixin(params) {
|
|
2202
|
+
return uploadMediaToCdn({
|
|
2203
|
+
...params,
|
|
2204
|
+
mediaType: UploadMediaType.VIDEO,
|
|
2205
|
+
label: "uploadVideoToWeixin"
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
async function uploadFileAttachmentToWeixin(params) {
|
|
2209
|
+
return uploadMediaToCdn({
|
|
2210
|
+
...params,
|
|
2211
|
+
mediaType: UploadMediaType.FILE,
|
|
2212
|
+
label: "uploadFileAttachmentToWeixin"
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
var init_upload = __esm(() => {
|
|
2216
|
+
init_api();
|
|
2217
|
+
init_aes_ecb();
|
|
2218
|
+
init_cdn_upload();
|
|
2219
|
+
init_logger();
|
|
2220
|
+
init_mime();
|
|
2221
|
+
init_random();
|
|
2222
|
+
init_types();
|
|
2223
|
+
});
|
|
2224
|
+
|
|
2225
|
+
// src/weixin/cdn/pic-decrypt.ts
|
|
2226
|
+
async function fetchCdnBytes(url, label) {
|
|
2227
|
+
let res;
|
|
2228
|
+
try {
|
|
2229
|
+
res = await fetch(url);
|
|
2230
|
+
} catch (err) {
|
|
2231
|
+
const cause = err.cause ?? err.code ?? "(no cause)";
|
|
2232
|
+
logger.error(`${label}: fetch network error url=${url} err=${String(err)} cause=${String(cause)}`);
|
|
2233
|
+
throw err;
|
|
2234
|
+
}
|
|
2235
|
+
logger.debug(`${label}: response status=${res.status} ok=${res.ok}`);
|
|
2236
|
+
if (!res.ok) {
|
|
2237
|
+
const body = await res.text().catch(() => "(unreadable)");
|
|
2238
|
+
const msg = `${label}: CDN download ${res.status} ${res.statusText} body=${body}`;
|
|
2239
|
+
logger.error(msg);
|
|
2240
|
+
throw new Error(msg);
|
|
2241
|
+
}
|
|
2242
|
+
return Buffer.from(await res.arrayBuffer());
|
|
2243
|
+
}
|
|
2244
|
+
function parseAesKey(aesKeyBase64, label) {
|
|
2245
|
+
const decoded = Buffer.from(aesKeyBase64, "base64");
|
|
2246
|
+
if (decoded.length === 16) {
|
|
2247
|
+
return decoded;
|
|
2248
|
+
}
|
|
2249
|
+
if (decoded.length === 32 && /^[0-9a-fA-F]{32}$/.test(decoded.toString("ascii"))) {
|
|
2250
|
+
return Buffer.from(decoded.toString("ascii"), "hex");
|
|
2251
|
+
}
|
|
2252
|
+
const msg = `${label}: aes_key must decode to 16 raw bytes or 32-char hex string, got ${decoded.length} bytes (base64="${aesKeyBase64}")`;
|
|
2253
|
+
logger.error(msg);
|
|
2254
|
+
throw new Error(msg);
|
|
2255
|
+
}
|
|
2256
|
+
async function downloadAndDecryptBuffer(encryptedQueryParam, aesKeyBase64, cdnBaseUrl, label, fullUrl) {
|
|
2257
|
+
const key = parseAesKey(aesKeyBase64, label);
|
|
2258
|
+
const url = fullUrl || buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
|
|
2259
|
+
logger.debug(`${label}: fetching url=${url}`);
|
|
2260
|
+
const encrypted = await fetchCdnBytes(url, label);
|
|
2261
|
+
logger.debug(`${label}: downloaded ${encrypted.byteLength} bytes, decrypting`);
|
|
2262
|
+
const decrypted = decryptAesEcb(encrypted, key);
|
|
2263
|
+
logger.debug(`${label}: decrypted ${decrypted.length} bytes`);
|
|
2264
|
+
return decrypted;
|
|
2265
|
+
}
|
|
2266
|
+
async function downloadPlainCdnBuffer(encryptedQueryParam, cdnBaseUrl, label, fullUrl) {
|
|
2267
|
+
const url = fullUrl || buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
|
|
2268
|
+
logger.debug(`${label}: fetching url=${url}`);
|
|
2269
|
+
return fetchCdnBytes(url, label);
|
|
2270
|
+
}
|
|
2271
|
+
var init_pic_decrypt = __esm(() => {
|
|
2272
|
+
init_aes_ecb();
|
|
2273
|
+
init_logger();
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
// src/weixin/media/silk-transcode.ts
|
|
2277
|
+
function pcmBytesToWav(pcm, sampleRate) {
|
|
2278
|
+
const pcmBytes = pcm.byteLength;
|
|
2279
|
+
const totalSize = 44 + pcmBytes;
|
|
2280
|
+
const buf = Buffer.allocUnsafe(totalSize);
|
|
2281
|
+
let offset = 0;
|
|
2282
|
+
buf.write("RIFF", offset);
|
|
2283
|
+
offset += 4;
|
|
2284
|
+
buf.writeUInt32LE(totalSize - 8, offset);
|
|
2285
|
+
offset += 4;
|
|
2286
|
+
buf.write("WAVE", offset);
|
|
2287
|
+
offset += 4;
|
|
2288
|
+
buf.write("fmt ", offset);
|
|
2289
|
+
offset += 4;
|
|
2290
|
+
buf.writeUInt32LE(16, offset);
|
|
2291
|
+
offset += 4;
|
|
2292
|
+
buf.writeUInt16LE(1, offset);
|
|
2293
|
+
offset += 2;
|
|
2294
|
+
buf.writeUInt16LE(1, offset);
|
|
2295
|
+
offset += 2;
|
|
2296
|
+
buf.writeUInt32LE(sampleRate, offset);
|
|
2297
|
+
offset += 4;
|
|
2298
|
+
buf.writeUInt32LE(sampleRate * 2, offset);
|
|
2299
|
+
offset += 4;
|
|
2300
|
+
buf.writeUInt16LE(2, offset);
|
|
2301
|
+
offset += 2;
|
|
2302
|
+
buf.writeUInt16LE(16, offset);
|
|
2303
|
+
offset += 2;
|
|
2304
|
+
buf.write("data", offset);
|
|
2305
|
+
offset += 4;
|
|
2306
|
+
buf.writeUInt32LE(pcmBytes, offset);
|
|
2307
|
+
offset += 4;
|
|
2308
|
+
Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength).copy(buf, offset);
|
|
2309
|
+
return buf;
|
|
2310
|
+
}
|
|
2311
|
+
async function silkToWav(silkBuf) {
|
|
2312
|
+
try {
|
|
2313
|
+
const { decode } = await import("silk-wasm");
|
|
2314
|
+
logger.debug(`silkToWav: decoding ${silkBuf.length} bytes of SILK`);
|
|
2315
|
+
const result = await decode(silkBuf, SILK_SAMPLE_RATE);
|
|
2316
|
+
logger.debug(`silkToWav: decoded duration=${result.duration}ms pcmBytes=${result.data.byteLength}`);
|
|
2317
|
+
const wav = pcmBytesToWav(result.data, SILK_SAMPLE_RATE);
|
|
2318
|
+
logger.debug(`silkToWav: WAV size=${wav.length}`);
|
|
2319
|
+
return wav;
|
|
2320
|
+
} catch (err) {
|
|
2321
|
+
logger.warn(`silkToWav: transcode failed, will use raw silk err=${String(err)}`);
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
var SILK_SAMPLE_RATE = 24000;
|
|
2326
|
+
var init_silk_transcode = __esm(() => {
|
|
2327
|
+
init_logger();
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
// src/weixin/media/media-download.ts
|
|
2331
|
+
async function downloadMediaFromItem(item, deps) {
|
|
2332
|
+
const { cdnBaseUrl, saveMedia, log, errLog, label } = deps;
|
|
2333
|
+
const result = {};
|
|
2334
|
+
if (item.type === MessageItemType.IMAGE) {
|
|
2335
|
+
const img = item.image_item;
|
|
2336
|
+
if (!img?.media?.encrypt_query_param && !img?.media?.full_url)
|
|
2337
|
+
return result;
|
|
2338
|
+
const aesKeyBase64 = img.aeskey ? Buffer.from(img.aeskey, "hex").toString("base64") : img.media.aes_key;
|
|
2339
|
+
logger.debug(`${label} image: encrypt_query_param=${(img.media.encrypt_query_param ?? "").slice(0, 40)}... hasAesKey=${Boolean(aesKeyBase64)} aeskeySource=${img.aeskey ? "image_item.aeskey" : "media.aes_key"} full_url=${Boolean(img.media.full_url)}`);
|
|
2340
|
+
try {
|
|
2341
|
+
const buf = aesKeyBase64 ? await downloadAndDecryptBuffer(img.media.encrypt_query_param ?? "", aesKeyBase64, cdnBaseUrl, `${label} image`, img.media.full_url) : await downloadPlainCdnBuffer(img.media.encrypt_query_param ?? "", cdnBaseUrl, `${label} image-plain`, img.media.full_url);
|
|
2342
|
+
const saved = await saveMedia(buf, undefined, "inbound", WEIXIN_MEDIA_MAX_BYTES);
|
|
2343
|
+
result.decryptedPicPath = saved.path;
|
|
2344
|
+
logger.debug(`${label} image saved: ${saved.path}`);
|
|
2345
|
+
} catch (err) {
|
|
2346
|
+
logger.error(`${label} image download/decrypt failed: ${String(err)}`);
|
|
2347
|
+
errLog(`weixin ${label} image download/decrypt failed: ${String(err)}`);
|
|
2348
|
+
}
|
|
2349
|
+
} else if (item.type === MessageItemType.VOICE) {
|
|
2350
|
+
const voice = item.voice_item;
|
|
2351
|
+
if (!voice?.media?.encrypt_query_param && !voice?.media?.full_url || !voice?.media?.aes_key)
|
|
2352
|
+
return result;
|
|
2353
|
+
try {
|
|
2354
|
+
const silkBuf = await downloadAndDecryptBuffer(voice.media.encrypt_query_param ?? "", voice.media.aes_key, cdnBaseUrl, `${label} voice`, voice.media.full_url);
|
|
2355
|
+
logger.debug(`${label} voice: decrypted ${silkBuf.length} bytes, attempting silk transcode`);
|
|
2356
|
+
const wavBuf = await silkToWav(silkBuf);
|
|
2357
|
+
if (wavBuf) {
|
|
2358
|
+
const saved = await saveMedia(wavBuf, "audio/wav", "inbound", WEIXIN_MEDIA_MAX_BYTES);
|
|
2359
|
+
result.decryptedVoicePath = saved.path;
|
|
2360
|
+
result.voiceMediaType = "audio/wav";
|
|
2361
|
+
logger.debug(`${label} voice: saved WAV to ${saved.path}`);
|
|
2362
|
+
} else {
|
|
2363
|
+
const saved = await saveMedia(silkBuf, "audio/silk", "inbound", WEIXIN_MEDIA_MAX_BYTES);
|
|
2364
|
+
result.decryptedVoicePath = saved.path;
|
|
2365
|
+
result.voiceMediaType = "audio/silk";
|
|
2366
|
+
logger.debug(`${label} voice: silk transcode unavailable, saved raw SILK to ${saved.path}`);
|
|
2367
|
+
}
|
|
2368
|
+
} catch (err) {
|
|
2369
|
+
logger.error(`${label} voice download/transcode failed: ${String(err)}`);
|
|
2370
|
+
errLog(`weixin ${label} voice download/transcode failed: ${String(err)}`);
|
|
2371
|
+
}
|
|
2372
|
+
} else if (item.type === MessageItemType.FILE) {
|
|
2373
|
+
const fileItem = item.file_item;
|
|
2374
|
+
if (!fileItem?.media?.encrypt_query_param && !fileItem?.media?.full_url || !fileItem?.media?.aes_key)
|
|
2375
|
+
return result;
|
|
2376
|
+
try {
|
|
2377
|
+
const buf = await downloadAndDecryptBuffer(fileItem.media.encrypt_query_param ?? "", fileItem.media.aes_key, cdnBaseUrl, `${label} file`, fileItem.media.full_url);
|
|
2378
|
+
const mime = getMimeFromFilename(fileItem.file_name ?? "file.bin");
|
|
2379
|
+
const saved = await saveMedia(buf, mime, "inbound", WEIXIN_MEDIA_MAX_BYTES, fileItem.file_name ?? undefined);
|
|
2380
|
+
result.decryptedFilePath = saved.path;
|
|
2381
|
+
result.fileMediaType = mime;
|
|
2382
|
+
logger.debug(`${label} file: saved to ${saved.path} mime=${mime}`);
|
|
2383
|
+
} catch (err) {
|
|
2384
|
+
logger.error(`${label} file download failed: ${String(err)}`);
|
|
2385
|
+
errLog(`weixin ${label} file download failed: ${String(err)}`);
|
|
2386
|
+
}
|
|
2387
|
+
} else if (item.type === MessageItemType.VIDEO) {
|
|
2388
|
+
const videoItem = item.video_item;
|
|
2389
|
+
if (!videoItem?.media?.encrypt_query_param && !videoItem?.media?.full_url || !videoItem?.media?.aes_key)
|
|
2390
|
+
return result;
|
|
2391
|
+
try {
|
|
2392
|
+
const buf = await downloadAndDecryptBuffer(videoItem.media.encrypt_query_param ?? "", videoItem.media.aes_key, cdnBaseUrl, `${label} video`, videoItem.media.full_url);
|
|
2393
|
+
const saved = await saveMedia(buf, "video/mp4", "inbound", WEIXIN_MEDIA_MAX_BYTES);
|
|
2394
|
+
result.decryptedVideoPath = saved.path;
|
|
2395
|
+
logger.debug(`${label} video: saved to ${saved.path}`);
|
|
2396
|
+
} catch (err) {
|
|
2397
|
+
logger.error(`${label} video download failed: ${String(err)}`);
|
|
2398
|
+
errLog(`weixin ${label} video download failed: ${String(err)}`);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
return result;
|
|
2402
|
+
}
|
|
2403
|
+
var WEIXIN_MEDIA_MAX_BYTES;
|
|
2404
|
+
var init_media_download = __esm(() => {
|
|
2405
|
+
init_logger();
|
|
2406
|
+
init_mime();
|
|
2407
|
+
init_pic_decrypt();
|
|
2408
|
+
init_silk_transcode();
|
|
2409
|
+
init_types();
|
|
2410
|
+
WEIXIN_MEDIA_MAX_BYTES = 100 * 1024 * 1024;
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
// src/weixin/messaging/inbound.ts
|
|
2414
|
+
function contextTokenKey(accountId, userId) {
|
|
2415
|
+
return `${accountId}:${userId}`;
|
|
2416
|
+
}
|
|
2417
|
+
function setContextToken(accountId, userId, token) {
|
|
2418
|
+
const k = contextTokenKey(accountId, userId);
|
|
2419
|
+
logger.debug(`setContextToken: key=${k}`);
|
|
2420
|
+
contextTokenStore.set(k, token);
|
|
2421
|
+
}
|
|
2422
|
+
function getContextToken(accountId, userId) {
|
|
2423
|
+
const k = contextTokenKey(accountId, userId);
|
|
2424
|
+
const val = contextTokenStore.get(k);
|
|
2425
|
+
logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
|
|
2426
|
+
return val;
|
|
2427
|
+
}
|
|
2428
|
+
function isMediaItem(item) {
|
|
2429
|
+
return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
|
|
2430
|
+
}
|
|
2431
|
+
function bodyFromItemList(itemList) {
|
|
2432
|
+
if (!itemList?.length)
|
|
2433
|
+
return "";
|
|
2434
|
+
for (const item of itemList) {
|
|
2435
|
+
if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
|
|
2436
|
+
const text = String(item.text_item.text);
|
|
2437
|
+
const ref = item.ref_msg;
|
|
2438
|
+
if (!ref)
|
|
2439
|
+
return text;
|
|
2440
|
+
if (ref.message_item && isMediaItem(ref.message_item))
|
|
2441
|
+
return text;
|
|
2442
|
+
const parts = [];
|
|
2443
|
+
if (ref.title)
|
|
2444
|
+
parts.push(ref.title);
|
|
2445
|
+
if (ref.message_item) {
|
|
2446
|
+
const refBody = bodyFromItemList([ref.message_item]);
|
|
2447
|
+
if (refBody)
|
|
2448
|
+
parts.push(refBody);
|
|
2449
|
+
}
|
|
2450
|
+
if (!parts.length)
|
|
2451
|
+
return text;
|
|
2452
|
+
return `[引用: ${parts.join(" | ")}]
|
|
2453
|
+
${text}`;
|
|
2454
|
+
}
|
|
2455
|
+
if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
|
|
2456
|
+
return item.voice_item.text;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
return "";
|
|
2460
|
+
}
|
|
2461
|
+
var contextTokenStore;
|
|
2462
|
+
var init_inbound = __esm(() => {
|
|
2463
|
+
init_logger();
|
|
2464
|
+
init_random();
|
|
2465
|
+
init_types();
|
|
2466
|
+
contextTokenStore = new Map;
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2469
|
+
// src/weixin/messaging/send.ts
|
|
2470
|
+
function generateClientId() {
|
|
2471
|
+
return generateId("openclaw-weixin");
|
|
2472
|
+
}
|
|
2473
|
+
function markdownToPlainText(text) {
|
|
2474
|
+
let result = text;
|
|
2475
|
+
result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
|
|
2476
|
+
result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
|
|
2477
|
+
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
2478
|
+
result = result.replace(/^\|[\s:|-]+\|$/gm, "");
|
|
2479
|
+
result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split("|").map((cell) => cell.trim()).join(" "));
|
|
2480
|
+
result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
|
|
2481
|
+
return result;
|
|
2482
|
+
}
|
|
2483
|
+
function buildTextMessageReq(params) {
|
|
2484
|
+
const { to, text, contextToken, clientId } = params;
|
|
2485
|
+
const item_list = text ? [{ type: MessageItemType.TEXT, text_item: { text } }] : [];
|
|
2486
|
+
return {
|
|
2487
|
+
msg: {
|
|
2488
|
+
from_user_id: "",
|
|
2489
|
+
to_user_id: to,
|
|
2490
|
+
client_id: clientId,
|
|
2491
|
+
message_type: MessageType.BOT,
|
|
2492
|
+
message_state: MessageState.FINISH,
|
|
2493
|
+
item_list: item_list.length ? item_list : undefined,
|
|
2494
|
+
context_token: contextToken ?? undefined
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
function buildSendMessageReq(params) {
|
|
2499
|
+
const { to, contextToken, text, clientId } = params;
|
|
2500
|
+
return buildTextMessageReq({ to, text, contextToken, clientId });
|
|
2501
|
+
}
|
|
2502
|
+
async function sendMessageWeixin(params) {
|
|
2503
|
+
const { to, text, opts } = params;
|
|
2504
|
+
if (!opts.contextToken) {
|
|
2505
|
+
logger.error(`sendMessageWeixin: contextToken missing, refusing to send to=${to}`);
|
|
2506
|
+
throw new Error("sendMessageWeixin: contextToken is required");
|
|
2507
|
+
}
|
|
2508
|
+
const clientId = generateClientId();
|
|
2509
|
+
const req = buildSendMessageReq({
|
|
2510
|
+
to,
|
|
2511
|
+
contextToken: opts.contextToken,
|
|
2512
|
+
text,
|
|
2513
|
+
clientId
|
|
2514
|
+
});
|
|
2515
|
+
try {
|
|
2516
|
+
await sendMessage({
|
|
2517
|
+
baseUrl: opts.baseUrl,
|
|
2518
|
+
token: opts.token,
|
|
2519
|
+
timeoutMs: opts.timeoutMs,
|
|
2520
|
+
body: req
|
|
2521
|
+
});
|
|
2522
|
+
} catch (err) {
|
|
2523
|
+
logger.error(`sendMessageWeixin: failed to=${to} clientId=${clientId} err=${String(err)}`);
|
|
2524
|
+
throw err;
|
|
2525
|
+
}
|
|
2526
|
+
return { messageId: clientId };
|
|
2527
|
+
}
|
|
2528
|
+
async function sendMediaItems(params) {
|
|
2529
|
+
const { to, text, mediaItem, opts, label } = params;
|
|
2530
|
+
const items = [];
|
|
2531
|
+
if (text) {
|
|
2532
|
+
items.push({ type: MessageItemType.TEXT, text_item: { text } });
|
|
2533
|
+
}
|
|
2534
|
+
items.push(mediaItem);
|
|
2535
|
+
let lastClientId = "";
|
|
2536
|
+
for (const item of items) {
|
|
2537
|
+
lastClientId = generateClientId();
|
|
2538
|
+
const req = {
|
|
2539
|
+
msg: {
|
|
2540
|
+
from_user_id: "",
|
|
2541
|
+
to_user_id: to,
|
|
2542
|
+
client_id: lastClientId,
|
|
2543
|
+
message_type: MessageType.BOT,
|
|
2544
|
+
message_state: MessageState.FINISH,
|
|
2545
|
+
item_list: [item],
|
|
2546
|
+
context_token: opts.contextToken ?? undefined
|
|
2547
|
+
}
|
|
2548
|
+
};
|
|
2549
|
+
try {
|
|
2550
|
+
await sendMessage({
|
|
2551
|
+
baseUrl: opts.baseUrl,
|
|
2552
|
+
token: opts.token,
|
|
2553
|
+
timeoutMs: opts.timeoutMs,
|
|
2554
|
+
body: req
|
|
2555
|
+
});
|
|
2556
|
+
} catch (err) {
|
|
2557
|
+
logger.error(`${label}: failed to=${to} clientId=${lastClientId} err=${String(err)}`);
|
|
2558
|
+
throw err;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
logger.debug(`${label}: success to=${to} clientId=${lastClientId}`);
|
|
2562
|
+
return { messageId: lastClientId };
|
|
2563
|
+
}
|
|
2564
|
+
async function sendImageMessageWeixin(params) {
|
|
2565
|
+
const { to, text, uploaded, opts } = params;
|
|
2566
|
+
if (!opts.contextToken) {
|
|
2567
|
+
logger.error(`sendImageMessageWeixin: contextToken missing, refusing to send to=${to}`);
|
|
2568
|
+
throw new Error("sendImageMessageWeixin: contextToken is required");
|
|
2569
|
+
}
|
|
2570
|
+
logger.debug(`sendImageMessageWeixin: to=${to} filekey=${uploaded.filekey} fileSize=${uploaded.fileSize} aeskey=present`);
|
|
2571
|
+
const imageItem = {
|
|
2572
|
+
type: MessageItemType.IMAGE,
|
|
2573
|
+
image_item: {
|
|
2574
|
+
media: {
|
|
2575
|
+
encrypt_query_param: uploaded.downloadEncryptedQueryParam,
|
|
2576
|
+
aes_key: Buffer.from(uploaded.aeskey).toString("base64"),
|
|
2577
|
+
encrypt_type: 1
|
|
2578
|
+
},
|
|
2579
|
+
mid_size: uploaded.fileSizeCiphertext
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
return sendMediaItems({ to, text, mediaItem: imageItem, opts, label: "sendImageMessageWeixin" });
|
|
2583
|
+
}
|
|
2584
|
+
async function sendVideoMessageWeixin(params) {
|
|
2585
|
+
const { to, text, uploaded, opts } = params;
|
|
2586
|
+
if (!opts.contextToken) {
|
|
2587
|
+
logger.error(`sendVideoMessageWeixin: contextToken missing, refusing to send to=${to}`);
|
|
2588
|
+
throw new Error("sendVideoMessageWeixin: contextToken is required");
|
|
2589
|
+
}
|
|
2590
|
+
const videoItem = {
|
|
2591
|
+
type: MessageItemType.VIDEO,
|
|
2592
|
+
video_item: {
|
|
2593
|
+
media: {
|
|
2594
|
+
encrypt_query_param: uploaded.downloadEncryptedQueryParam,
|
|
2595
|
+
aes_key: Buffer.from(uploaded.aeskey).toString("base64"),
|
|
2596
|
+
encrypt_type: 1
|
|
2597
|
+
},
|
|
2598
|
+
video_size: uploaded.fileSizeCiphertext
|
|
2599
|
+
}
|
|
2600
|
+
};
|
|
2601
|
+
return sendMediaItems({ to, text, mediaItem: videoItem, opts, label: "sendVideoMessageWeixin" });
|
|
2602
|
+
}
|
|
2603
|
+
async function sendFileMessageWeixin(params) {
|
|
2604
|
+
const { to, text, fileName, uploaded, opts } = params;
|
|
2605
|
+
if (!opts.contextToken) {
|
|
2606
|
+
logger.error(`sendFileMessageWeixin: contextToken missing, refusing to send to=${to}`);
|
|
2607
|
+
throw new Error("sendFileMessageWeixin: contextToken is required");
|
|
2608
|
+
}
|
|
2609
|
+
const fileItem = {
|
|
2610
|
+
type: MessageItemType.FILE,
|
|
2611
|
+
file_item: {
|
|
2612
|
+
media: {
|
|
2613
|
+
encrypt_query_param: uploaded.downloadEncryptedQueryParam,
|
|
2614
|
+
aes_key: Buffer.from(uploaded.aeskey).toString("base64"),
|
|
2615
|
+
encrypt_type: 1
|
|
2616
|
+
},
|
|
2617
|
+
file_name: fileName,
|
|
2618
|
+
len: String(uploaded.fileSize)
|
|
2619
|
+
}
|
|
2620
|
+
};
|
|
2621
|
+
return sendMediaItems({ to, text, mediaItem: fileItem, opts, label: "sendFileMessageWeixin" });
|
|
2622
|
+
}
|
|
2623
|
+
var init_send = __esm(() => {
|
|
2624
|
+
init_api();
|
|
2625
|
+
init_logger();
|
|
2626
|
+
init_random();
|
|
2627
|
+
init_types();
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
// src/weixin/messaging/error-notice.ts
|
|
2631
|
+
async function sendWeixinErrorNotice(params) {
|
|
2632
|
+
if (!params.contextToken) {
|
|
2633
|
+
logger.warn(`sendWeixinErrorNotice: no contextToken for to=${params.to}, cannot notify user`);
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
try {
|
|
2637
|
+
await sendMessageWeixin({ to: params.to, text: params.message, opts: {
|
|
2638
|
+
baseUrl: params.baseUrl,
|
|
2639
|
+
token: params.token,
|
|
2640
|
+
contextToken: params.contextToken
|
|
2641
|
+
} });
|
|
2642
|
+
logger.debug(`sendWeixinErrorNotice: sent to=${params.to}`);
|
|
2643
|
+
} catch (err) {
|
|
2644
|
+
params.errLog(`[weixin] sendWeixinErrorNotice failed to=${params.to}: ${String(err)}`);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
var init_error_notice = __esm(() => {
|
|
2648
|
+
init_logger();
|
|
2649
|
+
init_send();
|
|
2650
|
+
});
|
|
2651
|
+
|
|
2652
|
+
// src/weixin/messaging/send-media.ts
|
|
2653
|
+
import path7 from "node:path";
|
|
2654
|
+
async function sendWeixinMediaFile(params) {
|
|
2655
|
+
const { filePath, to, text, opts, cdnBaseUrl } = params;
|
|
2656
|
+
const mime = getMimeFromFilename(filePath);
|
|
2657
|
+
const uploadOpts = { baseUrl: opts.baseUrl, token: opts.token };
|
|
2658
|
+
if (mime.startsWith("video/")) {
|
|
2659
|
+
logger.info(`[weixin] sendWeixinMediaFile: uploading video filePath=${filePath} to=${to}`);
|
|
2660
|
+
const uploaded2 = await uploadVideoToWeixin({
|
|
2661
|
+
filePath,
|
|
2662
|
+
toUserId: to,
|
|
2663
|
+
opts: uploadOpts,
|
|
2664
|
+
cdnBaseUrl
|
|
2665
|
+
});
|
|
2666
|
+
logger.info(`[weixin] sendWeixinMediaFile: video upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
|
|
2667
|
+
return sendVideoMessageWeixin({ to, text, uploaded: uploaded2, opts });
|
|
2668
|
+
}
|
|
2669
|
+
if (mime.startsWith("image/")) {
|
|
2670
|
+
logger.info(`[weixin] sendWeixinMediaFile: uploading image filePath=${filePath} to=${to}`);
|
|
2671
|
+
const uploaded2 = await uploadFileToWeixin({
|
|
2672
|
+
filePath,
|
|
2673
|
+
toUserId: to,
|
|
2674
|
+
opts: uploadOpts,
|
|
2675
|
+
cdnBaseUrl
|
|
2676
|
+
});
|
|
2677
|
+
logger.info(`[weixin] sendWeixinMediaFile: image upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
|
|
2678
|
+
return sendImageMessageWeixin({ to, text, uploaded: uploaded2, opts });
|
|
2679
|
+
}
|
|
2680
|
+
const fileName = path7.basename(filePath);
|
|
2681
|
+
logger.info(`[weixin] sendWeixinMediaFile: uploading file attachment filePath=${filePath} name=${fileName} to=${to}`);
|
|
2682
|
+
const uploaded = await uploadFileAttachmentToWeixin({
|
|
2683
|
+
filePath,
|
|
2684
|
+
fileName,
|
|
2685
|
+
toUserId: to,
|
|
2686
|
+
opts: uploadOpts,
|
|
2687
|
+
cdnBaseUrl
|
|
2688
|
+
});
|
|
2689
|
+
logger.info(`[weixin] sendWeixinMediaFile: file upload done filekey=${uploaded.filekey} size=${uploaded.fileSize}`);
|
|
2690
|
+
return sendFileMessageWeixin({ to, text, fileName, uploaded, opts });
|
|
2691
|
+
}
|
|
2692
|
+
var init_send_media = __esm(() => {
|
|
2693
|
+
init_logger();
|
|
2694
|
+
init_mime();
|
|
2695
|
+
init_send();
|
|
2696
|
+
init_upload();
|
|
2697
|
+
});
|
|
2698
|
+
|
|
2699
|
+
// src/weixin/messaging/debug-mode.ts
|
|
2700
|
+
import fs5 from "node:fs";
|
|
2701
|
+
import path8 from "node:path";
|
|
2702
|
+
function resolveDebugModePath() {
|
|
2703
|
+
return path8.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
|
|
2704
|
+
}
|
|
2705
|
+
function loadState() {
|
|
2706
|
+
try {
|
|
2707
|
+
const raw = fs5.readFileSync(resolveDebugModePath(), "utf-8");
|
|
2708
|
+
const parsed = JSON.parse(raw);
|
|
2709
|
+
if (parsed && typeof parsed.accounts === "object")
|
|
2710
|
+
return parsed;
|
|
2711
|
+
} catch {}
|
|
2712
|
+
return { accounts: {} };
|
|
2713
|
+
}
|
|
2714
|
+
function saveState(state) {
|
|
2715
|
+
const filePath = resolveDebugModePath();
|
|
2716
|
+
fs5.mkdirSync(path8.dirname(filePath), { recursive: true });
|
|
2717
|
+
fs5.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
2718
|
+
}
|
|
2719
|
+
function toggleDebugMode(accountId) {
|
|
2720
|
+
const state = loadState();
|
|
2721
|
+
const next = !state.accounts[accountId];
|
|
2722
|
+
state.accounts[accountId] = next;
|
|
2723
|
+
try {
|
|
2724
|
+
saveState(state);
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
logger.error(`debug-mode: failed to persist state: ${String(err)}`);
|
|
2727
|
+
}
|
|
2728
|
+
return next;
|
|
2729
|
+
}
|
|
2730
|
+
var init_debug_mode = __esm(() => {
|
|
2731
|
+
init_state_dir();
|
|
2732
|
+
init_logger();
|
|
1055
2733
|
});
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
2734
|
+
|
|
2735
|
+
// src/weixin/messaging/slash-commands.ts
|
|
2736
|
+
async function sendReply(ctx, text) {
|
|
2737
|
+
const opts = {
|
|
2738
|
+
baseUrl: ctx.baseUrl,
|
|
2739
|
+
token: ctx.token,
|
|
2740
|
+
contextToken: ctx.contextToken
|
|
2741
|
+
};
|
|
2742
|
+
await sendMessageWeixin({ to: ctx.to, text, opts });
|
|
2743
|
+
}
|
|
2744
|
+
async function handleEcho(ctx, args, receivedAt, eventTimestamp) {
|
|
2745
|
+
const message = args.trim();
|
|
2746
|
+
if (message) {
|
|
2747
|
+
await sendReply(ctx, message);
|
|
2748
|
+
}
|
|
2749
|
+
const eventTs = eventTimestamp ?? 0;
|
|
2750
|
+
const platformDelay = eventTs > 0 ? `${receivedAt - eventTs}ms` : "N/A";
|
|
2751
|
+
const timing = [
|
|
2752
|
+
"⏱ 通道耗时",
|
|
2753
|
+
`├ 事件时间: ${eventTs > 0 ? new Date(eventTs).toISOString() : "N/A"}`,
|
|
2754
|
+
`├ 平台→插件: ${platformDelay}`,
|
|
2755
|
+
`└ 插件处理: ${Date.now() - receivedAt}ms`
|
|
2756
|
+
].join(`
|
|
2757
|
+
`);
|
|
2758
|
+
await sendReply(ctx, timing);
|
|
2759
|
+
}
|
|
2760
|
+
async function handleSlashCommand(content, ctx, receivedAt, eventTimestamp) {
|
|
2761
|
+
const trimmed = content.trim();
|
|
2762
|
+
if (!trimmed.startsWith("/")) {
|
|
2763
|
+
return { handled: false };
|
|
2764
|
+
}
|
|
2765
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
2766
|
+
const command = spaceIdx === -1 ? trimmed.toLowerCase() : trimmed.slice(0, spaceIdx).toLowerCase();
|
|
2767
|
+
const args = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1);
|
|
2768
|
+
logger.info(`[weixin] Slash command: ${command}, args: ${args.slice(0, 50)}`);
|
|
2769
|
+
try {
|
|
2770
|
+
switch (command) {
|
|
2771
|
+
case "/echo":
|
|
2772
|
+
await handleEcho(ctx, args, receivedAt, eventTimestamp);
|
|
2773
|
+
return { handled: true };
|
|
2774
|
+
case "/toggle-debug": {
|
|
2775
|
+
const enabled = toggleDebugMode(ctx.accountId);
|
|
2776
|
+
await sendReply(ctx, enabled ? "Debug 模式已开启" : "Debug 模式已关闭");
|
|
2777
|
+
return { handled: true };
|
|
2778
|
+
}
|
|
2779
|
+
case "/clear": {
|
|
2780
|
+
await ctx.onClear?.();
|
|
2781
|
+
await sendReply(ctx, "✅ 会话已清除,重新开始对话");
|
|
2782
|
+
return { handled: true };
|
|
2783
|
+
}
|
|
2784
|
+
case "/logout": {
|
|
2785
|
+
if (listWeixinAccountIds().length === 0) {
|
|
2786
|
+
await sendReply(ctx, "当前没有已登录的账号");
|
|
2787
|
+
return { handled: true };
|
|
2788
|
+
}
|
|
2789
|
+
clearAllWeixinAccounts();
|
|
2790
|
+
await sendReply(ctx, "✅ 已退出登录,清除所有账号凭证");
|
|
2791
|
+
return { handled: true };
|
|
2792
|
+
}
|
|
2793
|
+
default:
|
|
2794
|
+
return { handled: false };
|
|
2795
|
+
}
|
|
2796
|
+
} catch (err) {
|
|
2797
|
+
logger.error(`[weixin] Slash command error: ${String(err)}`);
|
|
2798
|
+
try {
|
|
2799
|
+
await sendReply(ctx, `❌ 指令执行失败: ${String(err).slice(0, 200)}`);
|
|
2800
|
+
} catch {}
|
|
2801
|
+
return { handled: true };
|
|
1060
2802
|
}
|
|
1061
|
-
candidates.push("weixin-agent-sdk");
|
|
1062
|
-
return candidates;
|
|
1063
2803
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
2804
|
+
var init_slash_commands = __esm(() => {
|
|
2805
|
+
init_accounts();
|
|
2806
|
+
init_logger();
|
|
2807
|
+
init_debug_mode();
|
|
2808
|
+
init_send();
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
// src/weixin/messaging/process-message.ts
|
|
2812
|
+
import crypto4 from "node:crypto";
|
|
2813
|
+
import fs6 from "node:fs/promises";
|
|
2814
|
+
import path9 from "node:path";
|
|
2815
|
+
async function saveMediaBuffer(buffer, contentType, subdir, _maxBytes, originalFilename) {
|
|
2816
|
+
const dir = path9.join(MEDIA_TEMP_DIR, subdir ?? "");
|
|
2817
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
2818
|
+
let ext = ".bin";
|
|
2819
|
+
if (originalFilename) {
|
|
2820
|
+
ext = path9.extname(originalFilename) || ".bin";
|
|
2821
|
+
} else if (contentType) {
|
|
2822
|
+
ext = getExtensionFromMime(contentType);
|
|
2823
|
+
}
|
|
2824
|
+
const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
|
|
2825
|
+
const filePath = path9.join(dir, name);
|
|
2826
|
+
await fs6.writeFile(filePath, buffer);
|
|
2827
|
+
return { path: filePath };
|
|
2828
|
+
}
|
|
2829
|
+
function extractTextBody(itemList) {
|
|
2830
|
+
if (!itemList?.length)
|
|
2831
|
+
return "";
|
|
2832
|
+
for (const item of itemList) {
|
|
2833
|
+
if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
|
|
2834
|
+
return String(item.text_item.text);
|
|
2835
|
+
}
|
|
1067
2836
|
}
|
|
1068
|
-
return
|
|
2837
|
+
return "";
|
|
1069
2838
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
2839
|
+
function findMediaItem(itemList) {
|
|
2840
|
+
if (!itemList?.length)
|
|
2841
|
+
return;
|
|
2842
|
+
const direct = itemList.find((i) => i.type === MessageItemType.IMAGE && hasDownloadableMedia(i.image_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.VIDEO && hasDownloadableMedia(i.video_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.FILE && hasDownloadableMedia(i.file_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.VOICE && hasDownloadableMedia(i.voice_item?.media) && !i.voice_item?.text);
|
|
2843
|
+
if (direct)
|
|
2844
|
+
return direct;
|
|
2845
|
+
const refItem = itemList.find((i) => i.type === MessageItemType.TEXT && i.ref_msg?.message_item && isMediaItem(i.ref_msg.message_item));
|
|
2846
|
+
return refItem?.ref_msg?.message_item ?? undefined;
|
|
2847
|
+
}
|
|
2848
|
+
async function processOneMessage(full, deps) {
|
|
2849
|
+
const receivedAt = Date.now();
|
|
2850
|
+
const textBody = extractTextBody(full.item_list);
|
|
2851
|
+
if (textBody.startsWith("/")) {
|
|
2852
|
+
const conversationId = full.from_user_id ?? "";
|
|
2853
|
+
const slashResult = await handleSlashCommand(textBody, {
|
|
2854
|
+
to: conversationId,
|
|
2855
|
+
contextToken: full.context_token,
|
|
2856
|
+
baseUrl: deps.baseUrl,
|
|
2857
|
+
token: deps.token,
|
|
2858
|
+
accountId: deps.accountId,
|
|
2859
|
+
log: deps.log,
|
|
2860
|
+
errLog: deps.errLog,
|
|
2861
|
+
onClear: () => deps.agent.clearSession?.(conversationId)
|
|
2862
|
+
}, receivedAt, full.create_time_ms);
|
|
2863
|
+
if (slashResult.handled)
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const contextToken = full.context_token;
|
|
2867
|
+
if (contextToken) {
|
|
2868
|
+
setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
|
|
2869
|
+
}
|
|
2870
|
+
let media;
|
|
2871
|
+
const mediaItem = findMediaItem(full.item_list);
|
|
2872
|
+
if (mediaItem) {
|
|
1074
2873
|
try {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
2874
|
+
const downloaded = await downloadMediaFromItem(mediaItem, {
|
|
2875
|
+
cdnBaseUrl: deps.cdnBaseUrl,
|
|
2876
|
+
saveMedia: saveMediaBuffer,
|
|
2877
|
+
log: deps.log,
|
|
2878
|
+
errLog: deps.errLog,
|
|
2879
|
+
label: "inbound"
|
|
2880
|
+
});
|
|
2881
|
+
if (downloaded.decryptedPicPath) {
|
|
2882
|
+
media = { type: "image", filePath: downloaded.decryptedPicPath, mimeType: "image/*" };
|
|
2883
|
+
} else if (downloaded.decryptedVideoPath) {
|
|
2884
|
+
media = { type: "video", filePath: downloaded.decryptedVideoPath, mimeType: "video/mp4" };
|
|
2885
|
+
} else if (downloaded.decryptedFilePath) {
|
|
2886
|
+
media = {
|
|
2887
|
+
type: "file",
|
|
2888
|
+
filePath: downloaded.decryptedFilePath,
|
|
2889
|
+
mimeType: downloaded.fileMediaType ?? "application/octet-stream"
|
|
2890
|
+
};
|
|
2891
|
+
} else if (downloaded.decryptedVoicePath) {
|
|
2892
|
+
media = {
|
|
2893
|
+
type: "audio",
|
|
2894
|
+
filePath: downloaded.decryptedVoicePath,
|
|
2895
|
+
mimeType: downloaded.voiceMediaType ?? "audio/wav"
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
} catch (err) {
|
|
2899
|
+
logger.error(`media download failed: ${String(err)}`);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
const to = full.from_user_id ?? "";
|
|
2903
|
+
const reply = async (text) => {
|
|
2904
|
+
try {
|
|
2905
|
+
await sendMessageWeixin({
|
|
2906
|
+
to,
|
|
2907
|
+
text: markdownToPlainText(text),
|
|
2908
|
+
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
|
|
2909
|
+
});
|
|
2910
|
+
} catch (err) {
|
|
2911
|
+
logger.error(`intermediate reply failed: ${String(err)}`);
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
const request = {
|
|
2915
|
+
conversationId: full.from_user_id ?? "",
|
|
2916
|
+
text: bodyFromItemList(full.item_list),
|
|
2917
|
+
media,
|
|
2918
|
+
reply
|
|
2919
|
+
};
|
|
2920
|
+
let typingTimer;
|
|
2921
|
+
const startTyping = () => {
|
|
2922
|
+
if (!deps.typingTicket)
|
|
2923
|
+
return;
|
|
2924
|
+
sendTyping({
|
|
2925
|
+
baseUrl: deps.baseUrl,
|
|
2926
|
+
token: deps.token,
|
|
2927
|
+
body: {
|
|
2928
|
+
ilink_user_id: to,
|
|
2929
|
+
typing_ticket: deps.typingTicket,
|
|
2930
|
+
status: TypingStatus.TYPING
|
|
2931
|
+
}
|
|
2932
|
+
}).catch(() => {});
|
|
2933
|
+
};
|
|
2934
|
+
if (deps.typingTicket) {
|
|
2935
|
+
startTyping();
|
|
2936
|
+
typingTimer = setInterval(startTyping, 1e4);
|
|
2937
|
+
}
|
|
2938
|
+
try {
|
|
2939
|
+
const response = await deps.agent.chat(request);
|
|
2940
|
+
if (response.media) {
|
|
2941
|
+
let filePath;
|
|
2942
|
+
const mediaUrl = response.media.url;
|
|
2943
|
+
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
2944
|
+
filePath = await downloadRemoteImageToTemp(mediaUrl, path9.join(MEDIA_TEMP_DIR, "outbound"));
|
|
2945
|
+
} else {
|
|
2946
|
+
filePath = path9.isAbsolute(mediaUrl) ? mediaUrl : path9.resolve(mediaUrl);
|
|
2947
|
+
}
|
|
2948
|
+
await sendWeixinMediaFile({
|
|
2949
|
+
filePath,
|
|
2950
|
+
to,
|
|
2951
|
+
text: response.text ? markdownToPlainText(response.text) : "",
|
|
2952
|
+
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
|
|
2953
|
+
cdnBaseUrl: deps.cdnBaseUrl
|
|
2954
|
+
});
|
|
2955
|
+
} else if (response.text) {
|
|
2956
|
+
await sendMessageWeixin({
|
|
2957
|
+
to,
|
|
2958
|
+
text: markdownToPlainText(response.text),
|
|
2959
|
+
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
} catch (err) {
|
|
2963
|
+
logger.error(`processOneMessage: agent or send failed: ${err instanceof Error ? err.stack ?? err.message : JSON.stringify(err)}`);
|
|
2964
|
+
sendWeixinErrorNotice({
|
|
2965
|
+
to,
|
|
2966
|
+
contextToken,
|
|
2967
|
+
message: `⚠️ 过程失败:${err instanceof Error ? err.message : JSON.stringify(err)}`,
|
|
2968
|
+
baseUrl: deps.baseUrl,
|
|
2969
|
+
token: deps.token,
|
|
2970
|
+
errLog: deps.errLog
|
|
2971
|
+
});
|
|
2972
|
+
} finally {
|
|
2973
|
+
if (typingTimer)
|
|
2974
|
+
clearInterval(typingTimer);
|
|
2975
|
+
if (deps.typingTicket) {
|
|
2976
|
+
sendTyping({
|
|
2977
|
+
baseUrl: deps.baseUrl,
|
|
2978
|
+
token: deps.token,
|
|
2979
|
+
body: {
|
|
2980
|
+
ilink_user_id: to,
|
|
2981
|
+
typing_ticket: deps.typingTicket,
|
|
2982
|
+
status: TypingStatus.CANCEL
|
|
2983
|
+
}
|
|
2984
|
+
}).catch(() => {});
|
|
1079
2985
|
}
|
|
1080
2986
|
}
|
|
1081
|
-
throw new Error([
|
|
1082
|
-
"Unable to load weixin-agent-sdk.",
|
|
1083
|
-
"Tried:",
|
|
1084
|
-
...errors.map((entry) => `- ${entry}`),
|
|
1085
|
-
'Set WEACPX_WEIXIN_SDK to a local SDK entry file, or install the "weixin-agent-sdk" package.'
|
|
1086
|
-
].join(`
|
|
1087
|
-
`));
|
|
1088
2987
|
}
|
|
2988
|
+
var MEDIA_TEMP_DIR = "/tmp/weixin-agent/media", hasDownloadableMedia = (m) => m?.encrypt_query_param || m?.full_url;
|
|
2989
|
+
var init_process_message = __esm(() => {
|
|
2990
|
+
init_api();
|
|
2991
|
+
init_types();
|
|
2992
|
+
init_upload();
|
|
2993
|
+
init_media_download();
|
|
2994
|
+
init_mime();
|
|
2995
|
+
init_logger();
|
|
2996
|
+
init_inbound();
|
|
2997
|
+
init_error_notice();
|
|
2998
|
+
init_send_media();
|
|
2999
|
+
init_send();
|
|
3000
|
+
init_slash_commands();
|
|
3001
|
+
});
|
|
1089
3002
|
|
|
1090
|
-
// src/
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
3003
|
+
// src/weixin/storage/sync-buf.ts
|
|
3004
|
+
import fs7 from "node:fs";
|
|
3005
|
+
import path10 from "node:path";
|
|
3006
|
+
function resolveAccountsDir2() {
|
|
3007
|
+
return path10.join(resolveStateDir(), "openclaw-weixin", "accounts");
|
|
3008
|
+
}
|
|
3009
|
+
function getSyncBufFilePath(accountId) {
|
|
3010
|
+
return path10.join(resolveAccountsDir2(), `${accountId}.sync.json`);
|
|
3011
|
+
}
|
|
3012
|
+
function getLegacySyncBufDefaultJsonPath() {
|
|
3013
|
+
return path10.join(resolveStateDir(), "agents", "default", "sessions", ".openclaw-weixin-sync", "default.json");
|
|
3014
|
+
}
|
|
3015
|
+
function readSyncBufFile(filePath) {
|
|
3016
|
+
try {
|
|
3017
|
+
const raw = fs7.readFileSync(filePath, "utf-8");
|
|
3018
|
+
const data = JSON.parse(raw);
|
|
3019
|
+
if (typeof data.get_updates_buf === "string") {
|
|
3020
|
+
return data.get_updates_buf;
|
|
3021
|
+
}
|
|
3022
|
+
} catch {}
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
function loadGetUpdatesBuf(filePath) {
|
|
3026
|
+
const value = readSyncBufFile(filePath);
|
|
3027
|
+
if (value !== undefined)
|
|
3028
|
+
return value;
|
|
3029
|
+
const accountId = path10.basename(filePath, ".sync.json");
|
|
3030
|
+
const rawId = deriveRawAccountId(accountId);
|
|
3031
|
+
if (rawId) {
|
|
3032
|
+
const compatPath = path10.join(resolveAccountsDir2(), `${rawId}.sync.json`);
|
|
3033
|
+
const compatValue = readSyncBufFile(compatPath);
|
|
3034
|
+
if (compatValue !== undefined)
|
|
3035
|
+
return compatValue;
|
|
3036
|
+
}
|
|
3037
|
+
return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
|
|
3038
|
+
}
|
|
3039
|
+
function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
|
|
3040
|
+
const dir = path10.dirname(filePath);
|
|
3041
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
3042
|
+
fs7.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
|
|
3043
|
+
}
|
|
3044
|
+
var init_sync_buf = __esm(() => {
|
|
3045
|
+
init_accounts();
|
|
3046
|
+
init_state_dir();
|
|
1095
3047
|
});
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
3048
|
+
|
|
3049
|
+
// src/weixin/monitor/monitor.ts
|
|
3050
|
+
async function monitorWeixinProvider(opts) {
|
|
3051
|
+
const {
|
|
3052
|
+
baseUrl,
|
|
3053
|
+
cdnBaseUrl,
|
|
3054
|
+
token,
|
|
3055
|
+
accountId,
|
|
3056
|
+
agent,
|
|
3057
|
+
abortSignal,
|
|
3058
|
+
longPollTimeoutMs
|
|
3059
|
+
} = opts;
|
|
3060
|
+
const log = opts.log ?? ((msg) => console.log(msg));
|
|
3061
|
+
const errLog = (msg) => {
|
|
3062
|
+
log(msg);
|
|
3063
|
+
logger.error(msg);
|
|
3064
|
+
};
|
|
3065
|
+
const aLog = logger.withAccount(accountId);
|
|
3066
|
+
log(`[weixin] monitor started (${baseUrl}, account=${accountId})`);
|
|
3067
|
+
aLog.info(`Monitor started: baseUrl=${baseUrl}`);
|
|
3068
|
+
const syncFilePath = getSyncBufFilePath(accountId);
|
|
3069
|
+
const previousGetUpdatesBuf = loadGetUpdatesBuf(syncFilePath);
|
|
3070
|
+
let getUpdatesBuf = previousGetUpdatesBuf ?? "";
|
|
3071
|
+
if (previousGetUpdatesBuf) {
|
|
3072
|
+
log(`[weixin] resuming from previous sync buf (${getUpdatesBuf.length} bytes)`);
|
|
3073
|
+
} else {
|
|
3074
|
+
log(`[weixin] no previous sync buf, starting fresh`);
|
|
3075
|
+
}
|
|
3076
|
+
const configManager = new WeixinConfigManager({ baseUrl, token }, log);
|
|
3077
|
+
let nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
|
|
3078
|
+
let consecutiveFailures = 0;
|
|
3079
|
+
while (!abortSignal?.aborted) {
|
|
1099
3080
|
try {
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
3081
|
+
const resp = await getUpdates({
|
|
3082
|
+
baseUrl,
|
|
3083
|
+
token,
|
|
3084
|
+
get_updates_buf: getUpdatesBuf,
|
|
3085
|
+
timeoutMs: nextTimeoutMs,
|
|
3086
|
+
abortSignal
|
|
3087
|
+
});
|
|
3088
|
+
if (resp.longpolling_timeout_ms != null && resp.longpolling_timeout_ms > 0) {
|
|
3089
|
+
nextTimeoutMs = resp.longpolling_timeout_ms;
|
|
3090
|
+
}
|
|
3091
|
+
const isApiError = resp.ret !== undefined && resp.ret !== 0 || resp.errcode !== undefined && resp.errcode !== 0;
|
|
3092
|
+
if (isApiError) {
|
|
3093
|
+
const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
|
|
3094
|
+
if (isSessionExpired) {
|
|
3095
|
+
pauseSession(accountId);
|
|
3096
|
+
const pauseMs = getRemainingPauseMs(accountId);
|
|
3097
|
+
errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), pausing for ${Math.ceil(pauseMs / 60000)} min. Please run \`npx weixin-acp login\` to re-login.`);
|
|
3098
|
+
consecutiveFailures = 0;
|
|
3099
|
+
await sleep(pauseMs, abortSignal);
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
consecutiveFailures += 1;
|
|
3103
|
+
errLog(`[weixin] getUpdates failed: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? ""} (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
|
|
3104
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
3105
|
+
errLog(`[weixin] ${MAX_CONSECUTIVE_FAILURES} consecutive failures, backing off 30s`);
|
|
3106
|
+
consecutiveFailures = 0;
|
|
3107
|
+
await sleep(BACKOFF_DELAY_MS, abortSignal);
|
|
3108
|
+
} else {
|
|
3109
|
+
await sleep(RETRY_DELAY_MS, abortSignal);
|
|
3110
|
+
}
|
|
3111
|
+
continue;
|
|
3112
|
+
}
|
|
3113
|
+
consecutiveFailures = 0;
|
|
3114
|
+
if (resp.get_updates_buf != null && resp.get_updates_buf !== "") {
|
|
3115
|
+
saveGetUpdatesBuf(syncFilePath, resp.get_updates_buf);
|
|
3116
|
+
getUpdatesBuf = resp.get_updates_buf;
|
|
3117
|
+
}
|
|
3118
|
+
const list = resp.msgs ?? [];
|
|
3119
|
+
for (const full of list) {
|
|
3120
|
+
aLog.info(`inbound: from=${full.from_user_id} types=${full.item_list?.map((i) => i.type).join(",") ?? "none"}`);
|
|
3121
|
+
const fromUserId = full.from_user_id ?? "";
|
|
3122
|
+
const cachedConfig = await configManager.getForUser(fromUserId, full.context_token);
|
|
3123
|
+
await processOneMessage(full, {
|
|
3124
|
+
accountId,
|
|
3125
|
+
agent,
|
|
3126
|
+
baseUrl,
|
|
3127
|
+
cdnBaseUrl,
|
|
3128
|
+
token,
|
|
3129
|
+
typingTicket: cachedConfig.typingTicket,
|
|
3130
|
+
log,
|
|
3131
|
+
errLog
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
} catch (err) {
|
|
3135
|
+
if (abortSignal?.aborted) {
|
|
3136
|
+
aLog.info(`Monitor stopped (aborted)`);
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
consecutiveFailures += 1;
|
|
3140
|
+
errLog(`[weixin] getUpdates error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${String(err)}`);
|
|
3141
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
3142
|
+
consecutiveFailures = 0;
|
|
3143
|
+
await sleep(BACKOFF_DELAY_MS, abortSignal);
|
|
3144
|
+
} else {
|
|
3145
|
+
await sleep(RETRY_DELAY_MS, abortSignal);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
1105
3148
|
}
|
|
1106
|
-
|
|
3149
|
+
aLog.info(`Monitor ended`);
|
|
1107
3150
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
3151
|
+
function sleep(ms, signal) {
|
|
3152
|
+
return new Promise((resolve, reject) => {
|
|
3153
|
+
const t = setTimeout(resolve, ms);
|
|
3154
|
+
signal?.addEventListener("abort", () => {
|
|
3155
|
+
clearTimeout(t);
|
|
3156
|
+
reject(new Error("aborted"));
|
|
3157
|
+
}, { once: true });
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
var DEFAULT_LONG_POLL_TIMEOUT_MS2 = 35000, MAX_CONSECUTIVE_FAILURES = 3, BACKOFF_DELAY_MS = 30000, RETRY_DELAY_MS = 2000;
|
|
3161
|
+
var init_monitor = __esm(() => {
|
|
3162
|
+
init_api();
|
|
3163
|
+
init_config_cache();
|
|
3164
|
+
init_session_guard();
|
|
3165
|
+
init_process_message();
|
|
3166
|
+
init_sync_buf();
|
|
3167
|
+
init_logger();
|
|
3168
|
+
});
|
|
3169
|
+
|
|
3170
|
+
// src/weixin/bot.ts
|
|
3171
|
+
async function login(opts) {
|
|
3172
|
+
const log = opts?.log ?? console.log;
|
|
3173
|
+
const apiBaseUrl = opts?.baseUrl ?? DEFAULT_BASE_URL;
|
|
3174
|
+
log("正在启动微信扫码登录...");
|
|
3175
|
+
const startResult = await startWeixinLoginWithQr({
|
|
1119
3176
|
apiBaseUrl,
|
|
1120
|
-
botType:
|
|
3177
|
+
botType: DEFAULT_ILINK_BOT_TYPE
|
|
1121
3178
|
});
|
|
1122
3179
|
if (!startResult.qrcodeUrl) {
|
|
1123
3180
|
throw new Error(startResult.message);
|
|
1124
3181
|
}
|
|
1125
|
-
|
|
3182
|
+
log(`
|
|
1126
3183
|
使用微信扫描以下二维码,以完成连接:
|
|
1127
3184
|
`);
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
3185
|
+
try {
|
|
3186
|
+
const qrcodeterminal = await Promise.resolve().then(() => __toESM(require_main(), 1));
|
|
3187
|
+
await new Promise((resolve) => {
|
|
3188
|
+
qrcodeterminal.default.generate(startResult.qrcodeUrl, { small: true }, (qr) => {
|
|
3189
|
+
console.log(qr);
|
|
3190
|
+
resolve();
|
|
3191
|
+
});
|
|
1132
3192
|
});
|
|
1133
|
-
}
|
|
1134
|
-
|
|
3193
|
+
} catch {
|
|
3194
|
+
log(`二维码链接: ${startResult.qrcodeUrl}`);
|
|
3195
|
+
}
|
|
3196
|
+
log(`
|
|
1135
3197
|
等待扫码...
|
|
1136
3198
|
`);
|
|
1137
|
-
const waitResult = await
|
|
3199
|
+
const waitResult = await waitForWeixinLogin({
|
|
1138
3200
|
sessionKey: startResult.sessionKey,
|
|
1139
3201
|
apiBaseUrl,
|
|
1140
3202
|
timeoutMs: 480000,
|
|
1141
|
-
botType:
|
|
3203
|
+
botType: DEFAULT_ILINK_BOT_TYPE
|
|
1142
3204
|
});
|
|
1143
3205
|
if (!waitResult.connected || !waitResult.botToken || !waitResult.accountId) {
|
|
1144
3206
|
throw new Error(waitResult.message);
|
|
1145
3207
|
}
|
|
1146
|
-
const normalizedId =
|
|
1147
|
-
|
|
3208
|
+
const normalizedId = normalizeAccountId(waitResult.accountId);
|
|
3209
|
+
saveWeixinAccount(normalizedId, {
|
|
1148
3210
|
token: waitResult.botToken,
|
|
1149
3211
|
baseUrl: waitResult.baseUrl,
|
|
1150
3212
|
userId: waitResult.userId
|
|
1151
3213
|
});
|
|
1152
|
-
|
|
1153
|
-
|
|
3214
|
+
registerWeixinAccountId(normalizedId);
|
|
3215
|
+
log(`
|
|
1154
3216
|
✅ 与微信连接成功!`);
|
|
3217
|
+
return normalizedId;
|
|
1155
3218
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
3219
|
+
function logout(opts) {
|
|
3220
|
+
const log = opts?.log ?? console.log;
|
|
3221
|
+
const ids = listWeixinAccountIds();
|
|
3222
|
+
if (ids.length === 0) {
|
|
3223
|
+
log("当前没有已登录的账号");
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
clearAllWeixinAccounts();
|
|
3227
|
+
log("✅ 已退出登录");
|
|
1158
3228
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
3229
|
+
function isLoggedIn() {
|
|
3230
|
+
const ids = listWeixinAccountIds();
|
|
3231
|
+
if (ids.length === 0)
|
|
3232
|
+
return false;
|
|
3233
|
+
const account = resolveWeixinAccount(ids[0]);
|
|
3234
|
+
return account.configured;
|
|
3235
|
+
}
|
|
3236
|
+
async function start(agent, opts) {
|
|
3237
|
+
const log = opts?.log ?? console.log;
|
|
3238
|
+
let accountId = opts?.accountId;
|
|
3239
|
+
if (!accountId) {
|
|
3240
|
+
const ids = listWeixinAccountIds();
|
|
3241
|
+
if (ids.length === 0) {
|
|
3242
|
+
throw new Error("没有已登录的账号,请先运行 login");
|
|
3243
|
+
}
|
|
3244
|
+
accountId = ids[0];
|
|
3245
|
+
if (ids.length > 1) {
|
|
3246
|
+
log(`[weixin] 检测到多个账号,使用第一个: ${accountId}`);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
const account = resolveWeixinAccount(accountId);
|
|
3250
|
+
if (!account.configured) {
|
|
3251
|
+
throw new Error(`账号 ${accountId} 未配置 (缺少 token),请先运行 login`);
|
|
3252
|
+
}
|
|
3253
|
+
log(`[weixin] 启动 bot, account=${account.accountId}`);
|
|
3254
|
+
await monitorWeixinProvider({
|
|
3255
|
+
baseUrl: account.baseUrl,
|
|
3256
|
+
cdnBaseUrl: account.cdnBaseUrl,
|
|
3257
|
+
token: account.token,
|
|
3258
|
+
accountId: account.accountId,
|
|
3259
|
+
agent,
|
|
3260
|
+
abortSignal: opts?.abortSignal,
|
|
3261
|
+
log
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3264
|
+
var init_bot = __esm(() => {
|
|
3265
|
+
init_accounts();
|
|
3266
|
+
init_login_qr();
|
|
3267
|
+
init_monitor();
|
|
3268
|
+
});
|
|
3269
|
+
|
|
3270
|
+
// src/weixin/index.ts
|
|
3271
|
+
var exports_weixin = {};
|
|
3272
|
+
__export(exports_weixin, {
|
|
3273
|
+
start: () => start,
|
|
3274
|
+
sendMessageWeixin: () => sendMessageWeixin,
|
|
3275
|
+
resolveWeixinAccount: () => resolveWeixinAccount,
|
|
3276
|
+
markdownToPlainText: () => markdownToPlainText,
|
|
3277
|
+
logout: () => logout,
|
|
3278
|
+
login: () => login,
|
|
3279
|
+
listWeixinAccountIds: () => listWeixinAccountIds,
|
|
3280
|
+
isLoggedIn: () => isLoggedIn,
|
|
3281
|
+
getContextToken: () => getContextToken,
|
|
3282
|
+
clearAllWeixinAccounts: () => clearAllWeixinAccounts
|
|
3283
|
+
});
|
|
3284
|
+
var init_weixin = __esm(() => {
|
|
3285
|
+
init_bot();
|
|
3286
|
+
init_send();
|
|
3287
|
+
init_inbound();
|
|
3288
|
+
init_accounts();
|
|
3289
|
+
});
|
|
3290
|
+
|
|
3291
|
+
// src/weixin-sdk.ts
|
|
3292
|
+
var exports_weixin_sdk = {};
|
|
3293
|
+
__export(exports_weixin_sdk, {
|
|
3294
|
+
start: () => start,
|
|
3295
|
+
resolveWeixinAccount: () => resolveWeixinAccount,
|
|
3296
|
+
logout: () => logout,
|
|
3297
|
+
login: () => login,
|
|
3298
|
+
loadWeixinSdk: () => loadWeixinSdk,
|
|
3299
|
+
listWeixinAccountIds: () => listWeixinAccountIds,
|
|
3300
|
+
isLoggedIn: () => isLoggedIn,
|
|
3301
|
+
clearAllWeixinAccounts: () => clearAllWeixinAccounts
|
|
3302
|
+
});
|
|
3303
|
+
async function loadWeixinSdk() {
|
|
3304
|
+
const { login: login2, start: start2, isLoggedIn: isLoggedIn2 } = await Promise.resolve().then(() => (init_weixin(), exports_weixin));
|
|
3305
|
+
return { login: login2, start: start2, isLoggedIn: isLoggedIn2 };
|
|
3306
|
+
}
|
|
3307
|
+
var init_weixin_sdk = __esm(() => {
|
|
3308
|
+
init_weixin();
|
|
3309
|
+
init_weixin();
|
|
1163
3310
|
});
|
|
1164
3311
|
|
|
1165
3312
|
// src/config/agent-templates.ts
|
|
@@ -1204,9 +3351,13 @@ function renderHelpText() {
|
|
|
1204
3351
|
"/ss new <agent> -d <path>",
|
|
1205
3352
|
"/ss new <alias> -a <name> --ws <name>",
|
|
1206
3353
|
"/ss attach <alias> -a <name> --ws <name> --name <transport-session>",
|
|
3354
|
+
"/pm 或 /permission",
|
|
3355
|
+
"/pm set <allow|read|deny>",
|
|
3356
|
+
"/pm auto [allow|deny|fail]",
|
|
1207
3357
|
"/use <alias>",
|
|
1208
3358
|
"/status",
|
|
1209
|
-
"/cancel 或 /stop"
|
|
3359
|
+
"/cancel 或 /stop",
|
|
3360
|
+
"/session reset 或 /clear"
|
|
1210
3361
|
].join(`
|
|
1211
3362
|
`);
|
|
1212
3363
|
}
|
|
@@ -1242,19 +3393,19 @@ function createAppLogger(options) {
|
|
|
1242
3393
|
const now = options.now ?? (() => new Date);
|
|
1243
3394
|
return {
|
|
1244
3395
|
debug: async (event, message, context) => {
|
|
1245
|
-
await
|
|
3396
|
+
await writeLog2("debug", event, message, context);
|
|
1246
3397
|
},
|
|
1247
3398
|
info: async (event, message, context) => {
|
|
1248
|
-
await
|
|
3399
|
+
await writeLog2("info", event, message, context);
|
|
1249
3400
|
},
|
|
1250
3401
|
error: async (event, message, context) => {
|
|
1251
|
-
await
|
|
3402
|
+
await writeLog2("error", event, message, context);
|
|
1252
3403
|
},
|
|
1253
3404
|
cleanup: async () => {
|
|
1254
3405
|
await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays, now);
|
|
1255
3406
|
}
|
|
1256
3407
|
};
|
|
1257
|
-
async function
|
|
3408
|
+
async function writeLog2(level, event, message, context = {}) {
|
|
1258
3409
|
if (LEVEL_ORDER[level] > LEVEL_ORDER[options.level]) {
|
|
1259
3410
|
return;
|
|
1260
3411
|
}
|
|
@@ -1333,17 +3484,149 @@ function formatValue(value) {
|
|
|
1333
3484
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
1334
3485
|
return String(value);
|
|
1335
3486
|
}
|
|
1336
|
-
return JSON.stringify(value);
|
|
3487
|
+
return JSON.stringify(value);
|
|
3488
|
+
}
|
|
3489
|
+
function isMissingFileError(error) {
|
|
3490
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3491
|
+
}
|
|
3492
|
+
var LEVEL_ORDER;
|
|
3493
|
+
var init_app_logger = __esm(() => {
|
|
3494
|
+
LEVEL_ORDER = {
|
|
3495
|
+
error: 0,
|
|
3496
|
+
info: 1,
|
|
3497
|
+
debug: 2
|
|
3498
|
+
};
|
|
3499
|
+
});
|
|
3500
|
+
|
|
3501
|
+
// src/transport/prompt-output.ts
|
|
3502
|
+
function getPromptText(result) {
|
|
3503
|
+
const stdoutOutput = extractPromptOutput(result.stdout);
|
|
3504
|
+
if (result.code === 0) {
|
|
3505
|
+
return sanitizePromptText(stdoutOutput.text);
|
|
3506
|
+
}
|
|
3507
|
+
const preferredError = extractPromptFailureMessage(result);
|
|
3508
|
+
if (preferredError) {
|
|
3509
|
+
throw new PromptCommandError(preferredError, result);
|
|
3510
|
+
}
|
|
3511
|
+
const stderrOutput = extractPromptOutput(result.stderr);
|
|
3512
|
+
const partialReply = [stdoutOutput, stderrOutput].filter((output) => output.hasAgentMessage && output.text.length > 0).map((output) => sanitizePromptText(output.text)).find((text) => text.length > 0);
|
|
3513
|
+
if (partialReply) {
|
|
3514
|
+
return partialReply;
|
|
3515
|
+
}
|
|
3516
|
+
throw new PromptCommandError(`command failed with exit code ${result.code}`, result);
|
|
3517
|
+
}
|
|
3518
|
+
function normalizeCommandError(result) {
|
|
3519
|
+
const preferredError = extractPromptFailureMessage(result);
|
|
3520
|
+
if (preferredError) {
|
|
3521
|
+
return preferredError;
|
|
3522
|
+
}
|
|
3523
|
+
return result.stdout.trim() || null;
|
|
3524
|
+
}
|
|
3525
|
+
function extractPromptFailureMessage(result) {
|
|
3526
|
+
const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
|
|
3527
|
+
const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
|
|
3528
|
+
if (preferredMessage) {
|
|
3529
|
+
return preferredMessage;
|
|
3530
|
+
}
|
|
3531
|
+
if (rpcMessages.length > 0) {
|
|
3532
|
+
return rpcMessages[rpcMessages.length - 1] ?? null;
|
|
3533
|
+
}
|
|
3534
|
+
const stderrText = result.stderr.trim();
|
|
3535
|
+
if (stderrText.length > 0) {
|
|
3536
|
+
return stderrText;
|
|
3537
|
+
}
|
|
3538
|
+
return null;
|
|
3539
|
+
}
|
|
3540
|
+
function extractPromptOutput(output) {
|
|
3541
|
+
const lines = output.split(`
|
|
3542
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
3543
|
+
const messageSegments = [];
|
|
3544
|
+
let currentSegment = "";
|
|
3545
|
+
let hasAgentMessage = false;
|
|
3546
|
+
for (const line of lines) {
|
|
3547
|
+
try {
|
|
3548
|
+
const event = JSON.parse(line);
|
|
3549
|
+
const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
|
|
3550
|
+
if (isMessageChunk) {
|
|
3551
|
+
hasAgentMessage = true;
|
|
3552
|
+
const chunk = event.params.update.content.text ?? "";
|
|
3553
|
+
if (chunk.length > 0) {
|
|
3554
|
+
currentSegment += chunk;
|
|
3555
|
+
}
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
if (currentSegment.trim().length > 0) {
|
|
3559
|
+
messageSegments.push(currentSegment.trim());
|
|
3560
|
+
}
|
|
3561
|
+
currentSegment = "";
|
|
3562
|
+
} catch {
|
|
3563
|
+
if (currentSegment.trim().length > 0) {
|
|
3564
|
+
messageSegments.push(currentSegment.trim());
|
|
3565
|
+
currentSegment = "";
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
if (currentSegment.trim().length > 0) {
|
|
3570
|
+
messageSegments.push(currentSegment.trim());
|
|
3571
|
+
}
|
|
3572
|
+
if (messageSegments.length > 0) {
|
|
3573
|
+
return {
|
|
3574
|
+
text: messageSegments[messageSegments.length - 1],
|
|
3575
|
+
hasAgentMessage
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
return {
|
|
3579
|
+
text: output.trim(),
|
|
3580
|
+
hasAgentMessage
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
function sanitizePromptText(text) {
|
|
3584
|
+
const trimmed = text.trim();
|
|
3585
|
+
const paragraphs = trimmed.split(/\n\s*\n/);
|
|
3586
|
+
if (paragraphs.length < 2) {
|
|
3587
|
+
return trimmed;
|
|
3588
|
+
}
|
|
3589
|
+
const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
|
|
3590
|
+
if (!looksLikeWorkflowPreamble(firstParagraph)) {
|
|
3591
|
+
return trimmed;
|
|
3592
|
+
}
|
|
3593
|
+
return paragraphs.slice(1).join(`
|
|
3594
|
+
|
|
3595
|
+
`).trim();
|
|
3596
|
+
}
|
|
3597
|
+
function looksLikeWorkflowPreamble(paragraph) {
|
|
3598
|
+
if (!paragraph.startsWith("using ")) {
|
|
3599
|
+
return false;
|
|
3600
|
+
}
|
|
3601
|
+
return paragraph.includes("using-superpowers") || paragraph.includes("repo workflow requirement") || paragraph.includes("workflow requirement") || paragraph.includes("before responding") || paragraph.includes("skill check");
|
|
1337
3602
|
}
|
|
1338
|
-
function
|
|
1339
|
-
return
|
|
3603
|
+
function extractJsonRpcErrorMessages(output) {
|
|
3604
|
+
return output.split(`
|
|
3605
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
|
|
3606
|
+
try {
|
|
3607
|
+
const payload = JSON.parse(line);
|
|
3608
|
+
if (typeof payload.error?.message === "string" && payload.error.message.length > 0) {
|
|
3609
|
+
return [payload.error.message];
|
|
3610
|
+
}
|
|
3611
|
+
} catch {
|
|
3612
|
+
return [];
|
|
3613
|
+
}
|
|
3614
|
+
return [];
|
|
3615
|
+
});
|
|
1340
3616
|
}
|
|
1341
|
-
var
|
|
1342
|
-
var
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
3617
|
+
var PromptCommandError;
|
|
3618
|
+
var init_prompt_output = __esm(() => {
|
|
3619
|
+
PromptCommandError = class PromptCommandError extends Error {
|
|
3620
|
+
exitCode;
|
|
3621
|
+
stdout;
|
|
3622
|
+
stderr;
|
|
3623
|
+
constructor(message, result) {
|
|
3624
|
+
super(message);
|
|
3625
|
+
this.name = "PromptCommandError";
|
|
3626
|
+
this.exitCode = result.code;
|
|
3627
|
+
this.stdout = result.stdout;
|
|
3628
|
+
this.stderr = result.stderr;
|
|
3629
|
+
}
|
|
1347
3630
|
};
|
|
1348
3631
|
});
|
|
1349
3632
|
|
|
@@ -1367,10 +3650,31 @@ function parseCommand(input) {
|
|
|
1367
3650
|
return { kind: "status" };
|
|
1368
3651
|
if (command === "/cancel")
|
|
1369
3652
|
return { kind: "cancel" };
|
|
3653
|
+
if (command === "/clear")
|
|
3654
|
+
return { kind: "session.reset" };
|
|
3655
|
+
if (command === "/permission" && parts.length === 1)
|
|
3656
|
+
return { kind: "permission.status" };
|
|
1370
3657
|
if (command === "/session" && parts.length === 1)
|
|
1371
3658
|
return { kind: "sessions" };
|
|
1372
3659
|
if (command === "/workspace" && parts.length === 1)
|
|
1373
3660
|
return { kind: "workspaces" };
|
|
3661
|
+
if (command === "/session" && parts[1] === "reset" && parts.length === 2)
|
|
3662
|
+
return { kind: "session.reset" };
|
|
3663
|
+
if (command === "/permission" && parts[1] === "set") {
|
|
3664
|
+
const mode = toPermissionMode(parts[2] ?? "");
|
|
3665
|
+
if (mode) {
|
|
3666
|
+
return { kind: "permission.mode.set", mode };
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
if (command === "/permission" && parts[1] === "auto") {
|
|
3670
|
+
if (parts.length === 2) {
|
|
3671
|
+
return { kind: "permission.auto.status" };
|
|
3672
|
+
}
|
|
3673
|
+
const policy = toNonInteractivePermission(parts[2] ?? "");
|
|
3674
|
+
if (policy) {
|
|
3675
|
+
return { kind: "permission.auto.set", policy };
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
1374
3678
|
if (command === "/use" && parts[1]) {
|
|
1375
3679
|
return { kind: "session.use", alias: parts[1] };
|
|
1376
3680
|
}
|
|
@@ -1415,7 +3719,7 @@ function parseCommand(input) {
|
|
|
1415
3719
|
return { kind: "session.shortcut.new", agent: parts[2], cwd };
|
|
1416
3720
|
}
|
|
1417
3721
|
}
|
|
1418
|
-
if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach") {
|
|
3722
|
+
if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach" && parts[1] !== "reset") {
|
|
1419
3723
|
const cwd = readFlagValue(parts, ["--cwd", "-d"]);
|
|
1420
3724
|
if (cwd) {
|
|
1421
3725
|
return { kind: "session.shortcut", agent: parts[1], cwd };
|
|
@@ -1461,6 +3765,8 @@ function normalizeCommand(command) {
|
|
|
1461
3765
|
return "/session";
|
|
1462
3766
|
if (command === "/ws")
|
|
1463
3767
|
return "/workspace";
|
|
3768
|
+
if (command === "/pm")
|
|
3769
|
+
return "/permission";
|
|
1464
3770
|
if (command === "/stop")
|
|
1465
3771
|
return "/cancel";
|
|
1466
3772
|
return command;
|
|
@@ -1468,6 +3774,21 @@ function normalizeCommand(command) {
|
|
|
1468
3774
|
function isRecognizedCommand(command) {
|
|
1469
3775
|
return RECOGNIZED_COMMANDS.has(command);
|
|
1470
3776
|
}
|
|
3777
|
+
function toPermissionMode(value) {
|
|
3778
|
+
if (value === "allow")
|
|
3779
|
+
return "approve-all";
|
|
3780
|
+
if (value === "read")
|
|
3781
|
+
return "approve-reads";
|
|
3782
|
+
if (value === "deny")
|
|
3783
|
+
return "deny-all";
|
|
3784
|
+
return null;
|
|
3785
|
+
}
|
|
3786
|
+
function toNonInteractivePermission(value) {
|
|
3787
|
+
if (value === "allow" || value === "deny" || value === "fail") {
|
|
3788
|
+
return value;
|
|
3789
|
+
}
|
|
3790
|
+
return null;
|
|
3791
|
+
}
|
|
1471
3792
|
function tokenizeCommand(input) {
|
|
1472
3793
|
const tokens = [];
|
|
1473
3794
|
let current = "";
|
|
@@ -1508,6 +3829,8 @@ var init_parse_command = __esm(() => {
|
|
|
1508
3829
|
"/sessions",
|
|
1509
3830
|
"/status",
|
|
1510
3831
|
"/cancel",
|
|
3832
|
+
"/clear",
|
|
3833
|
+
"/permission",
|
|
1511
3834
|
"/session",
|
|
1512
3835
|
"/workspace",
|
|
1513
3836
|
"/use",
|
|
@@ -1526,14 +3849,14 @@ class CommandRouter {
|
|
|
1526
3849
|
config;
|
|
1527
3850
|
configStore;
|
|
1528
3851
|
logger;
|
|
1529
|
-
constructor(sessions, transport, config, configStore,
|
|
3852
|
+
constructor(sessions, transport, config, configStore, logger2) {
|
|
1530
3853
|
this.sessions = sessions;
|
|
1531
3854
|
this.transport = transport;
|
|
1532
3855
|
this.config = config;
|
|
1533
3856
|
this.configStore = configStore;
|
|
1534
|
-
this.logger =
|
|
3857
|
+
this.logger = logger2 ?? createNoopAppLogger();
|
|
1535
3858
|
}
|
|
1536
|
-
async handle(chatKey, input) {
|
|
3859
|
+
async handle(chatKey, input, reply) {
|
|
1537
3860
|
const startedAt = Date.now();
|
|
1538
3861
|
const command = parseCommand(input);
|
|
1539
3862
|
await this.logger.debug("command.parsed", "parsed inbound command", {
|
|
@@ -1582,6 +3905,30 @@ class CommandRouter {
|
|
|
1582
3905
|
this.replaceConfig(updated);
|
|
1583
3906
|
return { text: `Agent「${command.name}」已删除` };
|
|
1584
3907
|
}
|
|
3908
|
+
case "permission.status":
|
|
3909
|
+
return { text: this.renderPermissionStatus("当前权限模式:") };
|
|
3910
|
+
case "permission.mode.set": {
|
|
3911
|
+
if (!this.config || !this.configStore) {
|
|
3912
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
3913
|
+
}
|
|
3914
|
+
const updated = await this.configStore.updateTransport({
|
|
3915
|
+
permissionMode: command.mode
|
|
3916
|
+
});
|
|
3917
|
+
this.replaceConfig(updated);
|
|
3918
|
+
return { text: this.renderPermissionStatus("权限模式已更新:") };
|
|
3919
|
+
}
|
|
3920
|
+
case "permission.auto.status":
|
|
3921
|
+
return { text: this.renderPermissionStatus("当前非交互策略:") };
|
|
3922
|
+
case "permission.auto.set": {
|
|
3923
|
+
if (!this.config || !this.configStore) {
|
|
3924
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
3925
|
+
}
|
|
3926
|
+
const updated = await this.configStore.updateTransport({
|
|
3927
|
+
nonInteractivePermissions: command.policy
|
|
3928
|
+
});
|
|
3929
|
+
this.replaceConfig(updated);
|
|
3930
|
+
return { text: this.renderPermissionStatus("非交互策略已更新:") };
|
|
3931
|
+
}
|
|
1585
3932
|
case "workspaces":
|
|
1586
3933
|
return { text: this.config ? renderWorkspaces(this.config) : "No config loaded." };
|
|
1587
3934
|
case "workspace.new": {
|
|
@@ -1697,14 +4044,16 @@ class CommandRouter {
|
|
|
1697
4044
|
return this.renderTransportError(session, error);
|
|
1698
4045
|
}
|
|
1699
4046
|
}
|
|
4047
|
+
case "session.reset":
|
|
4048
|
+
return await this.resetCurrentSession(chatKey);
|
|
1700
4049
|
case "prompt": {
|
|
1701
4050
|
const session = await this.sessions.getCurrentSession(chatKey);
|
|
1702
4051
|
if (!session) {
|
|
1703
4052
|
return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
|
|
1704
4053
|
}
|
|
1705
4054
|
try {
|
|
1706
|
-
const
|
|
1707
|
-
return { text:
|
|
4055
|
+
const result = await this.promptTransportSession(session, command.text, reply);
|
|
4056
|
+
return { text: result.text };
|
|
1708
4057
|
} catch (error) {
|
|
1709
4058
|
return this.renderTransportError(session, error);
|
|
1710
4059
|
}
|
|
@@ -1712,6 +4061,9 @@ class CommandRouter {
|
|
|
1712
4061
|
}
|
|
1713
4062
|
});
|
|
1714
4063
|
}
|
|
4064
|
+
async clearSession(chatKey) {
|
|
4065
|
+
await this.resetCurrentSession(chatKey);
|
|
4066
|
+
}
|
|
1715
4067
|
async handleSessionShortcut(chatKey, agent, cwdInput, createNew) {
|
|
1716
4068
|
if (!this.config || !this.configStore) {
|
|
1717
4069
|
return { text: "当前没有加载可写入的配置。" };
|
|
@@ -1779,6 +4131,12 @@ class CommandRouter {
|
|
|
1779
4131
|
this.config.agents = { ...updated.agents };
|
|
1780
4132
|
this.config.workspaces = { ...updated.workspaces };
|
|
1781
4133
|
}
|
|
4134
|
+
renderPermissionStatus(title) {
|
|
4135
|
+
const permissionMode = this.config?.transport.permissionMode ?? "approve-all";
|
|
4136
|
+
const nonInteractivePermissions = this.config?.transport.nonInteractivePermissions ?? "fail";
|
|
4137
|
+
return [title, `- mode: ${permissionMode}`, `- auto: ${nonInteractivePermissions}`].join(`
|
|
4138
|
+
`);
|
|
4139
|
+
}
|
|
1782
4140
|
renderTransportError(session, error) {
|
|
1783
4141
|
const message = error instanceof Error ? error.message : String(error);
|
|
1784
4142
|
if (message.includes("No acpx session found")) {
|
|
@@ -1894,11 +4252,46 @@ class CommandRouter {
|
|
|
1894
4252
|
async ensureTransportSession(session) {
|
|
1895
4253
|
await this.measureTransportCall("ensure_session", session, () => this.transport.ensureSession(session));
|
|
1896
4254
|
}
|
|
4255
|
+
async resetCurrentSession(chatKey) {
|
|
4256
|
+
const session = await this.sessions.getCurrentSession(chatKey);
|
|
4257
|
+
if (!session) {
|
|
4258
|
+
return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
|
|
4259
|
+
}
|
|
4260
|
+
const resetSession = this.sessions.resolveSession(session.alias, session.agent, session.workspace, this.buildResetTransportSessionName(session));
|
|
4261
|
+
try {
|
|
4262
|
+
await this.ensureTransportSession(resetSession);
|
|
4263
|
+
const exists = await this.checkTransportSession(resetSession);
|
|
4264
|
+
if (!exists) {
|
|
4265
|
+
return {
|
|
4266
|
+
text: [
|
|
4267
|
+
`会话「${session.alias}」重置失败。`,
|
|
4268
|
+
"新的后端会话未创建成功,请稍后重试。"
|
|
4269
|
+
].join(`
|
|
4270
|
+
`)
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
} catch (error) {
|
|
4274
|
+
return this.renderTransportError(resetSession, error);
|
|
4275
|
+
}
|
|
4276
|
+
await this.sessions.attachSession(resetSession.alias, resetSession.agent, resetSession.workspace, resetSession.transportSession);
|
|
4277
|
+
await this.sessions.useSession(chatKey, resetSession.alias);
|
|
4278
|
+
await this.logger.info("session.reset", "reset current logical session", {
|
|
4279
|
+
alias: resetSession.alias,
|
|
4280
|
+
agent: resetSession.agent,
|
|
4281
|
+
workspace: resetSession.workspace,
|
|
4282
|
+
transportSession: resetSession.transportSession,
|
|
4283
|
+
chatKey
|
|
4284
|
+
});
|
|
4285
|
+
return { text: `会话「${resetSession.alias}」已重置` };
|
|
4286
|
+
}
|
|
4287
|
+
buildResetTransportSessionName(session) {
|
|
4288
|
+
return `${session.workspace}:${session.alias}:reset-${Date.now()}`;
|
|
4289
|
+
}
|
|
1897
4290
|
async checkTransportSession(session) {
|
|
1898
4291
|
return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
|
|
1899
4292
|
}
|
|
1900
|
-
async promptTransportSession(session, text) {
|
|
1901
|
-
return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text));
|
|
4293
|
+
async promptTransportSession(session, text, reply) {
|
|
4294
|
+
return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text, reply));
|
|
1902
4295
|
}
|
|
1903
4296
|
async cancelTransportSession(session) {
|
|
1904
4297
|
return await this.measureTransportCall("cancel", session, () => this.transport.cancel(session));
|
|
@@ -1916,28 +4309,40 @@ class CommandRouter {
|
|
|
1916
4309
|
});
|
|
1917
4310
|
return result;
|
|
1918
4311
|
} catch (error) {
|
|
4312
|
+
const diagnosticContext = error instanceof PromptCommandError ? {
|
|
4313
|
+
exitCode: error.exitCode,
|
|
4314
|
+
stdoutPreview: summarizeTransportDiagnostic(error.stdout),
|
|
4315
|
+
stdoutTailPreview: summarizeTransportDiagnosticTail(error.stdout),
|
|
4316
|
+
stdoutLength: error.stdout.length,
|
|
4317
|
+
...summarizeTransportNdjson(error.stdout, "stdout"),
|
|
4318
|
+
stderrPreview: summarizeTransportDiagnostic(error.stderr),
|
|
4319
|
+
stderrTailPreview: summarizeTransportDiagnosticTail(error.stderr),
|
|
4320
|
+
stderrLength: error.stderr.length,
|
|
4321
|
+
...summarizeTransportNdjson(error.stderr, "stderr")
|
|
4322
|
+
} : {};
|
|
1919
4323
|
await this.logger.error(`transport.${operation}.failed`, "transport operation failed", {
|
|
1920
4324
|
operation,
|
|
1921
4325
|
agent: session.agent,
|
|
1922
4326
|
workspace: session.workspace,
|
|
1923
4327
|
alias: session.alias,
|
|
1924
4328
|
durationMs: Date.now() - startedAt,
|
|
1925
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4329
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4330
|
+
...diagnosticContext
|
|
1926
4331
|
});
|
|
1927
4332
|
throw error;
|
|
1928
4333
|
}
|
|
1929
4334
|
}
|
|
1930
4335
|
}
|
|
1931
|
-
async function pathExists(
|
|
4336
|
+
async function pathExists(path11) {
|
|
1932
4337
|
try {
|
|
1933
|
-
await access(
|
|
4338
|
+
await access(path11);
|
|
1934
4339
|
return true;
|
|
1935
4340
|
} catch {
|
|
1936
4341
|
return false;
|
|
1937
4342
|
}
|
|
1938
4343
|
}
|
|
1939
|
-
function normalizePathForWorkspace(
|
|
1940
|
-
const expanded =
|
|
4344
|
+
function normalizePathForWorkspace(path11) {
|
|
4345
|
+
const expanded = path11.startsWith("~") ? homedir() + path11.slice(1) : path11;
|
|
1941
4346
|
return normalize(expanded);
|
|
1942
4347
|
}
|
|
1943
4348
|
function sameWorkspacePath(left, right) {
|
|
@@ -1951,12 +4356,66 @@ function sameWorkspacePath(left, right) {
|
|
|
1951
4356
|
function summarizeTransportError(message) {
|
|
1952
4357
|
return message.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
1953
4358
|
}
|
|
4359
|
+
function summarizeTransportDiagnostic(output) {
|
|
4360
|
+
const trimmed = output.replace(/\s+/g, " ").trim();
|
|
4361
|
+
if (trimmed.length === 0) {
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
return trimmed.slice(0, 200);
|
|
4365
|
+
}
|
|
4366
|
+
function summarizeTransportDiagnosticTail(output) {
|
|
4367
|
+
const trimmed = output.replace(/\s+/g, " ").trim();
|
|
4368
|
+
if (trimmed.length === 0) {
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
return trimmed.slice(-200);
|
|
4372
|
+
}
|
|
4373
|
+
function summarizeTransportNdjson(output, prefix) {
|
|
4374
|
+
const lines = output.split(`
|
|
4375
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
4376
|
+
if (lines.length === 0) {
|
|
4377
|
+
return {};
|
|
4378
|
+
}
|
|
4379
|
+
const methods = new Set;
|
|
4380
|
+
let agentMessageChunkCount = 0;
|
|
4381
|
+
let stopReason;
|
|
4382
|
+
for (const line of lines) {
|
|
4383
|
+
try {
|
|
4384
|
+
const payload = JSON.parse(line);
|
|
4385
|
+
if (typeof payload.method === "string" && payload.method.length > 0) {
|
|
4386
|
+
methods.add(payload.method);
|
|
4387
|
+
}
|
|
4388
|
+
if (payload.params?.update?.sessionUpdate === "agent_message_chunk") {
|
|
4389
|
+
agentMessageChunkCount += 1;
|
|
4390
|
+
}
|
|
4391
|
+
if (typeof payload.result?.stopReason === "string" && payload.result.stopReason.length > 0) {
|
|
4392
|
+
stopReason = payload.result.stopReason;
|
|
4393
|
+
}
|
|
4394
|
+
} catch {
|
|
4395
|
+
continue;
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
const summary = {
|
|
4399
|
+
[`${prefix}LineCount`]: lines.length
|
|
4400
|
+
};
|
|
4401
|
+
if (methods.size > 0) {
|
|
4402
|
+
summary[`${prefix}Methods`] = [...methods].join(",");
|
|
4403
|
+
}
|
|
4404
|
+
if (agentMessageChunkCount > 0) {
|
|
4405
|
+
summary[`${prefix}AgentMessageChunkCount`] = agentMessageChunkCount;
|
|
4406
|
+
}
|
|
4407
|
+
if (stopReason) {
|
|
4408
|
+
summary[`${prefix}StopReason`] = stopReason;
|
|
4409
|
+
}
|
|
4410
|
+
return summary;
|
|
4411
|
+
}
|
|
1954
4412
|
function isPartialPromptOutputError(message) {
|
|
1955
4413
|
return message.includes("未收到最终回复");
|
|
1956
4414
|
}
|
|
1957
4415
|
var init_command_router = __esm(() => {
|
|
1958
4416
|
init_agent_templates();
|
|
1959
4417
|
init_app_logger();
|
|
4418
|
+
init_prompt_output();
|
|
1960
4419
|
init_parse_command();
|
|
1961
4420
|
});
|
|
1962
4421
|
|
|
@@ -1980,8 +4439,8 @@ import { readFile as readFile3 } from "node:fs/promises";
|
|
|
1980
4439
|
function isRecord(value) {
|
|
1981
4440
|
return typeof value === "object" && value !== null;
|
|
1982
4441
|
}
|
|
1983
|
-
async function loadConfig(
|
|
1984
|
-
const raw = JSON.parse(await readFile3(
|
|
4442
|
+
async function loadConfig(path11, options = {}) {
|
|
4443
|
+
const raw = JSON.parse(await readFile3(path11, "utf8"));
|
|
1985
4444
|
return parseConfig(raw, options);
|
|
1986
4445
|
}
|
|
1987
4446
|
function parseConfig(raw, options = {}) {
|
|
@@ -1998,6 +4457,12 @@ function parseConfig(raw, options = {}) {
|
|
|
1998
4457
|
if ("sessionInitTimeoutMs" in transport && (typeof transport.sessionInitTimeoutMs !== "number" || !Number.isFinite(transport.sessionInitTimeoutMs) || transport.sessionInitTimeoutMs <= 0)) {
|
|
1999
4458
|
throw new Error("transport.sessionInitTimeoutMs must be a positive number");
|
|
2000
4459
|
}
|
|
4460
|
+
if ("permissionMode" in transport && transport.permissionMode !== "approve-all" && transport.permissionMode !== "approve-reads" && transport.permissionMode !== "deny-all") {
|
|
4461
|
+
throw new Error("transport.permissionMode must be approve-all, approve-reads, or deny-all");
|
|
4462
|
+
}
|
|
4463
|
+
if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "allow" && transport.nonInteractivePermissions !== "deny" && transport.nonInteractivePermissions !== "fail") {
|
|
4464
|
+
throw new Error("transport.nonInteractivePermissions must be allow, deny, or fail");
|
|
4465
|
+
}
|
|
2001
4466
|
if (!isRecord(raw.agents)) {
|
|
2002
4467
|
throw new Error("agents must be an object");
|
|
2003
4468
|
}
|
|
@@ -2032,8 +4497,9 @@ function parseConfig(raw, options = {}) {
|
|
|
2032
4497
|
throw new Error(`workspace "${name}" allowed_agents must be an array of strings`);
|
|
2033
4498
|
}
|
|
2034
4499
|
}
|
|
4500
|
+
const rawAgents = raw.agents;
|
|
2035
4501
|
const agents = {};
|
|
2036
|
-
for (const [name, agent] of Object.entries(
|
|
4502
|
+
for (const [name, agent] of Object.entries(rawAgents)) {
|
|
2037
4503
|
const driver = agent.driver;
|
|
2038
4504
|
const command = typeof agent.command === "string" ? resolveAgentCommand(driver, agent.command) : undefined;
|
|
2039
4505
|
agents[name] = {
|
|
@@ -2041,21 +4507,29 @@ function parseConfig(raw, options = {}) {
|
|
|
2041
4507
|
...command ? { command } : {}
|
|
2042
4508
|
};
|
|
2043
4509
|
}
|
|
4510
|
+
const rawWorkspaces = raw.workspaces;
|
|
2044
4511
|
const workspaces = {};
|
|
2045
|
-
for (const [name, workspace] of Object.entries(
|
|
4512
|
+
for (const [name, workspace] of Object.entries(rawWorkspaces)) {
|
|
2046
4513
|
workspaces[name] = {
|
|
2047
4514
|
cwd: workspace.cwd,
|
|
2048
4515
|
...typeof workspace.description === "string" ? { description: workspace.description } : {}
|
|
2049
4516
|
};
|
|
2050
4517
|
}
|
|
4518
|
+
const transportType = transport.type === "acpx-cli" || transport.type === "acpx-bridge" ? transport.type : "acpx-bridge";
|
|
4519
|
+
const permissionMode = transport.permissionMode === "approve-all" || transport.permissionMode === "approve-reads" || transport.permissionMode === "deny-all" ? transport.permissionMode : DEFAULT_PERMISSION_MODE;
|
|
4520
|
+
const nonInteractivePermissions = transport.nonInteractivePermissions === "allow" || transport.nonInteractivePermissions === "deny" || transport.nonInteractivePermissions === "fail" ? transport.nonInteractivePermissions : DEFAULT_NON_INTERACTIVE_PERMISSIONS;
|
|
4521
|
+
const loggingLevel = logging?.level;
|
|
4522
|
+
const resolvedLoggingLevel = loggingLevel === "error" || loggingLevel === "info" || loggingLevel === "debug" ? loggingLevel : options.defaultLoggingLevel ?? DEFAULT_LOGGING_CONFIG.level;
|
|
2051
4523
|
return {
|
|
2052
4524
|
transport: {
|
|
2053
4525
|
...typeof transport.command === "string" ? { command: transport.command } : {},
|
|
2054
4526
|
...typeof transport.sessionInitTimeoutMs === "number" ? { sessionInitTimeoutMs: transport.sessionInitTimeoutMs } : {},
|
|
2055
|
-
type:
|
|
4527
|
+
type: transportType,
|
|
4528
|
+
permissionMode,
|
|
4529
|
+
nonInteractivePermissions
|
|
2056
4530
|
},
|
|
2057
4531
|
logging: {
|
|
2058
|
-
level:
|
|
4532
|
+
level: resolvedLoggingLevel,
|
|
2059
4533
|
maxSizeBytes: typeof logging?.maxSizeBytes === "number" ? logging.maxSizeBytes : DEFAULT_LOGGING_CONFIG.maxSizeBytes,
|
|
2060
4534
|
maxFiles: typeof logging?.maxFiles === "number" ? logging.maxFiles : DEFAULT_LOGGING_CONFIG.maxFiles,
|
|
2061
4535
|
retentionDays: typeof logging?.retentionDays === "number" ? logging.retentionDays : DEFAULT_LOGGING_CONFIG.retentionDays
|
|
@@ -2064,7 +4538,7 @@ function parseConfig(raw, options = {}) {
|
|
|
2064
4538
|
workspaces
|
|
2065
4539
|
};
|
|
2066
4540
|
}
|
|
2067
|
-
var DEFAULT_LOGGING_CONFIG;
|
|
4541
|
+
var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "fail";
|
|
2068
4542
|
var init_load_config = __esm(() => {
|
|
2069
4543
|
DEFAULT_LOGGING_CONFIG = {
|
|
2070
4544
|
level: "info",
|
|
@@ -2080,8 +4554,8 @@ import { dirname as dirname5 } from "node:path";
|
|
|
2080
4554
|
|
|
2081
4555
|
class ConfigStore {
|
|
2082
4556
|
path;
|
|
2083
|
-
constructor(
|
|
2084
|
-
this.path =
|
|
4557
|
+
constructor(path11) {
|
|
4558
|
+
this.path = path11;
|
|
2085
4559
|
}
|
|
2086
4560
|
async load() {
|
|
2087
4561
|
return await loadConfig(this.path);
|
|
@@ -2119,6 +4593,15 @@ class ConfigStore {
|
|
|
2119
4593
|
await this.save(config);
|
|
2120
4594
|
return config;
|
|
2121
4595
|
}
|
|
4596
|
+
async updateTransport(transport) {
|
|
4597
|
+
const config = await this.load();
|
|
4598
|
+
config.transport = {
|
|
4599
|
+
...config.transport,
|
|
4600
|
+
...transport
|
|
4601
|
+
};
|
|
4602
|
+
await this.save(config);
|
|
4603
|
+
return config;
|
|
4604
|
+
}
|
|
2122
4605
|
}
|
|
2123
4606
|
var init_config_store = __esm(() => {
|
|
2124
4607
|
init_load_config();
|
|
@@ -2126,14 +4609,14 @@ var init_config_store = __esm(() => {
|
|
|
2126
4609
|
|
|
2127
4610
|
// src/config/ensure-config.ts
|
|
2128
4611
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
2129
|
-
async function ensureConfigExists(
|
|
4612
|
+
async function ensureConfigExists(path11) {
|
|
2130
4613
|
try {
|
|
2131
|
-
await loadConfig(
|
|
4614
|
+
await loadConfig(path11);
|
|
2132
4615
|
} catch (error) {
|
|
2133
4616
|
if (!isMissingFileError2(error)) {
|
|
2134
4617
|
throw error;
|
|
2135
4618
|
}
|
|
2136
|
-
const store = new ConfigStore(
|
|
4619
|
+
const store = new ConfigStore(path11);
|
|
2137
4620
|
await store.save(await loadDefaultConfigTemplate());
|
|
2138
4621
|
}
|
|
2139
4622
|
}
|
|
@@ -2170,7 +4653,7 @@ function resolveAcpxCommand(options = {}) {
|
|
|
2170
4653
|
}
|
|
2171
4654
|
const platform = options.platform ?? process.platform;
|
|
2172
4655
|
const resolvePackageJson = options.resolvePackageJson ?? ((id) => require2.resolve(id));
|
|
2173
|
-
const readPackageJson = options.readPackageJson ?? ((
|
|
4656
|
+
const readPackageJson = options.readPackageJson ?? ((path11) => JSON.parse(readFileSync(path11, "utf8")));
|
|
2174
4657
|
try {
|
|
2175
4658
|
const packageJsonPath = resolvePackageJson("acpx/package.json");
|
|
2176
4659
|
const pkg = readPackageJson(packageJsonPath);
|
|
@@ -2192,9 +4675,9 @@ var init_resolve_acpx_command = __esm(() => {
|
|
|
2192
4675
|
class ConsoleAgent {
|
|
2193
4676
|
router;
|
|
2194
4677
|
logger;
|
|
2195
|
-
constructor(router,
|
|
4678
|
+
constructor(router, logger2) {
|
|
2196
4679
|
this.router = router;
|
|
2197
|
-
this.logger =
|
|
4680
|
+
this.logger = logger2 ?? createNoopAppLogger();
|
|
2198
4681
|
}
|
|
2199
4682
|
async chat(request) {
|
|
2200
4683
|
if (!request.text.trim()) {
|
|
@@ -2205,7 +4688,10 @@ class ConsoleAgent {
|
|
|
2205
4688
|
kind: request.text.trim().startsWith("/") ? "command" : "prompt",
|
|
2206
4689
|
text: summarizeText(request.text)
|
|
2207
4690
|
});
|
|
2208
|
-
return await this.router.handle(request.conversationId, request.text);
|
|
4691
|
+
return await this.router.handle(request.conversationId, request.text, request.reply);
|
|
4692
|
+
}
|
|
4693
|
+
async clearSession(conversationId) {
|
|
4694
|
+
await this.router.clearSession?.(conversationId);
|
|
2209
4695
|
}
|
|
2210
4696
|
}
|
|
2211
4697
|
function summarizeText(text) {
|
|
@@ -2332,8 +4818,8 @@ import { dirname as dirname6 } from "node:path";
|
|
|
2332
4818
|
|
|
2333
4819
|
class StateStore {
|
|
2334
4820
|
path;
|
|
2335
|
-
constructor(
|
|
2336
|
-
this.path =
|
|
4821
|
+
constructor(path11) {
|
|
4822
|
+
this.path = path11;
|
|
2337
4823
|
}
|
|
2338
4824
|
async load() {
|
|
2339
4825
|
try {
|
|
@@ -2366,7 +4852,15 @@ async function runConsole(paths, deps) {
|
|
|
2366
4852
|
const sdk = await deps.loadWeixinSdk();
|
|
2367
4853
|
const setIntervalFn = deps.setInterval ?? ((fn, delay) => setInterval(fn, delay));
|
|
2368
4854
|
const clearIntervalFn = deps.clearInterval ?? ((timer) => clearInterval(timer));
|
|
4855
|
+
const addProcessListener = deps.addProcessListener ?? ((signal, handler) => process.on(signal, handler));
|
|
4856
|
+
const removeProcessListener = deps.removeProcessListener ?? ((signal, handler) => process.off(signal, handler));
|
|
2369
4857
|
let heartbeatTimer = null;
|
|
4858
|
+
const shutdownController = new AbortController;
|
|
4859
|
+
const signalHandler = () => {
|
|
4860
|
+
shutdownController.abort();
|
|
4861
|
+
};
|
|
4862
|
+
addProcessListener("SIGINT", signalHandler);
|
|
4863
|
+
addProcessListener("SIGTERM", signalHandler);
|
|
2370
4864
|
try {
|
|
2371
4865
|
if (deps.daemonRuntime) {
|
|
2372
4866
|
await deps.daemonRuntime.start({
|
|
@@ -2377,9 +4871,15 @@ async function runConsole(paths, deps) {
|
|
|
2377
4871
|
deps.daemonRuntime?.heartbeat().catch(() => {});
|
|
2378
4872
|
}, deps.heartbeatIntervalMs ?? 30000);
|
|
2379
4873
|
}
|
|
2380
|
-
|
|
4874
|
+
if (!sdk.isLoggedIn()) {
|
|
4875
|
+
console.log("[weacpx] 未检测到登录凭证,正在启动扫码登录...");
|
|
4876
|
+
await sdk.login();
|
|
4877
|
+
}
|
|
4878
|
+
await sdk.start(runtime.agent, { abortSignal: shutdownController.signal });
|
|
2381
4879
|
} finally {
|
|
2382
4880
|
let disposeError = null;
|
|
4881
|
+
removeProcessListener("SIGINT", signalHandler);
|
|
4882
|
+
removeProcessListener("SIGTERM", signalHandler);
|
|
2383
4883
|
if (heartbeatTimer !== null) {
|
|
2384
4884
|
clearIntervalFn(heartbeatTimer);
|
|
2385
4885
|
}
|
|
@@ -2405,7 +4905,7 @@ function encodeBridgeRequest(request) {
|
|
|
2405
4905
|
|
|
2406
4906
|
// src/transport/acpx-bridge/acpx-bridge-client.ts
|
|
2407
4907
|
import { spawn as spawn2 } from "node:child_process";
|
|
2408
|
-
import { fileURLToPath } from "node:url";
|
|
4908
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2409
4909
|
import { createInterface } from "node:readline";
|
|
2410
4910
|
|
|
2411
4911
|
class AcpxBridgeClient {
|
|
@@ -2419,7 +4919,10 @@ class AcpxBridgeClient {
|
|
|
2419
4919
|
const id = String(this.nextId);
|
|
2420
4920
|
this.nextId += 1;
|
|
2421
4921
|
return awaitable((resolve, reject) => {
|
|
2422
|
-
this.pending.set(id, {
|
|
4922
|
+
this.pending.set(id, {
|
|
4923
|
+
resolve: (value) => resolve(value),
|
|
4924
|
+
reject
|
|
4925
|
+
});
|
|
2423
4926
|
this.writeLine(encodeBridgeRequest({
|
|
2424
4927
|
id,
|
|
2425
4928
|
method,
|
|
@@ -2438,6 +4941,14 @@ class AcpxBridgeClient {
|
|
|
2438
4941
|
pending.resolve(response.result);
|
|
2439
4942
|
return;
|
|
2440
4943
|
}
|
|
4944
|
+
if (response.error.details?.exitCode !== undefined) {
|
|
4945
|
+
pending.reject(new PromptCommandError(response.error.message, {
|
|
4946
|
+
code: response.error.details.exitCode,
|
|
4947
|
+
stdout: response.error.details.stdout ?? "",
|
|
4948
|
+
stderr: response.error.details.stderr ?? ""
|
|
4949
|
+
}));
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
2441
4952
|
pending.reject(new Error(response.error.message));
|
|
2442
4953
|
}
|
|
2443
4954
|
handleExit(error) {
|
|
@@ -2461,7 +4972,7 @@ function buildBridgeSpawnSpec(options) {
|
|
|
2461
4972
|
};
|
|
2462
4973
|
}
|
|
2463
4974
|
async function spawnAcpxBridgeClient(options = {}) {
|
|
2464
|
-
const bridgeEntryPath = options.bridgeEntryPath ??
|
|
4975
|
+
const bridgeEntryPath = options.bridgeEntryPath ?? fileURLToPath2(new URL("../../bridge/bridge-main.ts", import.meta.url));
|
|
2465
4976
|
const spawnSpec = buildBridgeSpawnSpec({
|
|
2466
4977
|
execPath: process.execPath,
|
|
2467
4978
|
bridgeEntryPath
|
|
@@ -2470,7 +4981,9 @@ async function spawnAcpxBridgeClient(options = {}) {
|
|
|
2470
4981
|
cwd: options.cwd ?? process.cwd(),
|
|
2471
4982
|
env: {
|
|
2472
4983
|
...process.env,
|
|
2473
|
-
WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx"
|
|
4984
|
+
WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
|
|
4985
|
+
WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
|
|
4986
|
+
WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "fail"
|
|
2474
4987
|
},
|
|
2475
4988
|
stdio: ["pipe", "pipe", "inherit"]
|
|
2476
4989
|
});
|
|
@@ -2510,7 +5023,9 @@ function awaitable(executor) {
|
|
|
2510
5023
|
executor(resolve, reject);
|
|
2511
5024
|
});
|
|
2512
5025
|
}
|
|
2513
|
-
var init_acpx_bridge_client = () => {
|
|
5026
|
+
var init_acpx_bridge_client = __esm(() => {
|
|
5027
|
+
init_prompt_output();
|
|
5028
|
+
});
|
|
2514
5029
|
|
|
2515
5030
|
// src/transport/acpx-bridge/acpx-bridge-transport.ts
|
|
2516
5031
|
class AcpxBridgeTransport {
|
|
@@ -2521,7 +5036,7 @@ class AcpxBridgeTransport {
|
|
|
2521
5036
|
async ensureSession(session) {
|
|
2522
5037
|
await this.client.request("ensureSession", this.toParams(session));
|
|
2523
5038
|
}
|
|
2524
|
-
async prompt(session, text) {
|
|
5039
|
+
async prompt(session, text, _reply) {
|
|
2525
5040
|
return await this.client.request("prompt", {
|
|
2526
5041
|
...this.toParams(session),
|
|
2527
5042
|
text
|
|
@@ -2565,121 +5080,62 @@ var init_spawn_command = __esm(() => {
|
|
|
2565
5080
|
SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
|
|
2566
5081
|
});
|
|
2567
5082
|
|
|
2568
|
-
// src/transport/prompt
|
|
2569
|
-
function
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
const stderrOutput = extractPromptOutput(result.stderr);
|
|
2579
|
-
const partialReply = [stdoutOutput, stderrOutput].filter((output) => output.hasAgentMessage && output.text.length > 0).map((output) => sanitizePromptText(output.text)).find((text) => text.length > 0);
|
|
2580
|
-
if (partialReply) {
|
|
2581
|
-
return partialReply;
|
|
2582
|
-
}
|
|
2583
|
-
throw new Error(`command failed with exit code ${result.code}`);
|
|
2584
|
-
}
|
|
2585
|
-
function normalizeCommandError(result) {
|
|
2586
|
-
const preferredError = extractPromptFailureMessage(result);
|
|
2587
|
-
if (preferredError) {
|
|
2588
|
-
return preferredError;
|
|
2589
|
-
}
|
|
2590
|
-
return result.stdout.trim() || null;
|
|
2591
|
-
}
|
|
2592
|
-
function extractPromptFailureMessage(result) {
|
|
2593
|
-
const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
|
|
2594
|
-
const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
|
|
2595
|
-
if (preferredMessage) {
|
|
2596
|
-
return preferredMessage;
|
|
2597
|
-
}
|
|
2598
|
-
if (rpcMessages.length > 0) {
|
|
2599
|
-
return rpcMessages[rpcMessages.length - 1] ?? null;
|
|
2600
|
-
}
|
|
2601
|
-
const stderrText = result.stderr.trim();
|
|
2602
|
-
if (stderrText.length > 0) {
|
|
2603
|
-
return stderrText;
|
|
2604
|
-
}
|
|
2605
|
-
return null;
|
|
2606
|
-
}
|
|
2607
|
-
function extractPromptOutput(output) {
|
|
2608
|
-
const lines = output.split(`
|
|
2609
|
-
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2610
|
-
const messageSegments = [];
|
|
2611
|
-
let currentSegment = "";
|
|
2612
|
-
let hasAgentMessage = false;
|
|
2613
|
-
for (const line of lines) {
|
|
2614
|
-
try {
|
|
2615
|
-
const event = JSON.parse(line);
|
|
2616
|
-
const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
|
|
2617
|
-
if (isMessageChunk) {
|
|
2618
|
-
hasAgentMessage = true;
|
|
2619
|
-
const chunk = event.params.update.content.text ?? "";
|
|
2620
|
-
if (chunk.length > 0) {
|
|
2621
|
-
currentSegment += chunk;
|
|
2622
|
-
}
|
|
2623
|
-
continue;
|
|
2624
|
-
}
|
|
2625
|
-
if (currentSegment.trim().length > 0) {
|
|
2626
|
-
messageSegments.push(currentSegment.trim());
|
|
2627
|
-
}
|
|
2628
|
-
currentSegment = "";
|
|
2629
|
-
} catch {
|
|
2630
|
-
if (currentSegment.trim().length > 0) {
|
|
2631
|
-
messageSegments.push(currentSegment.trim());
|
|
2632
|
-
currentSegment = "";
|
|
5083
|
+
// src/transport/streaming-prompt.ts
|
|
5084
|
+
function createStreamingPromptState() {
|
|
5085
|
+
return {
|
|
5086
|
+
buffer: "",
|
|
5087
|
+
segments: [],
|
|
5088
|
+
hasAgentMessage: false,
|
|
5089
|
+
pendingLine: "",
|
|
5090
|
+
finalize() {
|
|
5091
|
+
if (this.pendingLine.trim().length > 0) {
|
|
5092
|
+
parseStreamingChunks(this, this.pendingLine);
|
|
2633
5093
|
}
|
|
5094
|
+
const remaining = this.buffer.trim();
|
|
5095
|
+
this.buffer = "";
|
|
5096
|
+
this.pendingLine = "";
|
|
5097
|
+
return remaining;
|
|
2634
5098
|
}
|
|
2635
|
-
}
|
|
2636
|
-
if (currentSegment.trim().length > 0) {
|
|
2637
|
-
messageSegments.push(currentSegment.trim());
|
|
2638
|
-
}
|
|
2639
|
-
if (messageSegments.length > 0) {
|
|
2640
|
-
return {
|
|
2641
|
-
text: messageSegments[messageSegments.length - 1],
|
|
2642
|
-
hasAgentMessage
|
|
2643
|
-
};
|
|
2644
|
-
}
|
|
2645
|
-
return {
|
|
2646
|
-
text: output.trim(),
|
|
2647
|
-
hasAgentMessage
|
|
2648
5099
|
};
|
|
2649
5100
|
}
|
|
2650
|
-
function
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
return trimmed;
|
|
5101
|
+
function parseStreamingDataChunk(state, chunk) {
|
|
5102
|
+
state.pendingLine += chunk;
|
|
5103
|
+
let boundary;
|
|
5104
|
+
while ((boundary = state.pendingLine.indexOf(`
|
|
5105
|
+
`)) !== -1) {
|
|
5106
|
+
const line = state.pendingLine.slice(0, boundary);
|
|
5107
|
+
state.pendingLine = state.pendingLine.slice(boundary + 1);
|
|
5108
|
+
parseStreamingChunks(state, line);
|
|
2659
5109
|
}
|
|
2660
|
-
return paragraphs.slice(1).join(`
|
|
2661
|
-
|
|
2662
|
-
`).trim();
|
|
2663
5110
|
}
|
|
2664
|
-
function
|
|
2665
|
-
|
|
2666
|
-
|
|
5111
|
+
function parseStreamingChunks(state, line) {
|
|
5112
|
+
const trimmed = line.trim();
|
|
5113
|
+
if (trimmed.length === 0)
|
|
5114
|
+
return;
|
|
5115
|
+
let event;
|
|
5116
|
+
try {
|
|
5117
|
+
event = JSON.parse(trimmed);
|
|
5118
|
+
} catch {
|
|
5119
|
+
return;
|
|
2667
5120
|
}
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
5121
|
+
const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
|
|
5122
|
+
if (!isMessageChunk)
|
|
5123
|
+
return;
|
|
5124
|
+
state.hasAgentMessage = true;
|
|
5125
|
+
const chunk = event.params.update.content.text ?? "";
|
|
5126
|
+
if (chunk.length === 0)
|
|
5127
|
+
return;
|
|
5128
|
+
state.buffer += chunk;
|
|
5129
|
+
let boundary;
|
|
5130
|
+
while ((boundary = state.buffer.indexOf(`
|
|
5131
|
+
|
|
5132
|
+
`)) !== -1) {
|
|
5133
|
+
const segment = state.buffer.slice(0, boundary).trim();
|
|
5134
|
+
state.buffer = state.buffer.slice(boundary + 2);
|
|
5135
|
+
if (segment.length > 0) {
|
|
5136
|
+
state.segments.push(segment);
|
|
2680
5137
|
}
|
|
2681
|
-
|
|
2682
|
-
});
|
|
5138
|
+
}
|
|
2683
5139
|
}
|
|
2684
5140
|
|
|
2685
5141
|
// src/transport/acpx-cli/node-pty-helper.ts
|
|
@@ -2769,11 +5225,15 @@ async function defaultPtyRunner(command, args, options) {
|
|
|
2769
5225
|
class AcpxCliTransport {
|
|
2770
5226
|
command;
|
|
2771
5227
|
sessionInitTimeoutMs;
|
|
5228
|
+
permissionMode;
|
|
5229
|
+
nonInteractivePermissions;
|
|
2772
5230
|
runCommand;
|
|
2773
5231
|
runPtyCommand;
|
|
2774
5232
|
constructor(options, runCommand = defaultRunner, runPtyCommand = defaultPtyRunner) {
|
|
2775
5233
|
this.command = options.command ?? "acpx";
|
|
2776
5234
|
this.sessionInitTimeoutMs = options.sessionInitTimeoutMs ?? 120000;
|
|
5235
|
+
this.permissionMode = options.permissionMode ?? "approve-all";
|
|
5236
|
+
this.nonInteractivePermissions = options.nonInteractivePermissions ?? "fail";
|
|
2777
5237
|
this.runCommand = runCommand;
|
|
2778
5238
|
this.runPtyCommand = runPtyCommand;
|
|
2779
5239
|
}
|
|
@@ -2789,8 +5249,13 @@ class AcpxCliTransport {
|
|
|
2789
5249
|
timeoutMs: this.sessionInitTimeoutMs
|
|
2790
5250
|
});
|
|
2791
5251
|
}
|
|
2792
|
-
async prompt(session, text) {
|
|
2793
|
-
const
|
|
5252
|
+
async prompt(session, text, reply) {
|
|
5253
|
+
const args = this.buildPromptArgs(session, text);
|
|
5254
|
+
if (reply) {
|
|
5255
|
+
const result2 = await this.runStreamingPrompt(this.command, args, reply);
|
|
5256
|
+
return { text: getPromptText(result2) };
|
|
5257
|
+
}
|
|
5258
|
+
const result = await this.runCommand(this.command, args);
|
|
2794
5259
|
return { text: getPromptText(result) };
|
|
2795
5260
|
}
|
|
2796
5261
|
async cancel(session) {
|
|
@@ -2849,26 +5314,93 @@ class AcpxCliTransport {
|
|
|
2849
5314
|
})
|
|
2850
5315
|
]);
|
|
2851
5316
|
}
|
|
5317
|
+
async runStreamingPrompt(command, args, reply, maxSegmentWaitMs = 30000) {
|
|
5318
|
+
return await new Promise((resolve, reject) => {
|
|
5319
|
+
const spawnSpec = resolveSpawnCommand(command, args);
|
|
5320
|
+
const child = spawn3(spawnSpec.command, spawnSpec.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5321
|
+
let stdout = "";
|
|
5322
|
+
let stderr = "";
|
|
5323
|
+
const state = createStreamingPromptState();
|
|
5324
|
+
let lastReplyAt = Date.now();
|
|
5325
|
+
const flushBuffer = () => {
|
|
5326
|
+
const remaining = state.buffer.trim();
|
|
5327
|
+
if (remaining.length > 0) {
|
|
5328
|
+
state.buffer = "";
|
|
5329
|
+
reply(remaining).catch(() => {});
|
|
5330
|
+
lastReplyAt = Date.now();
|
|
5331
|
+
}
|
|
5332
|
+
};
|
|
5333
|
+
const timer = setInterval(() => {
|
|
5334
|
+
if (state.buffer.trim().length > 0 && Date.now() - lastReplyAt >= maxSegmentWaitMs) {
|
|
5335
|
+
flushBuffer();
|
|
5336
|
+
}
|
|
5337
|
+
}, 5000);
|
|
5338
|
+
child.stdout.setEncoding("utf8");
|
|
5339
|
+
child.stdout.on("data", (chunk) => {
|
|
5340
|
+
stdout += String(chunk);
|
|
5341
|
+
parseStreamingDataChunk(state, String(chunk));
|
|
5342
|
+
for (const segment of state.segments.splice(0)) {
|
|
5343
|
+
reply(segment).catch(() => {});
|
|
5344
|
+
lastReplyAt = Date.now();
|
|
5345
|
+
}
|
|
5346
|
+
});
|
|
5347
|
+
child.stderr.on("data", (chunk) => {
|
|
5348
|
+
stderr += String(chunk);
|
|
5349
|
+
});
|
|
5350
|
+
child.on("error", (err) => {
|
|
5351
|
+
clearInterval(timer);
|
|
5352
|
+
reject(err);
|
|
5353
|
+
});
|
|
5354
|
+
child.on("close", (code) => {
|
|
5355
|
+
clearInterval(timer);
|
|
5356
|
+
const remaining = state.finalize();
|
|
5357
|
+
if (remaining.length > 0) {
|
|
5358
|
+
reply(remaining).catch(() => {});
|
|
5359
|
+
}
|
|
5360
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
5361
|
+
});
|
|
5362
|
+
});
|
|
5363
|
+
}
|
|
2852
5364
|
buildArgs(session, tail) {
|
|
2853
|
-
const prefix = [
|
|
5365
|
+
const prefix = [
|
|
5366
|
+
"--format",
|
|
5367
|
+
"quiet",
|
|
5368
|
+
"--cwd",
|
|
5369
|
+
session.cwd,
|
|
5370
|
+
...this.buildPermissionArgs()
|
|
5371
|
+
];
|
|
2854
5372
|
if (session.agentCommand) {
|
|
2855
5373
|
return [...prefix, "--agent", session.agentCommand, ...tail];
|
|
2856
5374
|
}
|
|
2857
5375
|
return [...prefix, session.agent, ...tail];
|
|
2858
5376
|
}
|
|
2859
5377
|
buildPromptArgs(session, text) {
|
|
2860
|
-
const prefix = [
|
|
5378
|
+
const prefix = [
|
|
5379
|
+
"--format",
|
|
5380
|
+
"json",
|
|
5381
|
+
"--json-strict",
|
|
5382
|
+
"--cwd",
|
|
5383
|
+
session.cwd,
|
|
5384
|
+
...this.buildPermissionArgs()
|
|
5385
|
+
];
|
|
2861
5386
|
const tail = ["prompt", "-s", session.transportSession, text];
|
|
2862
5387
|
if (session.agentCommand) {
|
|
2863
5388
|
return [...prefix, "--agent", session.agentCommand, ...tail];
|
|
2864
5389
|
}
|
|
2865
5390
|
return [...prefix, session.agent, ...tail];
|
|
2866
5391
|
}
|
|
5392
|
+
buildPermissionArgs() {
|
|
5393
|
+
const modeFlag = this.permissionMode === "approve-reads" ? "--approve-reads" : this.permissionMode === "deny-all" ? "--deny-all" : "--approve-all";
|
|
5394
|
+
return [modeFlag, "--non-interactive-permissions", this.nonInteractivePermissions];
|
|
5395
|
+
}
|
|
2867
5396
|
}
|
|
2868
5397
|
function renderCommandForError(args) {
|
|
2869
5398
|
const rendered = [];
|
|
2870
5399
|
for (let index = 0;index < args.length; index += 1) {
|
|
2871
5400
|
const arg = args[index];
|
|
5401
|
+
if (arg === undefined) {
|
|
5402
|
+
continue;
|
|
5403
|
+
}
|
|
2872
5404
|
if (arg === "--format") {
|
|
2873
5405
|
index += 1;
|
|
2874
5406
|
continue;
|
|
@@ -2884,6 +5416,7 @@ function renderCommandForError(args) {
|
|
|
2884
5416
|
var require3;
|
|
2885
5417
|
var init_acpx_cli_transport = __esm(() => {
|
|
2886
5418
|
init_spawn_command();
|
|
5419
|
+
init_prompt_output();
|
|
2887
5420
|
init_node_pty_helper();
|
|
2888
5421
|
require3 = createRequire3(import.meta.url);
|
|
2889
5422
|
});
|
|
@@ -2897,14 +5430,14 @@ __export(exports_main, {
|
|
|
2897
5430
|
});
|
|
2898
5431
|
import { homedir as homedir2 } from "node:os";
|
|
2899
5432
|
import { dirname as dirname8, join as join4 } from "node:path";
|
|
2900
|
-
import { fileURLToPath as
|
|
5433
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2901
5434
|
async function buildApp(paths, deps = {}) {
|
|
2902
5435
|
await ensureConfigExists(paths.configPath);
|
|
2903
5436
|
const configStore = new ConfigStore(paths.configPath);
|
|
2904
5437
|
const config = await loadConfig(paths.configPath, {
|
|
2905
5438
|
defaultLoggingLevel: deps.defaultLoggingLevel
|
|
2906
5439
|
});
|
|
2907
|
-
const
|
|
5440
|
+
const logger2 = createAppLogger({
|
|
2908
5441
|
filePath: resolveAppLogPath(paths.configPath),
|
|
2909
5442
|
level: config.logging.level,
|
|
2910
5443
|
maxSizeBytes: config.logging.maxSizeBytes,
|
|
@@ -2912,24 +5445,26 @@ async function buildApp(paths, deps = {}) {
|
|
|
2912
5445
|
retentionDays: config.logging.retentionDays,
|
|
2913
5446
|
now: deps.loggerNow
|
|
2914
5447
|
});
|
|
2915
|
-
await
|
|
5448
|
+
await logger2.cleanup();
|
|
2916
5449
|
const acpxCommand = resolveAcpxCommand({ configuredCommand: config.transport.command });
|
|
2917
5450
|
const stateStore = new StateStore(paths.statePath);
|
|
2918
5451
|
const state = await stateStore.load();
|
|
2919
5452
|
const sessions = new SessionService(config, stateStore, state);
|
|
2920
5453
|
const transport = config.transport.type === "acpx-bridge" ? await (deps.createBridgeTransport?.() ?? Promise.resolve(new AcpxBridgeTransport(await spawnAcpxBridgeClient({
|
|
2921
5454
|
acpxCommand,
|
|
2922
|
-
bridgeEntryPath: resolveBridgeEntryPath()
|
|
5455
|
+
bridgeEntryPath: resolveBridgeEntryPath(),
|
|
5456
|
+
permissionMode: config.transport.permissionMode,
|
|
5457
|
+
nonInteractivePermissions: config.transport.nonInteractivePermissions
|
|
2923
5458
|
})))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config.transport, command: acpxCommand });
|
|
2924
|
-
const router = new CommandRouter(sessions, transport, config, configStore,
|
|
2925
|
-
const agent = new ConsoleAgent(router,
|
|
5459
|
+
const router = new CommandRouter(sessions, transport, config, configStore, logger2);
|
|
5460
|
+
const agent = new ConsoleAgent(router, logger2);
|
|
2926
5461
|
return {
|
|
2927
5462
|
agent,
|
|
2928
5463
|
router,
|
|
2929
5464
|
sessions,
|
|
2930
5465
|
stateStore,
|
|
2931
5466
|
configStore,
|
|
2932
|
-
logger,
|
|
5467
|
+
logger: logger2,
|
|
2933
5468
|
dispose: async () => {
|
|
2934
5469
|
if ("dispose" in transport && typeof transport.dispose === "function") {
|
|
2935
5470
|
await transport.dispose();
|
|
@@ -2967,9 +5502,9 @@ function resolveRuntimePaths() {
|
|
|
2967
5502
|
}
|
|
2968
5503
|
function resolveBridgeEntryPath() {
|
|
2969
5504
|
if (import.meta.url.includes("/dist/")) {
|
|
2970
|
-
return
|
|
5505
|
+
return fileURLToPath3(new URL("./bridge/bridge-main.js", import.meta.url));
|
|
2971
5506
|
}
|
|
2972
|
-
return
|
|
5507
|
+
return fileURLToPath3(new URL("./bridge/bridge-main.ts", import.meta.url));
|
|
2973
5508
|
}
|
|
2974
5509
|
function resolveAppLogPath(configPath) {
|
|
2975
5510
|
const rootDir = dirname8(configPath);
|
|
@@ -2988,13 +5523,14 @@ var init_main = __esm(async () => {
|
|
|
2988
5523
|
init_state_store();
|
|
2989
5524
|
init_acpx_bridge_client();
|
|
2990
5525
|
init_acpx_cli_transport();
|
|
5526
|
+
init_weixin_sdk();
|
|
2991
5527
|
if (false) {}
|
|
2992
5528
|
});
|
|
2993
5529
|
|
|
2994
5530
|
// src/cli.ts
|
|
2995
5531
|
import { homedir as homedir3 } from "node:os";
|
|
2996
5532
|
import { sep } from "node:path";
|
|
2997
|
-
import { fileURLToPath as
|
|
5533
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2998
5534
|
|
|
2999
5535
|
// src/daemon/create-daemon-controller.ts
|
|
3000
5536
|
import { mkdir as mkdir3, open } from "node:fs/promises";
|
|
@@ -3044,15 +5580,23 @@ class DaemonController {
|
|
|
3044
5580
|
startupPollIntervalMs;
|
|
3045
5581
|
startupTimeoutMs;
|
|
3046
5582
|
onStartupPoll;
|
|
5583
|
+
shutdownPollIntervalMs;
|
|
5584
|
+
shutdownTimeoutMs;
|
|
5585
|
+
onShutdownPoll;
|
|
3047
5586
|
constructor(paths, deps) {
|
|
3048
5587
|
this.paths = paths;
|
|
3049
5588
|
this.deps = deps;
|
|
3050
5589
|
this.statusStore = new DaemonStatusStore(paths.statusFile);
|
|
3051
5590
|
this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
|
|
3052
5591
|
this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
|
|
5592
|
+
this.shutdownPollIntervalMs = deps.shutdownPollIntervalMs ?? 50;
|
|
5593
|
+
this.shutdownTimeoutMs = deps.shutdownTimeoutMs ?? 5000;
|
|
3053
5594
|
this.onStartupPoll = deps.onStartupPoll ?? (async () => {
|
|
3054
5595
|
await new Promise((resolve) => setTimeout(resolve, this.startupPollIntervalMs));
|
|
3055
5596
|
});
|
|
5597
|
+
this.onShutdownPoll = deps.onShutdownPoll ?? (async () => {
|
|
5598
|
+
await new Promise((resolve) => setTimeout(resolve, this.shutdownPollIntervalMs));
|
|
5599
|
+
});
|
|
3056
5600
|
}
|
|
3057
5601
|
async getStatus() {
|
|
3058
5602
|
const pid = await this.loadPid();
|
|
@@ -3091,6 +5635,7 @@ class DaemonController {
|
|
|
3091
5635
|
}
|
|
3092
5636
|
if (this.deps.isProcessRunning(pid)) {
|
|
3093
5637
|
await this.deps.terminateProcess(pid);
|
|
5638
|
+
await this.waitForShutdown(pid);
|
|
3094
5639
|
}
|
|
3095
5640
|
await this.clearRuntimeFiles();
|
|
3096
5641
|
return { state: "stopped", detail: "stopped" };
|
|
@@ -3131,6 +5676,19 @@ class DaemonController {
|
|
|
3131
5676
|
}
|
|
3132
5677
|
throw new Error(`weacpx daemon did not report ready state within ${this.startupTimeoutMs}ms (pid ${pid})`);
|
|
3133
5678
|
}
|
|
5679
|
+
async waitForShutdown(pid) {
|
|
5680
|
+
const deadline = Date.now() + this.shutdownTimeoutMs;
|
|
5681
|
+
while (Date.now() < deadline) {
|
|
5682
|
+
if (!this.deps.isProcessRunning(pid)) {
|
|
5683
|
+
return;
|
|
5684
|
+
}
|
|
5685
|
+
await this.onShutdownPoll();
|
|
5686
|
+
}
|
|
5687
|
+
if (!this.deps.isProcessRunning(pid)) {
|
|
5688
|
+
return;
|
|
5689
|
+
}
|
|
5690
|
+
throw new Error(`weacpx daemon did not exit within ${this.shutdownTimeoutMs}ms (pid ${pid})`);
|
|
5691
|
+
}
|
|
3134
5692
|
}
|
|
3135
5693
|
|
|
3136
5694
|
// src/daemon/create-daemon-controller.ts
|
|
@@ -3366,7 +5924,7 @@ class DaemonRuntime {
|
|
|
3366
5924
|
}
|
|
3367
5925
|
|
|
3368
5926
|
// src/cli.ts
|
|
3369
|
-
var HELP_LINES = ["用法:", "weacpx login", "weacpx run", "weacpx start", "weacpx status", "weacpx stop"];
|
|
5927
|
+
var HELP_LINES = ["用法:", "weacpx login", "weacpx logout", "weacpx run", "weacpx start", "weacpx status", "weacpx stop"];
|
|
3370
5928
|
async function runCli(args, deps = {}) {
|
|
3371
5929
|
const command = args[0];
|
|
3372
5930
|
const print = deps.print ?? ((line) => console.log(line));
|
|
@@ -3375,6 +5933,9 @@ async function runCli(args, deps = {}) {
|
|
|
3375
5933
|
case "login":
|
|
3376
5934
|
await (deps.login ?? defaultLogin)();
|
|
3377
5935
|
return 0;
|
|
5936
|
+
case "logout":
|
|
5937
|
+
await (deps.logout ?? defaultLogout)();
|
|
5938
|
+
return 0;
|
|
3378
5939
|
case "run":
|
|
3379
5940
|
await (deps.run ?? defaultRun)();
|
|
3380
5941
|
return 0;
|
|
@@ -3426,10 +5987,14 @@ async function defaultLogin() {
|
|
|
3426
5987
|
const { main: main3 } = await init_login().then(() => exports_login);
|
|
3427
5988
|
await main3();
|
|
3428
5989
|
}
|
|
5990
|
+
async function defaultLogout() {
|
|
5991
|
+
const { logout: logout2 } = await Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk));
|
|
5992
|
+
logout2();
|
|
5993
|
+
}
|
|
3429
5994
|
async function defaultRun() {
|
|
3430
5995
|
const [{ buildApp: buildApp2, resolveRuntimePaths: resolveRuntimePaths2 }, { loadWeixinSdk: loadWeixinSdk2 }, { runConsole: runConsole2 }] = await Promise.all([
|
|
3431
5996
|
init_main().then(() => exports_main),
|
|
3432
|
-
Promise.resolve().then(() => exports_weixin_sdk),
|
|
5997
|
+
Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk)),
|
|
3433
5998
|
Promise.resolve().then(() => exports_run_console)
|
|
3434
5999
|
]);
|
|
3435
6000
|
const runtimePaths = resolveRuntimePaths2();
|
|
@@ -3463,7 +6028,7 @@ function resolveCliEntryPath() {
|
|
|
3463
6028
|
if (process.argv[1]) {
|
|
3464
6029
|
return process.argv[1];
|
|
3465
6030
|
}
|
|
3466
|
-
return
|
|
6031
|
+
return fileURLToPath4(import.meta.url);
|
|
3467
6032
|
}
|
|
3468
6033
|
if (__require.main == __require.module) {
|
|
3469
6034
|
process.exitCode = await runCli(process.argv.slice(2));
|