weacpx 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -9
- package/dist/bridge/bridge-main.js +181 -10
- package/dist/cli.js +2853 -231
- 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();
|
|
1055
1553
|
});
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
+
}
|
|
1060
2560
|
}
|
|
1061
|
-
|
|
1062
|
-
return
|
|
2561
|
+
logger.debug(`${label}: success to=${to} clientId=${lastClientId}`);
|
|
2562
|
+
return { messageId: lastClientId };
|
|
1063
2563
|
}
|
|
1064
|
-
function
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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" });
|
|
1069
2583
|
}
|
|
1070
|
-
async function
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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)
|
|
1079
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)}`);
|
|
1080
2645
|
}
|
|
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
2646
|
}
|
|
2647
|
+
var init_error_notice = __esm(() => {
|
|
2648
|
+
init_logger();
|
|
2649
|
+
init_send();
|
|
2650
|
+
});
|
|
1089
2651
|
|
|
1090
|
-
// src/
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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();
|
|
1095
2733
|
});
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
+
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)}`);
|
|
1099
2798
|
try {
|
|
1100
|
-
|
|
1101
|
-
const accounts = await import(new URL("./src/auth/accounts.ts", sourceUrl).href);
|
|
1102
|
-
const loginQr = await import(new URL("./src/auth/login-qr.ts", sourceUrl).href);
|
|
1103
|
-
return { accounts, loginQr };
|
|
2799
|
+
await sendReply(ctx, `❌ 指令执行失败: ${String(err).slice(0, 200)}`);
|
|
1104
2800
|
} catch {}
|
|
2801
|
+
return { handled: true };
|
|
1105
2802
|
}
|
|
1106
|
-
return null;
|
|
1107
2803
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
return "";
|
|
2838
|
+
}
|
|
2839
|
+
function findMediaItem(itemList) {
|
|
2840
|
+
if (!itemList?.length)
|
|
1113
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;
|
|
1114
2865
|
}
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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) {
|
|
2873
|
+
try {
|
|
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(() => {});
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
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
|
+
});
|
|
3002
|
+
|
|
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();
|
|
3047
|
+
});
|
|
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) {
|
|
3080
|
+
try {
|
|
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
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
aLog.info(`Monitor ended`);
|
|
3150
|
+
}
|
|
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,6 +3351,9 @@ 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
3359
|
"/cancel 或 /stop"
|
|
@@ -1242,19 +3392,19 @@ function createAppLogger(options) {
|
|
|
1242
3392
|
const now = options.now ?? (() => new Date);
|
|
1243
3393
|
return {
|
|
1244
3394
|
debug: async (event, message, context) => {
|
|
1245
|
-
await
|
|
3395
|
+
await writeLog2("debug", event, message, context);
|
|
1246
3396
|
},
|
|
1247
3397
|
info: async (event, message, context) => {
|
|
1248
|
-
await
|
|
3398
|
+
await writeLog2("info", event, message, context);
|
|
1249
3399
|
},
|
|
1250
3400
|
error: async (event, message, context) => {
|
|
1251
|
-
await
|
|
3401
|
+
await writeLog2("error", event, message, context);
|
|
1252
3402
|
},
|
|
1253
3403
|
cleanup: async () => {
|
|
1254
3404
|
await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays, now);
|
|
1255
3405
|
}
|
|
1256
3406
|
};
|
|
1257
|
-
async function
|
|
3407
|
+
async function writeLog2(level, event, message, context = {}) {
|
|
1258
3408
|
if (LEVEL_ORDER[level] > LEVEL_ORDER[options.level]) {
|
|
1259
3409
|
return;
|
|
1260
3410
|
}
|
|
@@ -1333,17 +3483,149 @@ function formatValue(value) {
|
|
|
1333
3483
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
1334
3484
|
return String(value);
|
|
1335
3485
|
}
|
|
1336
|
-
return JSON.stringify(value);
|
|
3486
|
+
return JSON.stringify(value);
|
|
3487
|
+
}
|
|
3488
|
+
function isMissingFileError(error) {
|
|
3489
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3490
|
+
}
|
|
3491
|
+
var LEVEL_ORDER;
|
|
3492
|
+
var init_app_logger = __esm(() => {
|
|
3493
|
+
LEVEL_ORDER = {
|
|
3494
|
+
error: 0,
|
|
3495
|
+
info: 1,
|
|
3496
|
+
debug: 2
|
|
3497
|
+
};
|
|
3498
|
+
});
|
|
3499
|
+
|
|
3500
|
+
// src/transport/prompt-output.ts
|
|
3501
|
+
function getPromptText(result) {
|
|
3502
|
+
const stdoutOutput = extractPromptOutput(result.stdout);
|
|
3503
|
+
if (result.code === 0) {
|
|
3504
|
+
return sanitizePromptText(stdoutOutput.text);
|
|
3505
|
+
}
|
|
3506
|
+
const preferredError = extractPromptFailureMessage(result);
|
|
3507
|
+
if (preferredError) {
|
|
3508
|
+
throw new PromptCommandError(preferredError, result);
|
|
3509
|
+
}
|
|
3510
|
+
const stderrOutput = extractPromptOutput(result.stderr);
|
|
3511
|
+
const partialReply = [stdoutOutput, stderrOutput].filter((output) => output.hasAgentMessage && output.text.length > 0).map((output) => sanitizePromptText(output.text)).find((text) => text.length > 0);
|
|
3512
|
+
if (partialReply) {
|
|
3513
|
+
return partialReply;
|
|
3514
|
+
}
|
|
3515
|
+
throw new PromptCommandError(`command failed with exit code ${result.code}`, result);
|
|
3516
|
+
}
|
|
3517
|
+
function normalizeCommandError(result) {
|
|
3518
|
+
const preferredError = extractPromptFailureMessage(result);
|
|
3519
|
+
if (preferredError) {
|
|
3520
|
+
return preferredError;
|
|
3521
|
+
}
|
|
3522
|
+
return result.stdout.trim() || null;
|
|
3523
|
+
}
|
|
3524
|
+
function extractPromptFailureMessage(result) {
|
|
3525
|
+
const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
|
|
3526
|
+
const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
|
|
3527
|
+
if (preferredMessage) {
|
|
3528
|
+
return preferredMessage;
|
|
3529
|
+
}
|
|
3530
|
+
if (rpcMessages.length > 0) {
|
|
3531
|
+
return rpcMessages[rpcMessages.length - 1] ?? null;
|
|
3532
|
+
}
|
|
3533
|
+
const stderrText = result.stderr.trim();
|
|
3534
|
+
if (stderrText.length > 0) {
|
|
3535
|
+
return stderrText;
|
|
3536
|
+
}
|
|
3537
|
+
return null;
|
|
3538
|
+
}
|
|
3539
|
+
function extractPromptOutput(output) {
|
|
3540
|
+
const lines = output.split(`
|
|
3541
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
3542
|
+
const messageSegments = [];
|
|
3543
|
+
let currentSegment = "";
|
|
3544
|
+
let hasAgentMessage = false;
|
|
3545
|
+
for (const line of lines) {
|
|
3546
|
+
try {
|
|
3547
|
+
const event = JSON.parse(line);
|
|
3548
|
+
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";
|
|
3549
|
+
if (isMessageChunk) {
|
|
3550
|
+
hasAgentMessage = true;
|
|
3551
|
+
const chunk = event.params.update.content.text ?? "";
|
|
3552
|
+
if (chunk.length > 0) {
|
|
3553
|
+
currentSegment += chunk;
|
|
3554
|
+
}
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
if (currentSegment.trim().length > 0) {
|
|
3558
|
+
messageSegments.push(currentSegment.trim());
|
|
3559
|
+
}
|
|
3560
|
+
currentSegment = "";
|
|
3561
|
+
} catch {
|
|
3562
|
+
if (currentSegment.trim().length > 0) {
|
|
3563
|
+
messageSegments.push(currentSegment.trim());
|
|
3564
|
+
currentSegment = "";
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
if (currentSegment.trim().length > 0) {
|
|
3569
|
+
messageSegments.push(currentSegment.trim());
|
|
3570
|
+
}
|
|
3571
|
+
if (messageSegments.length > 0) {
|
|
3572
|
+
return {
|
|
3573
|
+
text: messageSegments[messageSegments.length - 1],
|
|
3574
|
+
hasAgentMessage
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
return {
|
|
3578
|
+
text: output.trim(),
|
|
3579
|
+
hasAgentMessage
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
function sanitizePromptText(text) {
|
|
3583
|
+
const trimmed = text.trim();
|
|
3584
|
+
const paragraphs = trimmed.split(/\n\s*\n/);
|
|
3585
|
+
if (paragraphs.length < 2) {
|
|
3586
|
+
return trimmed;
|
|
3587
|
+
}
|
|
3588
|
+
const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
|
|
3589
|
+
if (!looksLikeWorkflowPreamble(firstParagraph)) {
|
|
3590
|
+
return trimmed;
|
|
3591
|
+
}
|
|
3592
|
+
return paragraphs.slice(1).join(`
|
|
3593
|
+
|
|
3594
|
+
`).trim();
|
|
3595
|
+
}
|
|
3596
|
+
function looksLikeWorkflowPreamble(paragraph) {
|
|
3597
|
+
if (!paragraph.startsWith("using ")) {
|
|
3598
|
+
return false;
|
|
3599
|
+
}
|
|
3600
|
+
return paragraph.includes("using-superpowers") || paragraph.includes("repo workflow requirement") || paragraph.includes("workflow requirement") || paragraph.includes("before responding") || paragraph.includes("skill check");
|
|
1337
3601
|
}
|
|
1338
|
-
function
|
|
1339
|
-
return
|
|
3602
|
+
function extractJsonRpcErrorMessages(output) {
|
|
3603
|
+
return output.split(`
|
|
3604
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
|
|
3605
|
+
try {
|
|
3606
|
+
const payload = JSON.parse(line);
|
|
3607
|
+
if (typeof payload.error?.message === "string" && payload.error.message.length > 0) {
|
|
3608
|
+
return [payload.error.message];
|
|
3609
|
+
}
|
|
3610
|
+
} catch {
|
|
3611
|
+
return [];
|
|
3612
|
+
}
|
|
3613
|
+
return [];
|
|
3614
|
+
});
|
|
1340
3615
|
}
|
|
1341
|
-
var
|
|
1342
|
-
var
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
3616
|
+
var PromptCommandError;
|
|
3617
|
+
var init_prompt_output = __esm(() => {
|
|
3618
|
+
PromptCommandError = class PromptCommandError extends Error {
|
|
3619
|
+
exitCode;
|
|
3620
|
+
stdout;
|
|
3621
|
+
stderr;
|
|
3622
|
+
constructor(message, result) {
|
|
3623
|
+
super(message);
|
|
3624
|
+
this.name = "PromptCommandError";
|
|
3625
|
+
this.exitCode = result.code;
|
|
3626
|
+
this.stdout = result.stdout;
|
|
3627
|
+
this.stderr = result.stderr;
|
|
3628
|
+
}
|
|
1347
3629
|
};
|
|
1348
3630
|
});
|
|
1349
3631
|
|
|
@@ -1367,10 +3649,27 @@ function parseCommand(input) {
|
|
|
1367
3649
|
return { kind: "status" };
|
|
1368
3650
|
if (command === "/cancel")
|
|
1369
3651
|
return { kind: "cancel" };
|
|
3652
|
+
if (command === "/permission" && parts.length === 1)
|
|
3653
|
+
return { kind: "permission.status" };
|
|
1370
3654
|
if (command === "/session" && parts.length === 1)
|
|
1371
3655
|
return { kind: "sessions" };
|
|
1372
3656
|
if (command === "/workspace" && parts.length === 1)
|
|
1373
3657
|
return { kind: "workspaces" };
|
|
3658
|
+
if (command === "/permission" && parts[1] === "set") {
|
|
3659
|
+
const mode = toPermissionMode(parts[2] ?? "");
|
|
3660
|
+
if (mode) {
|
|
3661
|
+
return { kind: "permission.mode.set", mode };
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
if (command === "/permission" && parts[1] === "auto") {
|
|
3665
|
+
if (parts.length === 2) {
|
|
3666
|
+
return { kind: "permission.auto.status" };
|
|
3667
|
+
}
|
|
3668
|
+
const policy = toNonInteractivePermission(parts[2] ?? "");
|
|
3669
|
+
if (policy) {
|
|
3670
|
+
return { kind: "permission.auto.set", policy };
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
1374
3673
|
if (command === "/use" && parts[1]) {
|
|
1375
3674
|
return { kind: "session.use", alias: parts[1] };
|
|
1376
3675
|
}
|
|
@@ -1440,6 +3739,9 @@ function parseCommand(input) {
|
|
|
1440
3739
|
}
|
|
1441
3740
|
return { kind: "session.attach", alias, agent, workspace, transportSession };
|
|
1442
3741
|
}
|
|
3742
|
+
if (command.startsWith("/") && isRecognizedCommand(command)) {
|
|
3743
|
+
return { kind: "invalid", text: trimmed, recognizedCommand: command };
|
|
3744
|
+
}
|
|
1443
3745
|
return { kind: "prompt", text: trimmed };
|
|
1444
3746
|
}
|
|
1445
3747
|
function hasAnyFlag(parts, flags) {
|
|
@@ -1458,10 +3760,30 @@ function normalizeCommand(command) {
|
|
|
1458
3760
|
return "/session";
|
|
1459
3761
|
if (command === "/ws")
|
|
1460
3762
|
return "/workspace";
|
|
3763
|
+
if (command === "/pm")
|
|
3764
|
+
return "/permission";
|
|
1461
3765
|
if (command === "/stop")
|
|
1462
3766
|
return "/cancel";
|
|
1463
3767
|
return command;
|
|
1464
3768
|
}
|
|
3769
|
+
function isRecognizedCommand(command) {
|
|
3770
|
+
return RECOGNIZED_COMMANDS.has(command);
|
|
3771
|
+
}
|
|
3772
|
+
function toPermissionMode(value) {
|
|
3773
|
+
if (value === "allow")
|
|
3774
|
+
return "approve-all";
|
|
3775
|
+
if (value === "read")
|
|
3776
|
+
return "approve-reads";
|
|
3777
|
+
if (value === "deny")
|
|
3778
|
+
return "deny-all";
|
|
3779
|
+
return null;
|
|
3780
|
+
}
|
|
3781
|
+
function toNonInteractivePermission(value) {
|
|
3782
|
+
if (value === "allow" || value === "deny" || value === "fail") {
|
|
3783
|
+
return value;
|
|
3784
|
+
}
|
|
3785
|
+
return null;
|
|
3786
|
+
}
|
|
1465
3787
|
function tokenizeCommand(input) {
|
|
1466
3788
|
const tokens = [];
|
|
1467
3789
|
let current = "";
|
|
@@ -1493,10 +3815,27 @@ function tokenizeCommand(input) {
|
|
|
1493
3815
|
}
|
|
1494
3816
|
return tokens;
|
|
1495
3817
|
}
|
|
3818
|
+
var RECOGNIZED_COMMANDS;
|
|
3819
|
+
var init_parse_command = __esm(() => {
|
|
3820
|
+
RECOGNIZED_COMMANDS = new Set([
|
|
3821
|
+
"/help",
|
|
3822
|
+
"/agents",
|
|
3823
|
+
"/workspaces",
|
|
3824
|
+
"/sessions",
|
|
3825
|
+
"/status",
|
|
3826
|
+
"/cancel",
|
|
3827
|
+
"/permission",
|
|
3828
|
+
"/session",
|
|
3829
|
+
"/workspace",
|
|
3830
|
+
"/use",
|
|
3831
|
+
"/agent"
|
|
3832
|
+
]);
|
|
3833
|
+
});
|
|
1496
3834
|
|
|
1497
3835
|
// src/commands/command-router.ts
|
|
1498
3836
|
import { access } from "node:fs/promises";
|
|
1499
3837
|
import { basename as basename2, normalize } from "node:path";
|
|
3838
|
+
import { homedir } from "node:os";
|
|
1500
3839
|
|
|
1501
3840
|
class CommandRouter {
|
|
1502
3841
|
sessions;
|
|
@@ -1504,14 +3843,14 @@ class CommandRouter {
|
|
|
1504
3843
|
config;
|
|
1505
3844
|
configStore;
|
|
1506
3845
|
logger;
|
|
1507
|
-
constructor(sessions, transport, config, configStore,
|
|
3846
|
+
constructor(sessions, transport, config, configStore, logger2) {
|
|
1508
3847
|
this.sessions = sessions;
|
|
1509
3848
|
this.transport = transport;
|
|
1510
3849
|
this.config = config;
|
|
1511
3850
|
this.configStore = configStore;
|
|
1512
|
-
this.logger =
|
|
3851
|
+
this.logger = logger2 ?? createNoopAppLogger();
|
|
1513
3852
|
}
|
|
1514
|
-
async handle(chatKey, input) {
|
|
3853
|
+
async handle(chatKey, input, reply) {
|
|
1515
3854
|
const startedAt = Date.now();
|
|
1516
3855
|
const command = parseCommand(input);
|
|
1517
3856
|
await this.logger.debug("command.parsed", "parsed inbound command", {
|
|
@@ -1520,6 +3859,19 @@ class CommandRouter {
|
|
|
1520
3859
|
});
|
|
1521
3860
|
return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
|
|
1522
3861
|
switch (command.kind) {
|
|
3862
|
+
case "invalid":
|
|
3863
|
+
return {
|
|
3864
|
+
text: [
|
|
3865
|
+
"无法识别的命令格式。",
|
|
3866
|
+
"",
|
|
3867
|
+
"正确的会话创建格式:",
|
|
3868
|
+
"/session new <别名> --agent <Agent名> --ws <工作区名>",
|
|
3869
|
+
"",
|
|
3870
|
+
"例如:",
|
|
3871
|
+
"/session new demo --agent claude --ws weacpx"
|
|
3872
|
+
].join(`
|
|
3873
|
+
`)
|
|
3874
|
+
};
|
|
1523
3875
|
case "help":
|
|
1524
3876
|
return { text: renderHelpText() };
|
|
1525
3877
|
case "agents":
|
|
@@ -1547,16 +3899,41 @@ class CommandRouter {
|
|
|
1547
3899
|
this.replaceConfig(updated);
|
|
1548
3900
|
return { text: `Agent「${command.name}」已删除` };
|
|
1549
3901
|
}
|
|
3902
|
+
case "permission.status":
|
|
3903
|
+
return { text: this.renderPermissionStatus("当前权限模式:") };
|
|
3904
|
+
case "permission.mode.set": {
|
|
3905
|
+
if (!this.config || !this.configStore) {
|
|
3906
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
3907
|
+
}
|
|
3908
|
+
const updated = await this.configStore.updateTransport({
|
|
3909
|
+
permissionMode: command.mode
|
|
3910
|
+
});
|
|
3911
|
+
this.replaceConfig(updated);
|
|
3912
|
+
return { text: this.renderPermissionStatus("权限模式已更新:") };
|
|
3913
|
+
}
|
|
3914
|
+
case "permission.auto.status":
|
|
3915
|
+
return { text: this.renderPermissionStatus("当前非交互策略:") };
|
|
3916
|
+
case "permission.auto.set": {
|
|
3917
|
+
if (!this.config || !this.configStore) {
|
|
3918
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
3919
|
+
}
|
|
3920
|
+
const updated = await this.configStore.updateTransport({
|
|
3921
|
+
nonInteractivePermissions: command.policy
|
|
3922
|
+
});
|
|
3923
|
+
this.replaceConfig(updated);
|
|
3924
|
+
return { text: this.renderPermissionStatus("非交互策略已更新:") };
|
|
3925
|
+
}
|
|
1550
3926
|
case "workspaces":
|
|
1551
3927
|
return { text: this.config ? renderWorkspaces(this.config) : "No config loaded." };
|
|
1552
3928
|
case "workspace.new": {
|
|
1553
3929
|
if (!this.config || !this.configStore) {
|
|
1554
3930
|
return { text: "当前没有加载可写入的配置。" };
|
|
1555
3931
|
}
|
|
1556
|
-
|
|
3932
|
+
const wsCwd = normalizePathForWorkspace(command.cwd);
|
|
3933
|
+
if (!await pathExists(wsCwd)) {
|
|
1557
3934
|
return { text: `工作区路径不存在:${command.cwd}` };
|
|
1558
3935
|
}
|
|
1559
|
-
const updated = await this.configStore.upsertWorkspace(command.name,
|
|
3936
|
+
const updated = await this.configStore.upsertWorkspace(command.name, wsCwd);
|
|
1560
3937
|
this.replaceConfig(updated);
|
|
1561
3938
|
return { text: `工作区「${command.name}」已保存` };
|
|
1562
3939
|
}
|
|
@@ -1667,8 +4044,8 @@ class CommandRouter {
|
|
|
1667
4044
|
return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
|
|
1668
4045
|
}
|
|
1669
4046
|
try {
|
|
1670
|
-
const
|
|
1671
|
-
return { text:
|
|
4047
|
+
const result = await this.promptTransportSession(session, command.text, reply);
|
|
4048
|
+
return { text: result.text };
|
|
1672
4049
|
} catch (error) {
|
|
1673
4050
|
return this.renderTransportError(session, error);
|
|
1674
4051
|
}
|
|
@@ -1743,6 +4120,12 @@ class CommandRouter {
|
|
|
1743
4120
|
this.config.agents = { ...updated.agents };
|
|
1744
4121
|
this.config.workspaces = { ...updated.workspaces };
|
|
1745
4122
|
}
|
|
4123
|
+
renderPermissionStatus(title) {
|
|
4124
|
+
const permissionMode = this.config?.transport.permissionMode ?? "approve-all";
|
|
4125
|
+
const nonInteractivePermissions = this.config?.transport.nonInteractivePermissions ?? "fail";
|
|
4126
|
+
return [title, `- mode: ${permissionMode}`, `- auto: ${nonInteractivePermissions}`].join(`
|
|
4127
|
+
`);
|
|
4128
|
+
}
|
|
1746
4129
|
renderTransportError(session, error) {
|
|
1747
4130
|
const message = error instanceof Error ? error.message : String(error);
|
|
1748
4131
|
if (message.includes("No acpx session found")) {
|
|
@@ -1755,7 +4138,17 @@ class CommandRouter {
|
|
|
1755
4138
|
`)
|
|
1756
4139
|
};
|
|
1757
4140
|
}
|
|
1758
|
-
|
|
4141
|
+
if (!isPartialPromptOutputError(message)) {
|
|
4142
|
+
throw error;
|
|
4143
|
+
}
|
|
4144
|
+
return {
|
|
4145
|
+
text: [
|
|
4146
|
+
`当前会话「${session.alias}」执行中断,未收到最终回复。`,
|
|
4147
|
+
"请直接重试;如果长时间无响应,可先发送 /cancel 后再重试。",
|
|
4148
|
+
`错误信息:${summarizeTransportError(message)}`
|
|
4149
|
+
].join(`
|
|
4150
|
+
`)
|
|
4151
|
+
};
|
|
1759
4152
|
}
|
|
1760
4153
|
renderSessionCreationError(session, error) {
|
|
1761
4154
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1851,8 +4244,8 @@ class CommandRouter {
|
|
|
1851
4244
|
async checkTransportSession(session) {
|
|
1852
4245
|
return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
|
|
1853
4246
|
}
|
|
1854
|
-
async promptTransportSession(session, text) {
|
|
1855
|
-
return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text));
|
|
4247
|
+
async promptTransportSession(session, text, reply) {
|
|
4248
|
+
return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text, reply));
|
|
1856
4249
|
}
|
|
1857
4250
|
async cancelTransportSession(session) {
|
|
1858
4251
|
return await this.measureTransportCall("cancel", session, () => this.transport.cancel(session));
|
|
@@ -1870,28 +4263,41 @@ class CommandRouter {
|
|
|
1870
4263
|
});
|
|
1871
4264
|
return result;
|
|
1872
4265
|
} catch (error) {
|
|
4266
|
+
const diagnosticContext = error instanceof PromptCommandError ? {
|
|
4267
|
+
exitCode: error.exitCode,
|
|
4268
|
+
stdoutPreview: summarizeTransportDiagnostic(error.stdout),
|
|
4269
|
+
stdoutTailPreview: summarizeTransportDiagnosticTail(error.stdout),
|
|
4270
|
+
stdoutLength: error.stdout.length,
|
|
4271
|
+
...summarizeTransportNdjson(error.stdout, "stdout"),
|
|
4272
|
+
stderrPreview: summarizeTransportDiagnostic(error.stderr),
|
|
4273
|
+
stderrTailPreview: summarizeTransportDiagnosticTail(error.stderr),
|
|
4274
|
+
stderrLength: error.stderr.length,
|
|
4275
|
+
...summarizeTransportNdjson(error.stderr, "stderr")
|
|
4276
|
+
} : {};
|
|
1873
4277
|
await this.logger.error(`transport.${operation}.failed`, "transport operation failed", {
|
|
1874
4278
|
operation,
|
|
1875
4279
|
agent: session.agent,
|
|
1876
4280
|
workspace: session.workspace,
|
|
1877
4281
|
alias: session.alias,
|
|
1878
4282
|
durationMs: Date.now() - startedAt,
|
|
1879
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4283
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4284
|
+
...diagnosticContext
|
|
1880
4285
|
});
|
|
1881
4286
|
throw error;
|
|
1882
4287
|
}
|
|
1883
4288
|
}
|
|
1884
4289
|
}
|
|
1885
|
-
async function pathExists(
|
|
4290
|
+
async function pathExists(path11) {
|
|
1886
4291
|
try {
|
|
1887
|
-
await access(
|
|
4292
|
+
await access(path11);
|
|
1888
4293
|
return true;
|
|
1889
4294
|
} catch {
|
|
1890
4295
|
return false;
|
|
1891
4296
|
}
|
|
1892
4297
|
}
|
|
1893
|
-
function normalizePathForWorkspace(
|
|
1894
|
-
|
|
4298
|
+
function normalizePathForWorkspace(path11) {
|
|
4299
|
+
const expanded = path11.startsWith("~") ? homedir() + path11.slice(1) : path11;
|
|
4300
|
+
return normalize(expanded);
|
|
1895
4301
|
}
|
|
1896
4302
|
function sameWorkspacePath(left, right) {
|
|
1897
4303
|
const normalizedLeft = normalizePathForWorkspace(left);
|
|
@@ -1901,9 +4307,70 @@ function sameWorkspacePath(left, right) {
|
|
|
1901
4307
|
}
|
|
1902
4308
|
return normalizedLeft === normalizedRight;
|
|
1903
4309
|
}
|
|
4310
|
+
function summarizeTransportError(message) {
|
|
4311
|
+
return message.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
4312
|
+
}
|
|
4313
|
+
function summarizeTransportDiagnostic(output) {
|
|
4314
|
+
const trimmed = output.replace(/\s+/g, " ").trim();
|
|
4315
|
+
if (trimmed.length === 0) {
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
return trimmed.slice(0, 200);
|
|
4319
|
+
}
|
|
4320
|
+
function summarizeTransportDiagnosticTail(output) {
|
|
4321
|
+
const trimmed = output.replace(/\s+/g, " ").trim();
|
|
4322
|
+
if (trimmed.length === 0) {
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
return trimmed.slice(-200);
|
|
4326
|
+
}
|
|
4327
|
+
function summarizeTransportNdjson(output, prefix) {
|
|
4328
|
+
const lines = output.split(`
|
|
4329
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
4330
|
+
if (lines.length === 0) {
|
|
4331
|
+
return {};
|
|
4332
|
+
}
|
|
4333
|
+
const methods = new Set;
|
|
4334
|
+
let agentMessageChunkCount = 0;
|
|
4335
|
+
let stopReason;
|
|
4336
|
+
for (const line of lines) {
|
|
4337
|
+
try {
|
|
4338
|
+
const payload = JSON.parse(line);
|
|
4339
|
+
if (typeof payload.method === "string" && payload.method.length > 0) {
|
|
4340
|
+
methods.add(payload.method);
|
|
4341
|
+
}
|
|
4342
|
+
if (payload.params?.update?.sessionUpdate === "agent_message_chunk") {
|
|
4343
|
+
agentMessageChunkCount += 1;
|
|
4344
|
+
}
|
|
4345
|
+
if (typeof payload.result?.stopReason === "string" && payload.result.stopReason.length > 0) {
|
|
4346
|
+
stopReason = payload.result.stopReason;
|
|
4347
|
+
}
|
|
4348
|
+
} catch {
|
|
4349
|
+
continue;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
const summary = {
|
|
4353
|
+
[`${prefix}LineCount`]: lines.length
|
|
4354
|
+
};
|
|
4355
|
+
if (methods.size > 0) {
|
|
4356
|
+
summary[`${prefix}Methods`] = [...methods].join(",");
|
|
4357
|
+
}
|
|
4358
|
+
if (agentMessageChunkCount > 0) {
|
|
4359
|
+
summary[`${prefix}AgentMessageChunkCount`] = agentMessageChunkCount;
|
|
4360
|
+
}
|
|
4361
|
+
if (stopReason) {
|
|
4362
|
+
summary[`${prefix}StopReason`] = stopReason;
|
|
4363
|
+
}
|
|
4364
|
+
return summary;
|
|
4365
|
+
}
|
|
4366
|
+
function isPartialPromptOutputError(message) {
|
|
4367
|
+
return message.includes("未收到最终回复");
|
|
4368
|
+
}
|
|
1904
4369
|
var init_command_router = __esm(() => {
|
|
1905
4370
|
init_agent_templates();
|
|
1906
4371
|
init_app_logger();
|
|
4372
|
+
init_prompt_output();
|
|
4373
|
+
init_parse_command();
|
|
1907
4374
|
});
|
|
1908
4375
|
|
|
1909
4376
|
// src/config/resolve-agent-command.ts
|
|
@@ -1926,8 +4393,8 @@ import { readFile as readFile3 } from "node:fs/promises";
|
|
|
1926
4393
|
function isRecord(value) {
|
|
1927
4394
|
return typeof value === "object" && value !== null;
|
|
1928
4395
|
}
|
|
1929
|
-
async function loadConfig(
|
|
1930
|
-
const raw = JSON.parse(await readFile3(
|
|
4396
|
+
async function loadConfig(path11, options = {}) {
|
|
4397
|
+
const raw = JSON.parse(await readFile3(path11, "utf8"));
|
|
1931
4398
|
return parseConfig(raw, options);
|
|
1932
4399
|
}
|
|
1933
4400
|
function parseConfig(raw, options = {}) {
|
|
@@ -1944,6 +4411,12 @@ function parseConfig(raw, options = {}) {
|
|
|
1944
4411
|
if ("sessionInitTimeoutMs" in transport && (typeof transport.sessionInitTimeoutMs !== "number" || !Number.isFinite(transport.sessionInitTimeoutMs) || transport.sessionInitTimeoutMs <= 0)) {
|
|
1945
4412
|
throw new Error("transport.sessionInitTimeoutMs must be a positive number");
|
|
1946
4413
|
}
|
|
4414
|
+
if ("permissionMode" in transport && transport.permissionMode !== "approve-all" && transport.permissionMode !== "approve-reads" && transport.permissionMode !== "deny-all") {
|
|
4415
|
+
throw new Error("transport.permissionMode must be approve-all, approve-reads, or deny-all");
|
|
4416
|
+
}
|
|
4417
|
+
if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "allow" && transport.nonInteractivePermissions !== "deny" && transport.nonInteractivePermissions !== "fail") {
|
|
4418
|
+
throw new Error("transport.nonInteractivePermissions must be allow, deny, or fail");
|
|
4419
|
+
}
|
|
1947
4420
|
if (!isRecord(raw.agents)) {
|
|
1948
4421
|
throw new Error("agents must be an object");
|
|
1949
4422
|
}
|
|
@@ -1978,8 +4451,9 @@ function parseConfig(raw, options = {}) {
|
|
|
1978
4451
|
throw new Error(`workspace "${name}" allowed_agents must be an array of strings`);
|
|
1979
4452
|
}
|
|
1980
4453
|
}
|
|
4454
|
+
const rawAgents = raw.agents;
|
|
1981
4455
|
const agents = {};
|
|
1982
|
-
for (const [name, agent] of Object.entries(
|
|
4456
|
+
for (const [name, agent] of Object.entries(rawAgents)) {
|
|
1983
4457
|
const driver = agent.driver;
|
|
1984
4458
|
const command = typeof agent.command === "string" ? resolveAgentCommand(driver, agent.command) : undefined;
|
|
1985
4459
|
agents[name] = {
|
|
@@ -1987,21 +4461,29 @@ function parseConfig(raw, options = {}) {
|
|
|
1987
4461
|
...command ? { command } : {}
|
|
1988
4462
|
};
|
|
1989
4463
|
}
|
|
4464
|
+
const rawWorkspaces = raw.workspaces;
|
|
1990
4465
|
const workspaces = {};
|
|
1991
|
-
for (const [name, workspace] of Object.entries(
|
|
4466
|
+
for (const [name, workspace] of Object.entries(rawWorkspaces)) {
|
|
1992
4467
|
workspaces[name] = {
|
|
1993
4468
|
cwd: workspace.cwd,
|
|
1994
4469
|
...typeof workspace.description === "string" ? { description: workspace.description } : {}
|
|
1995
4470
|
};
|
|
1996
4471
|
}
|
|
4472
|
+
const transportType = transport.type === "acpx-cli" || transport.type === "acpx-bridge" ? transport.type : "acpx-bridge";
|
|
4473
|
+
const permissionMode = transport.permissionMode === "approve-all" || transport.permissionMode === "approve-reads" || transport.permissionMode === "deny-all" ? transport.permissionMode : DEFAULT_PERMISSION_MODE;
|
|
4474
|
+
const nonInteractivePermissions = transport.nonInteractivePermissions === "allow" || transport.nonInteractivePermissions === "deny" || transport.nonInteractivePermissions === "fail" ? transport.nonInteractivePermissions : DEFAULT_NON_INTERACTIVE_PERMISSIONS;
|
|
4475
|
+
const loggingLevel = logging?.level;
|
|
4476
|
+
const resolvedLoggingLevel = loggingLevel === "error" || loggingLevel === "info" || loggingLevel === "debug" ? loggingLevel : options.defaultLoggingLevel ?? DEFAULT_LOGGING_CONFIG.level;
|
|
1997
4477
|
return {
|
|
1998
4478
|
transport: {
|
|
1999
4479
|
...typeof transport.command === "string" ? { command: transport.command } : {},
|
|
2000
4480
|
...typeof transport.sessionInitTimeoutMs === "number" ? { sessionInitTimeoutMs: transport.sessionInitTimeoutMs } : {},
|
|
2001
|
-
type:
|
|
4481
|
+
type: transportType,
|
|
4482
|
+
permissionMode,
|
|
4483
|
+
nonInteractivePermissions
|
|
2002
4484
|
},
|
|
2003
4485
|
logging: {
|
|
2004
|
-
level:
|
|
4486
|
+
level: resolvedLoggingLevel,
|
|
2005
4487
|
maxSizeBytes: typeof logging?.maxSizeBytes === "number" ? logging.maxSizeBytes : DEFAULT_LOGGING_CONFIG.maxSizeBytes,
|
|
2006
4488
|
maxFiles: typeof logging?.maxFiles === "number" ? logging.maxFiles : DEFAULT_LOGGING_CONFIG.maxFiles,
|
|
2007
4489
|
retentionDays: typeof logging?.retentionDays === "number" ? logging.retentionDays : DEFAULT_LOGGING_CONFIG.retentionDays
|
|
@@ -2010,7 +4492,7 @@ function parseConfig(raw, options = {}) {
|
|
|
2010
4492
|
workspaces
|
|
2011
4493
|
};
|
|
2012
4494
|
}
|
|
2013
|
-
var DEFAULT_LOGGING_CONFIG;
|
|
4495
|
+
var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "fail";
|
|
2014
4496
|
var init_load_config = __esm(() => {
|
|
2015
4497
|
DEFAULT_LOGGING_CONFIG = {
|
|
2016
4498
|
level: "info",
|
|
@@ -2026,8 +4508,8 @@ import { dirname as dirname5 } from "node:path";
|
|
|
2026
4508
|
|
|
2027
4509
|
class ConfigStore {
|
|
2028
4510
|
path;
|
|
2029
|
-
constructor(
|
|
2030
|
-
this.path =
|
|
4511
|
+
constructor(path11) {
|
|
4512
|
+
this.path = path11;
|
|
2031
4513
|
}
|
|
2032
4514
|
async load() {
|
|
2033
4515
|
return await loadConfig(this.path);
|
|
@@ -2065,6 +4547,15 @@ class ConfigStore {
|
|
|
2065
4547
|
await this.save(config);
|
|
2066
4548
|
return config;
|
|
2067
4549
|
}
|
|
4550
|
+
async updateTransport(transport) {
|
|
4551
|
+
const config = await this.load();
|
|
4552
|
+
config.transport = {
|
|
4553
|
+
...config.transport,
|
|
4554
|
+
...transport
|
|
4555
|
+
};
|
|
4556
|
+
await this.save(config);
|
|
4557
|
+
return config;
|
|
4558
|
+
}
|
|
2068
4559
|
}
|
|
2069
4560
|
var init_config_store = __esm(() => {
|
|
2070
4561
|
init_load_config();
|
|
@@ -2072,14 +4563,14 @@ var init_config_store = __esm(() => {
|
|
|
2072
4563
|
|
|
2073
4564
|
// src/config/ensure-config.ts
|
|
2074
4565
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
2075
|
-
async function ensureConfigExists(
|
|
4566
|
+
async function ensureConfigExists(path11) {
|
|
2076
4567
|
try {
|
|
2077
|
-
await loadConfig(
|
|
4568
|
+
await loadConfig(path11);
|
|
2078
4569
|
} catch (error) {
|
|
2079
4570
|
if (!isMissingFileError2(error)) {
|
|
2080
4571
|
throw error;
|
|
2081
4572
|
}
|
|
2082
|
-
const store = new ConfigStore(
|
|
4573
|
+
const store = new ConfigStore(path11);
|
|
2083
4574
|
await store.save(await loadDefaultConfigTemplate());
|
|
2084
4575
|
}
|
|
2085
4576
|
}
|
|
@@ -2116,7 +4607,7 @@ function resolveAcpxCommand(options = {}) {
|
|
|
2116
4607
|
}
|
|
2117
4608
|
const platform = options.platform ?? process.platform;
|
|
2118
4609
|
const resolvePackageJson = options.resolvePackageJson ?? ((id) => require2.resolve(id));
|
|
2119
|
-
const readPackageJson = options.readPackageJson ?? ((
|
|
4610
|
+
const readPackageJson = options.readPackageJson ?? ((path11) => JSON.parse(readFileSync(path11, "utf8")));
|
|
2120
4611
|
try {
|
|
2121
4612
|
const packageJsonPath = resolvePackageJson("acpx/package.json");
|
|
2122
4613
|
const pkg = readPackageJson(packageJsonPath);
|
|
@@ -2138,9 +4629,9 @@ var init_resolve_acpx_command = __esm(() => {
|
|
|
2138
4629
|
class ConsoleAgent {
|
|
2139
4630
|
router;
|
|
2140
4631
|
logger;
|
|
2141
|
-
constructor(router,
|
|
4632
|
+
constructor(router, logger2) {
|
|
2142
4633
|
this.router = router;
|
|
2143
|
-
this.logger =
|
|
4634
|
+
this.logger = logger2 ?? createNoopAppLogger();
|
|
2144
4635
|
}
|
|
2145
4636
|
async chat(request) {
|
|
2146
4637
|
if (!request.text.trim()) {
|
|
@@ -2151,7 +4642,7 @@ class ConsoleAgent {
|
|
|
2151
4642
|
kind: request.text.trim().startsWith("/") ? "command" : "prompt",
|
|
2152
4643
|
text: summarizeText(request.text)
|
|
2153
4644
|
});
|
|
2154
|
-
return await this.router.handle(request.conversationId, request.text);
|
|
4645
|
+
return await this.router.handle(request.conversationId, request.text, request.reply);
|
|
2155
4646
|
}
|
|
2156
4647
|
}
|
|
2157
4648
|
function summarizeText(text) {
|
|
@@ -2278,8 +4769,8 @@ import { dirname as dirname6 } from "node:path";
|
|
|
2278
4769
|
|
|
2279
4770
|
class StateStore {
|
|
2280
4771
|
path;
|
|
2281
|
-
constructor(
|
|
2282
|
-
this.path =
|
|
4772
|
+
constructor(path11) {
|
|
4773
|
+
this.path = path11;
|
|
2283
4774
|
}
|
|
2284
4775
|
async load() {
|
|
2285
4776
|
try {
|
|
@@ -2319,17 +4810,31 @@ async function runConsole(paths, deps) {
|
|
|
2319
4810
|
configPath: paths.configPath,
|
|
2320
4811
|
statePath: paths.statePath
|
|
2321
4812
|
});
|
|
2322
|
-
heartbeatTimer = setIntervalFn(() =>
|
|
4813
|
+
heartbeatTimer = setIntervalFn(() => {
|
|
4814
|
+
deps.daemonRuntime?.heartbeat().catch(() => {});
|
|
4815
|
+
}, deps.heartbeatIntervalMs ?? 30000);
|
|
4816
|
+
}
|
|
4817
|
+
if (!sdk.isLoggedIn()) {
|
|
4818
|
+
console.log("[weacpx] 未检测到登录凭证,正在启动扫码登录...");
|
|
4819
|
+
await sdk.login();
|
|
2323
4820
|
}
|
|
2324
4821
|
await sdk.start(runtime.agent);
|
|
2325
4822
|
} finally {
|
|
4823
|
+
let disposeError = null;
|
|
2326
4824
|
if (heartbeatTimer !== null) {
|
|
2327
4825
|
clearIntervalFn(heartbeatTimer);
|
|
2328
4826
|
}
|
|
2329
|
-
|
|
4827
|
+
try {
|
|
4828
|
+
await runtime.dispose();
|
|
4829
|
+
} catch (error) {
|
|
4830
|
+
disposeError = error;
|
|
4831
|
+
}
|
|
2330
4832
|
if (deps.daemonRuntime) {
|
|
2331
4833
|
await deps.daemonRuntime.stop();
|
|
2332
4834
|
}
|
|
4835
|
+
if (disposeError) {
|
|
4836
|
+
throw disposeError;
|
|
4837
|
+
}
|
|
2333
4838
|
}
|
|
2334
4839
|
}
|
|
2335
4840
|
|
|
@@ -2341,7 +4846,7 @@ function encodeBridgeRequest(request) {
|
|
|
2341
4846
|
|
|
2342
4847
|
// src/transport/acpx-bridge/acpx-bridge-client.ts
|
|
2343
4848
|
import { spawn as spawn2 } from "node:child_process";
|
|
2344
|
-
import { fileURLToPath } from "node:url";
|
|
4849
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2345
4850
|
import { createInterface } from "node:readline";
|
|
2346
4851
|
|
|
2347
4852
|
class AcpxBridgeClient {
|
|
@@ -2355,7 +4860,10 @@ class AcpxBridgeClient {
|
|
|
2355
4860
|
const id = String(this.nextId);
|
|
2356
4861
|
this.nextId += 1;
|
|
2357
4862
|
return awaitable((resolve, reject) => {
|
|
2358
|
-
this.pending.set(id, {
|
|
4863
|
+
this.pending.set(id, {
|
|
4864
|
+
resolve: (value) => resolve(value),
|
|
4865
|
+
reject
|
|
4866
|
+
});
|
|
2359
4867
|
this.writeLine(encodeBridgeRequest({
|
|
2360
4868
|
id,
|
|
2361
4869
|
method,
|
|
@@ -2374,8 +4882,23 @@ class AcpxBridgeClient {
|
|
|
2374
4882
|
pending.resolve(response.result);
|
|
2375
4883
|
return;
|
|
2376
4884
|
}
|
|
4885
|
+
if (response.error.details?.exitCode !== undefined) {
|
|
4886
|
+
pending.reject(new PromptCommandError(response.error.message, {
|
|
4887
|
+
code: response.error.details.exitCode,
|
|
4888
|
+
stdout: response.error.details.stdout ?? "",
|
|
4889
|
+
stderr: response.error.details.stderr ?? ""
|
|
4890
|
+
}));
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
2377
4893
|
pending.reject(new Error(response.error.message));
|
|
2378
4894
|
}
|
|
4895
|
+
handleExit(error) {
|
|
4896
|
+
const pendingRequests = [...this.pending.values()];
|
|
4897
|
+
this.pending.clear();
|
|
4898
|
+
for (const pending of pendingRequests) {
|
|
4899
|
+
pending.reject(error);
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
2379
4902
|
}
|
|
2380
4903
|
function buildBridgeSpawnSpec(options) {
|
|
2381
4904
|
if (options.execPath.endsWith("bun")) {
|
|
@@ -2390,7 +4913,7 @@ function buildBridgeSpawnSpec(options) {
|
|
|
2390
4913
|
};
|
|
2391
4914
|
}
|
|
2392
4915
|
async function spawnAcpxBridgeClient(options = {}) {
|
|
2393
|
-
const bridgeEntryPath = options.bridgeEntryPath ??
|
|
4916
|
+
const bridgeEntryPath = options.bridgeEntryPath ?? fileURLToPath2(new URL("../../bridge/bridge-main.ts", import.meta.url));
|
|
2394
4917
|
const spawnSpec = buildBridgeSpawnSpec({
|
|
2395
4918
|
execPath: process.execPath,
|
|
2396
4919
|
bridgeEntryPath
|
|
@@ -2399,7 +4922,9 @@ async function spawnAcpxBridgeClient(options = {}) {
|
|
|
2399
4922
|
cwd: options.cwd ?? process.cwd(),
|
|
2400
4923
|
env: {
|
|
2401
4924
|
...process.env,
|
|
2402
|
-
WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx"
|
|
4925
|
+
WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
|
|
4926
|
+
WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
|
|
4927
|
+
WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "fail"
|
|
2403
4928
|
},
|
|
2404
4929
|
stdio: ["pipe", "pipe", "inherit"]
|
|
2405
4930
|
});
|
|
@@ -2415,6 +4940,10 @@ async function spawnAcpxBridgeClient(options = {}) {
|
|
|
2415
4940
|
});
|
|
2416
4941
|
child.on("exit", () => {
|
|
2417
4942
|
output.close();
|
|
4943
|
+
client.handleExit(new Error("bridge process exited before responding"));
|
|
4944
|
+
});
|
|
4945
|
+
child.on("error", (error) => {
|
|
4946
|
+
client.handleExit(error);
|
|
2418
4947
|
});
|
|
2419
4948
|
client.waitUntilReady = async () => {
|
|
2420
4949
|
await client.request("ping", {});
|
|
@@ -2435,7 +4964,9 @@ function awaitable(executor) {
|
|
|
2435
4964
|
executor(resolve, reject);
|
|
2436
4965
|
});
|
|
2437
4966
|
}
|
|
2438
|
-
var init_acpx_bridge_client = () => {
|
|
4967
|
+
var init_acpx_bridge_client = __esm(() => {
|
|
4968
|
+
init_prompt_output();
|
|
4969
|
+
});
|
|
2439
4970
|
|
|
2440
4971
|
// src/transport/acpx-bridge/acpx-bridge-transport.ts
|
|
2441
4972
|
class AcpxBridgeTransport {
|
|
@@ -2446,7 +4977,7 @@ class AcpxBridgeTransport {
|
|
|
2446
4977
|
async ensureSession(session) {
|
|
2447
4978
|
await this.client.request("ensureSession", this.toParams(session));
|
|
2448
4979
|
}
|
|
2449
|
-
async prompt(session, text) {
|
|
4980
|
+
async prompt(session, text, _reply) {
|
|
2450
4981
|
return await this.client.request("prompt", {
|
|
2451
4982
|
...this.toParams(session),
|
|
2452
4983
|
text
|
|
@@ -2490,6 +5021,64 @@ var init_spawn_command = __esm(() => {
|
|
|
2490
5021
|
SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
|
|
2491
5022
|
});
|
|
2492
5023
|
|
|
5024
|
+
// src/transport/streaming-prompt.ts
|
|
5025
|
+
function createStreamingPromptState() {
|
|
5026
|
+
return {
|
|
5027
|
+
buffer: "",
|
|
5028
|
+
segments: [],
|
|
5029
|
+
hasAgentMessage: false,
|
|
5030
|
+
pendingLine: "",
|
|
5031
|
+
finalize() {
|
|
5032
|
+
if (this.pendingLine.trim().length > 0) {
|
|
5033
|
+
parseStreamingChunks(this, this.pendingLine);
|
|
5034
|
+
}
|
|
5035
|
+
const remaining = this.buffer.trim();
|
|
5036
|
+
this.buffer = "";
|
|
5037
|
+
this.pendingLine = "";
|
|
5038
|
+
return remaining;
|
|
5039
|
+
}
|
|
5040
|
+
};
|
|
5041
|
+
}
|
|
5042
|
+
function parseStreamingDataChunk(state, chunk) {
|
|
5043
|
+
state.pendingLine += chunk;
|
|
5044
|
+
let boundary;
|
|
5045
|
+
while ((boundary = state.pendingLine.indexOf(`
|
|
5046
|
+
`)) !== -1) {
|
|
5047
|
+
const line = state.pendingLine.slice(0, boundary);
|
|
5048
|
+
state.pendingLine = state.pendingLine.slice(boundary + 1);
|
|
5049
|
+
parseStreamingChunks(state, line);
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
function parseStreamingChunks(state, line) {
|
|
5053
|
+
const trimmed = line.trim();
|
|
5054
|
+
if (trimmed.length === 0)
|
|
5055
|
+
return;
|
|
5056
|
+
let event;
|
|
5057
|
+
try {
|
|
5058
|
+
event = JSON.parse(trimmed);
|
|
5059
|
+
} catch {
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
5062
|
+
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";
|
|
5063
|
+
if (!isMessageChunk)
|
|
5064
|
+
return;
|
|
5065
|
+
state.hasAgentMessage = true;
|
|
5066
|
+
const chunk = event.params.update.content.text ?? "";
|
|
5067
|
+
if (chunk.length === 0)
|
|
5068
|
+
return;
|
|
5069
|
+
state.buffer += chunk;
|
|
5070
|
+
let boundary;
|
|
5071
|
+
while ((boundary = state.buffer.indexOf(`
|
|
5072
|
+
|
|
5073
|
+
`)) !== -1) {
|
|
5074
|
+
const segment = state.buffer.slice(0, boundary).trim();
|
|
5075
|
+
state.buffer = state.buffer.slice(boundary + 2);
|
|
5076
|
+
if (segment.length > 0) {
|
|
5077
|
+
state.segments.push(segment);
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
|
|
2493
5082
|
// src/transport/acpx-cli/node-pty-helper.ts
|
|
2494
5083
|
import { chmod as chmodFs } from "node:fs/promises";
|
|
2495
5084
|
import { dirname as dirname7, join as join3 } from "node:path";
|
|
@@ -2577,11 +5166,15 @@ async function defaultPtyRunner(command, args, options) {
|
|
|
2577
5166
|
class AcpxCliTransport {
|
|
2578
5167
|
command;
|
|
2579
5168
|
sessionInitTimeoutMs;
|
|
5169
|
+
permissionMode;
|
|
5170
|
+
nonInteractivePermissions;
|
|
2580
5171
|
runCommand;
|
|
2581
5172
|
runPtyCommand;
|
|
2582
5173
|
constructor(options, runCommand = defaultRunner, runPtyCommand = defaultPtyRunner) {
|
|
2583
5174
|
this.command = options.command ?? "acpx";
|
|
2584
5175
|
this.sessionInitTimeoutMs = options.sessionInitTimeoutMs ?? 120000;
|
|
5176
|
+
this.permissionMode = options.permissionMode ?? "approve-all";
|
|
5177
|
+
this.nonInteractivePermissions = options.nonInteractivePermissions ?? "fail";
|
|
2585
5178
|
this.runCommand = runCommand;
|
|
2586
5179
|
this.runPtyCommand = runPtyCommand;
|
|
2587
5180
|
}
|
|
@@ -2597,9 +5190,14 @@ class AcpxCliTransport {
|
|
|
2597
5190
|
timeoutMs: this.sessionInitTimeoutMs
|
|
2598
5191
|
});
|
|
2599
5192
|
}
|
|
2600
|
-
async prompt(session, text) {
|
|
2601
|
-
const
|
|
2602
|
-
|
|
5193
|
+
async prompt(session, text, reply) {
|
|
5194
|
+
const args = this.buildPromptArgs(session, text);
|
|
5195
|
+
if (reply) {
|
|
5196
|
+
const result2 = await this.runStreamingPrompt(this.command, args, reply);
|
|
5197
|
+
return { text: getPromptText(result2) };
|
|
5198
|
+
}
|
|
5199
|
+
const result = await this.runCommand(this.command, args);
|
|
5200
|
+
return { text: getPromptText(result) };
|
|
2603
5201
|
}
|
|
2604
5202
|
async cancel(session) {
|
|
2605
5203
|
const output = await this.run(this.buildArgs(session, [
|
|
@@ -2657,26 +5255,93 @@ class AcpxCliTransport {
|
|
|
2657
5255
|
})
|
|
2658
5256
|
]);
|
|
2659
5257
|
}
|
|
5258
|
+
async runStreamingPrompt(command, args, reply, maxSegmentWaitMs = 30000) {
|
|
5259
|
+
return await new Promise((resolve, reject) => {
|
|
5260
|
+
const spawnSpec = resolveSpawnCommand(command, args);
|
|
5261
|
+
const child = spawn3(spawnSpec.command, spawnSpec.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5262
|
+
let stdout = "";
|
|
5263
|
+
let stderr = "";
|
|
5264
|
+
const state = createStreamingPromptState();
|
|
5265
|
+
let lastReplyAt = Date.now();
|
|
5266
|
+
const flushBuffer = () => {
|
|
5267
|
+
const remaining = state.buffer.trim();
|
|
5268
|
+
if (remaining.length > 0) {
|
|
5269
|
+
state.buffer = "";
|
|
5270
|
+
reply(remaining).catch(() => {});
|
|
5271
|
+
lastReplyAt = Date.now();
|
|
5272
|
+
}
|
|
5273
|
+
};
|
|
5274
|
+
const timer = setInterval(() => {
|
|
5275
|
+
if (state.buffer.trim().length > 0 && Date.now() - lastReplyAt >= maxSegmentWaitMs) {
|
|
5276
|
+
flushBuffer();
|
|
5277
|
+
}
|
|
5278
|
+
}, 5000);
|
|
5279
|
+
child.stdout.setEncoding("utf8");
|
|
5280
|
+
child.stdout.on("data", (chunk) => {
|
|
5281
|
+
stdout += String(chunk);
|
|
5282
|
+
parseStreamingDataChunk(state, String(chunk));
|
|
5283
|
+
for (const segment of state.segments.splice(0)) {
|
|
5284
|
+
reply(segment).catch(() => {});
|
|
5285
|
+
lastReplyAt = Date.now();
|
|
5286
|
+
}
|
|
5287
|
+
});
|
|
5288
|
+
child.stderr.on("data", (chunk) => {
|
|
5289
|
+
stderr += String(chunk);
|
|
5290
|
+
});
|
|
5291
|
+
child.on("error", (err) => {
|
|
5292
|
+
clearInterval(timer);
|
|
5293
|
+
reject(err);
|
|
5294
|
+
});
|
|
5295
|
+
child.on("close", (code) => {
|
|
5296
|
+
clearInterval(timer);
|
|
5297
|
+
const remaining = state.finalize();
|
|
5298
|
+
if (remaining.length > 0) {
|
|
5299
|
+
reply(remaining).catch(() => {});
|
|
5300
|
+
}
|
|
5301
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
5302
|
+
});
|
|
5303
|
+
});
|
|
5304
|
+
}
|
|
2660
5305
|
buildArgs(session, tail) {
|
|
2661
|
-
const prefix = [
|
|
5306
|
+
const prefix = [
|
|
5307
|
+
"--format",
|
|
5308
|
+
"quiet",
|
|
5309
|
+
"--cwd",
|
|
5310
|
+
session.cwd,
|
|
5311
|
+
...this.buildPermissionArgs()
|
|
5312
|
+
];
|
|
2662
5313
|
if (session.agentCommand) {
|
|
2663
5314
|
return [...prefix, "--agent", session.agentCommand, ...tail];
|
|
2664
5315
|
}
|
|
2665
5316
|
return [...prefix, session.agent, ...tail];
|
|
2666
5317
|
}
|
|
2667
5318
|
buildPromptArgs(session, text) {
|
|
2668
|
-
const prefix = [
|
|
5319
|
+
const prefix = [
|
|
5320
|
+
"--format",
|
|
5321
|
+
"json",
|
|
5322
|
+
"--json-strict",
|
|
5323
|
+
"--cwd",
|
|
5324
|
+
session.cwd,
|
|
5325
|
+
...this.buildPermissionArgs()
|
|
5326
|
+
];
|
|
2669
5327
|
const tail = ["prompt", "-s", session.transportSession, text];
|
|
2670
5328
|
if (session.agentCommand) {
|
|
2671
5329
|
return [...prefix, "--agent", session.agentCommand, ...tail];
|
|
2672
5330
|
}
|
|
2673
5331
|
return [...prefix, session.agent, ...tail];
|
|
2674
5332
|
}
|
|
5333
|
+
buildPermissionArgs() {
|
|
5334
|
+
const modeFlag = this.permissionMode === "approve-reads" ? "--approve-reads" : this.permissionMode === "deny-all" ? "--deny-all" : "--approve-all";
|
|
5335
|
+
return [modeFlag, "--non-interactive-permissions", this.nonInteractivePermissions];
|
|
5336
|
+
}
|
|
2675
5337
|
}
|
|
2676
5338
|
function renderCommandForError(args) {
|
|
2677
5339
|
const rendered = [];
|
|
2678
5340
|
for (let index = 0;index < args.length; index += 1) {
|
|
2679
5341
|
const arg = args[index];
|
|
5342
|
+
if (arg === undefined) {
|
|
5343
|
+
continue;
|
|
5344
|
+
}
|
|
2680
5345
|
if (arg === "--format") {
|
|
2681
5346
|
index += 1;
|
|
2682
5347
|
continue;
|
|
@@ -2689,89 +5354,10 @@ function renderCommandForError(args) {
|
|
|
2689
5354
|
}
|
|
2690
5355
|
return rendered.join(" ");
|
|
2691
5356
|
}
|
|
2692
|
-
function extractPromptText(output) {
|
|
2693
|
-
const lines = output.split(`
|
|
2694
|
-
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2695
|
-
const messageSegments = [];
|
|
2696
|
-
let currentSegment = "";
|
|
2697
|
-
for (const line of lines) {
|
|
2698
|
-
try {
|
|
2699
|
-
const event = JSON.parse(line);
|
|
2700
|
-
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";
|
|
2701
|
-
if (isMessageChunk) {
|
|
2702
|
-
const chunk = event.params.update.content.text ?? "";
|
|
2703
|
-
if (chunk.length > 0) {
|
|
2704
|
-
currentSegment += chunk;
|
|
2705
|
-
}
|
|
2706
|
-
continue;
|
|
2707
|
-
}
|
|
2708
|
-
if (currentSegment.trim().length > 0) {
|
|
2709
|
-
messageSegments.push(currentSegment.trim());
|
|
2710
|
-
}
|
|
2711
|
-
currentSegment = "";
|
|
2712
|
-
} catch {
|
|
2713
|
-
if (currentSegment.trim().length > 0) {
|
|
2714
|
-
messageSegments.push(currentSegment.trim());
|
|
2715
|
-
currentSegment = "";
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
if (currentSegment.trim().length > 0) {
|
|
2720
|
-
messageSegments.push(currentSegment.trim());
|
|
2721
|
-
}
|
|
2722
|
-
if (messageSegments.length > 0) {
|
|
2723
|
-
return messageSegments[messageSegments.length - 1];
|
|
2724
|
-
}
|
|
2725
|
-
return output.trim();
|
|
2726
|
-
}
|
|
2727
|
-
function sanitizePromptText(text) {
|
|
2728
|
-
const trimmed = text.trim();
|
|
2729
|
-
const paragraphs = trimmed.split(/\n\s*\n/);
|
|
2730
|
-
if (paragraphs.length < 2) {
|
|
2731
|
-
return trimmed;
|
|
2732
|
-
}
|
|
2733
|
-
const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
|
|
2734
|
-
if (!looksLikeWorkflowPreamble(firstParagraph)) {
|
|
2735
|
-
return trimmed;
|
|
2736
|
-
}
|
|
2737
|
-
return paragraphs.slice(1).join(`
|
|
2738
|
-
|
|
2739
|
-
`).trim();
|
|
2740
|
-
}
|
|
2741
|
-
function looksLikeWorkflowPreamble(paragraph) {
|
|
2742
|
-
if (!paragraph.startsWith("using ")) {
|
|
2743
|
-
return false;
|
|
2744
|
-
}
|
|
2745
|
-
return paragraph.includes("using-superpowers") || paragraph.includes("repo workflow requirement") || paragraph.includes("workflow requirement") || paragraph.includes("before responding") || paragraph.includes("skill check");
|
|
2746
|
-
}
|
|
2747
|
-
function normalizeCommandError(result) {
|
|
2748
|
-
const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
|
|
2749
|
-
const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
|
|
2750
|
-
if (preferredMessage) {
|
|
2751
|
-
return preferredMessage;
|
|
2752
|
-
}
|
|
2753
|
-
if (rpcMessages.length > 0) {
|
|
2754
|
-
return rpcMessages[rpcMessages.length - 1] ?? null;
|
|
2755
|
-
}
|
|
2756
|
-
return result.stderr.trim() || result.stdout.trim() || null;
|
|
2757
|
-
}
|
|
2758
|
-
function extractJsonRpcErrorMessages(output) {
|
|
2759
|
-
return output.split(`
|
|
2760
|
-
`).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
|
|
2761
|
-
try {
|
|
2762
|
-
const payload = JSON.parse(line);
|
|
2763
|
-
if (typeof payload.error?.message === "string" && payload.error.message.length > 0) {
|
|
2764
|
-
return [payload.error.message];
|
|
2765
|
-
}
|
|
2766
|
-
} catch {
|
|
2767
|
-
return [];
|
|
2768
|
-
}
|
|
2769
|
-
return [];
|
|
2770
|
-
});
|
|
2771
|
-
}
|
|
2772
5357
|
var require3;
|
|
2773
5358
|
var init_acpx_cli_transport = __esm(() => {
|
|
2774
5359
|
init_spawn_command();
|
|
5360
|
+
init_prompt_output();
|
|
2775
5361
|
init_node_pty_helper();
|
|
2776
5362
|
require3 = createRequire3(import.meta.url);
|
|
2777
5363
|
});
|
|
@@ -2783,16 +5369,16 @@ __export(exports_main, {
|
|
|
2783
5369
|
main: () => main2,
|
|
2784
5370
|
buildApp: () => buildApp
|
|
2785
5371
|
});
|
|
2786
|
-
import { homedir } from "node:os";
|
|
5372
|
+
import { homedir as homedir2 } from "node:os";
|
|
2787
5373
|
import { dirname as dirname8, join as join4 } from "node:path";
|
|
2788
|
-
import { fileURLToPath as
|
|
5374
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2789
5375
|
async function buildApp(paths, deps = {}) {
|
|
2790
5376
|
await ensureConfigExists(paths.configPath);
|
|
2791
5377
|
const configStore = new ConfigStore(paths.configPath);
|
|
2792
5378
|
const config = await loadConfig(paths.configPath, {
|
|
2793
5379
|
defaultLoggingLevel: deps.defaultLoggingLevel
|
|
2794
5380
|
});
|
|
2795
|
-
const
|
|
5381
|
+
const logger2 = createAppLogger({
|
|
2796
5382
|
filePath: resolveAppLogPath(paths.configPath),
|
|
2797
5383
|
level: config.logging.level,
|
|
2798
5384
|
maxSizeBytes: config.logging.maxSizeBytes,
|
|
@@ -2800,24 +5386,26 @@ async function buildApp(paths, deps = {}) {
|
|
|
2800
5386
|
retentionDays: config.logging.retentionDays,
|
|
2801
5387
|
now: deps.loggerNow
|
|
2802
5388
|
});
|
|
2803
|
-
await
|
|
5389
|
+
await logger2.cleanup();
|
|
2804
5390
|
const acpxCommand = resolveAcpxCommand({ configuredCommand: config.transport.command });
|
|
2805
5391
|
const stateStore = new StateStore(paths.statePath);
|
|
2806
5392
|
const state = await stateStore.load();
|
|
2807
5393
|
const sessions = new SessionService(config, stateStore, state);
|
|
2808
5394
|
const transport = config.transport.type === "acpx-bridge" ? await (deps.createBridgeTransport?.() ?? Promise.resolve(new AcpxBridgeTransport(await spawnAcpxBridgeClient({
|
|
2809
5395
|
acpxCommand,
|
|
2810
|
-
bridgeEntryPath: resolveBridgeEntryPath()
|
|
5396
|
+
bridgeEntryPath: resolveBridgeEntryPath(),
|
|
5397
|
+
permissionMode: config.transport.permissionMode,
|
|
5398
|
+
nonInteractivePermissions: config.transport.nonInteractivePermissions
|
|
2811
5399
|
})))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config.transport, command: acpxCommand });
|
|
2812
|
-
const router = new CommandRouter(sessions, transport, config, configStore,
|
|
2813
|
-
const agent = new ConsoleAgent(router,
|
|
5400
|
+
const router = new CommandRouter(sessions, transport, config, configStore, logger2);
|
|
5401
|
+
const agent = new ConsoleAgent(router, logger2);
|
|
2814
5402
|
return {
|
|
2815
5403
|
agent,
|
|
2816
5404
|
router,
|
|
2817
5405
|
sessions,
|
|
2818
5406
|
stateStore,
|
|
2819
5407
|
configStore,
|
|
2820
|
-
logger,
|
|
5408
|
+
logger: logger2,
|
|
2821
5409
|
dispose: async () => {
|
|
2822
5410
|
if ("dispose" in transport && typeof transport.dispose === "function") {
|
|
2823
5411
|
await transport.dispose();
|
|
@@ -2844,7 +5432,7 @@ async function main2() {
|
|
|
2844
5432
|
}
|
|
2845
5433
|
}
|
|
2846
5434
|
function resolveRuntimePaths() {
|
|
2847
|
-
const home = process.env.HOME ??
|
|
5435
|
+
const home = process.env.HOME ?? homedir2();
|
|
2848
5436
|
if (!home) {
|
|
2849
5437
|
throw new Error("Unable to resolve the current user home directory");
|
|
2850
5438
|
}
|
|
@@ -2855,9 +5443,9 @@ function resolveRuntimePaths() {
|
|
|
2855
5443
|
}
|
|
2856
5444
|
function resolveBridgeEntryPath() {
|
|
2857
5445
|
if (import.meta.url.includes("/dist/")) {
|
|
2858
|
-
return
|
|
5446
|
+
return fileURLToPath3(new URL("./bridge/bridge-main.js", import.meta.url));
|
|
2859
5447
|
}
|
|
2860
|
-
return
|
|
5448
|
+
return fileURLToPath3(new URL("./bridge/bridge-main.ts", import.meta.url));
|
|
2861
5449
|
}
|
|
2862
5450
|
function resolveAppLogPath(configPath) {
|
|
2863
5451
|
const rootDir = dirname8(configPath);
|
|
@@ -2876,13 +5464,14 @@ var init_main = __esm(async () => {
|
|
|
2876
5464
|
init_state_store();
|
|
2877
5465
|
init_acpx_bridge_client();
|
|
2878
5466
|
init_acpx_cli_transport();
|
|
5467
|
+
init_weixin_sdk();
|
|
2879
5468
|
if (false) {}
|
|
2880
5469
|
});
|
|
2881
5470
|
|
|
2882
5471
|
// src/cli.ts
|
|
2883
|
-
import { homedir as
|
|
5472
|
+
import { homedir as homedir3 } from "node:os";
|
|
2884
5473
|
import { sep } from "node:path";
|
|
2885
|
-
import { fileURLToPath as
|
|
5474
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2886
5475
|
|
|
2887
5476
|
// src/daemon/create-daemon-controller.ts
|
|
2888
5477
|
import { mkdir as mkdir3, open } from "node:fs/promises";
|
|
@@ -2929,10 +5518,18 @@ class DaemonController {
|
|
|
2929
5518
|
paths;
|
|
2930
5519
|
deps;
|
|
2931
5520
|
statusStore;
|
|
5521
|
+
startupPollIntervalMs;
|
|
5522
|
+
startupTimeoutMs;
|
|
5523
|
+
onStartupPoll;
|
|
2932
5524
|
constructor(paths, deps) {
|
|
2933
5525
|
this.paths = paths;
|
|
2934
5526
|
this.deps = deps;
|
|
2935
5527
|
this.statusStore = new DaemonStatusStore(paths.statusFile);
|
|
5528
|
+
this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
|
|
5529
|
+
this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
|
|
5530
|
+
this.onStartupPoll = deps.onStartupPoll ?? (async () => {
|
|
5531
|
+
await new Promise((resolve) => setTimeout(resolve, this.startupPollIntervalMs));
|
|
5532
|
+
});
|
|
2936
5533
|
}
|
|
2937
5534
|
async getStatus() {
|
|
2938
5535
|
const pid = await this.loadPid();
|
|
@@ -2958,8 +5555,10 @@ class DaemonController {
|
|
|
2958
5555
|
if (current.state === "running") {
|
|
2959
5556
|
return { state: "already-running", pid: current.pid };
|
|
2960
5557
|
}
|
|
5558
|
+
await this.statusStore.clear();
|
|
2961
5559
|
const pid = await this.deps.spawnDetached();
|
|
2962
5560
|
await this.writePid(pid);
|
|
5561
|
+
await this.waitForStartupMetadata(pid);
|
|
2963
5562
|
return { state: "started", pid };
|
|
2964
5563
|
}
|
|
2965
5564
|
async stop() {
|
|
@@ -2994,6 +5593,21 @@ class DaemonController {
|
|
|
2994
5593
|
await rm2(this.paths.pidFile, { force: true });
|
|
2995
5594
|
await this.statusStore.clear();
|
|
2996
5595
|
}
|
|
5596
|
+
async waitForStartupMetadata(pid) {
|
|
5597
|
+
const deadline = Date.now() + this.startupTimeoutMs;
|
|
5598
|
+
while (Date.now() < deadline) {
|
|
5599
|
+
const status = await this.statusStore.load();
|
|
5600
|
+
if (status?.pid === pid) {
|
|
5601
|
+
return;
|
|
5602
|
+
}
|
|
5603
|
+
if (!this.deps.isProcessRunning(pid)) {
|
|
5604
|
+
await this.clearRuntimeFiles();
|
|
5605
|
+
throw new Error(`weacpx daemon exited before reporting ready state (pid ${pid})`);
|
|
5606
|
+
}
|
|
5607
|
+
await this.onStartupPoll();
|
|
5608
|
+
}
|
|
5609
|
+
throw new Error(`weacpx daemon did not report ready state within ${this.startupTimeoutMs}ms (pid ${pid})`);
|
|
5610
|
+
}
|
|
2997
5611
|
}
|
|
2998
5612
|
|
|
2999
5613
|
// src/daemon/create-daemon-controller.ts
|
|
@@ -3100,6 +5714,7 @@ async function spawnWindowsHiddenProcess(request) {
|
|
|
3100
5714
|
return;
|
|
3101
5715
|
}
|
|
3102
5716
|
settled = true;
|
|
5717
|
+
child.stdout?.destroy();
|
|
3103
5718
|
child.unref();
|
|
3104
5719
|
resolve(pid);
|
|
3105
5720
|
});
|
|
@@ -3228,7 +5843,7 @@ class DaemonRuntime {
|
|
|
3228
5843
|
}
|
|
3229
5844
|
|
|
3230
5845
|
// src/cli.ts
|
|
3231
|
-
var HELP_LINES = ["用法:", "weacpx login", "weacpx run", "weacpx start", "weacpx status", "weacpx stop"];
|
|
5846
|
+
var HELP_LINES = ["用法:", "weacpx login", "weacpx logout", "weacpx run", "weacpx start", "weacpx status", "weacpx stop"];
|
|
3232
5847
|
async function runCli(args, deps = {}) {
|
|
3233
5848
|
const command = args[0];
|
|
3234
5849
|
const print = deps.print ?? ((line) => console.log(line));
|
|
@@ -3237,6 +5852,9 @@ async function runCli(args, deps = {}) {
|
|
|
3237
5852
|
case "login":
|
|
3238
5853
|
await (deps.login ?? defaultLogin)();
|
|
3239
5854
|
return 0;
|
|
5855
|
+
case "logout":
|
|
5856
|
+
await (deps.logout ?? defaultLogout)();
|
|
5857
|
+
return 0;
|
|
3240
5858
|
case "run":
|
|
3241
5859
|
await (deps.run ?? defaultRun)();
|
|
3242
5860
|
return 0;
|
|
@@ -3288,10 +5906,14 @@ async function defaultLogin() {
|
|
|
3288
5906
|
const { main: main3 } = await init_login().then(() => exports_login);
|
|
3289
5907
|
await main3();
|
|
3290
5908
|
}
|
|
5909
|
+
async function defaultLogout() {
|
|
5910
|
+
const { logout: logout2 } = await Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk));
|
|
5911
|
+
logout2();
|
|
5912
|
+
}
|
|
3291
5913
|
async function defaultRun() {
|
|
3292
5914
|
const [{ buildApp: buildApp2, resolveRuntimePaths: resolveRuntimePaths2 }, { loadWeixinSdk: loadWeixinSdk2 }, { runConsole: runConsole2 }] = await Promise.all([
|
|
3293
5915
|
init_main().then(() => exports_main),
|
|
3294
|
-
Promise.resolve().then(() => exports_weixin_sdk),
|
|
5916
|
+
Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk)),
|
|
3295
5917
|
Promise.resolve().then(() => exports_run_console)
|
|
3296
5918
|
]);
|
|
3297
5919
|
const runtimePaths = resolveRuntimePaths2();
|
|
@@ -3315,7 +5937,7 @@ function createDefaultController() {
|
|
|
3315
5937
|
});
|
|
3316
5938
|
}
|
|
3317
5939
|
function requireHome() {
|
|
3318
|
-
const home = process.env.HOME ??
|
|
5940
|
+
const home = process.env.HOME ?? homedir3();
|
|
3319
5941
|
if (!home) {
|
|
3320
5942
|
throw new Error("Unable to resolve the current user home directory");
|
|
3321
5943
|
}
|
|
@@ -3325,7 +5947,7 @@ function resolveCliEntryPath() {
|
|
|
3325
5947
|
if (process.argv[1]) {
|
|
3326
5948
|
return process.argv[1];
|
|
3327
5949
|
}
|
|
3328
|
-
return
|
|
5950
|
+
return fileURLToPath4(import.meta.url);
|
|
3329
5951
|
}
|
|
3330
5952
|
if (__require.main == __require.module) {
|
|
3331
5953
|
process.exitCode = await runCli(process.argv.slice(2));
|