weacpx 0.1.3 → 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/dist/cli.js CHANGED
@@ -1046,120 +1046,2267 @@ var require_main = __commonJS((exports, module) => {
1046
1046
  };
1047
1047
  });
1048
1048
 
1049
- // src/weixin-sdk.ts
1050
- var exports_weixin_sdk = {};
1051
- __export(exports_weixin_sdk, {
1052
- loadWeixinSdk: () => loadWeixinSdk,
1053
- buildWeixinSdkSourceCandidates: () => buildWeixinSdkSourceCandidates,
1054
- buildWeixinSdkImportCandidates: () => buildWeixinSdkImportCandidates
1049
+ // src/weixin/storage/state-dir.ts
1050
+ import os from "node:os";
1051
+ import path from "node:path";
1052
+ function resolveStateDir() {
1053
+ return process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
1054
+ }
1055
+ var init_state_dir = () => {};
1056
+
1057
+ // src/weixin/auth/accounts.ts
1058
+ import fs from "node:fs";
1059
+ import path2 from "node:path";
1060
+ function normalizeAccountId(raw) {
1061
+ return raw.trim().toLowerCase().replace(/[@.]/g, "-");
1062
+ }
1063
+ function deriveRawAccountId(normalizedId) {
1064
+ if (normalizedId.endsWith("-im-bot")) {
1065
+ return `${normalizedId.slice(0, -7)}@im.bot`;
1066
+ }
1067
+ if (normalizedId.endsWith("-im-wechat")) {
1068
+ return `${normalizedId.slice(0, -10)}@im.wechat`;
1069
+ }
1070
+ return;
1071
+ }
1072
+ function resolveWeixinStateDir() {
1073
+ return path2.join(resolveStateDir(), "openclaw-weixin");
1074
+ }
1075
+ function resolveAccountIndexPath() {
1076
+ return path2.join(resolveWeixinStateDir(), "accounts.json");
1077
+ }
1078
+ function listIndexedWeixinAccountIds() {
1079
+ const filePath = resolveAccountIndexPath();
1080
+ try {
1081
+ if (!fs.existsSync(filePath))
1082
+ return [];
1083
+ const raw = fs.readFileSync(filePath, "utf-8");
1084
+ const parsed = JSON.parse(raw);
1085
+ if (!Array.isArray(parsed))
1086
+ return [];
1087
+ return parsed.filter((id) => typeof id === "string" && id.trim() !== "");
1088
+ } catch {
1089
+ return [];
1090
+ }
1091
+ }
1092
+ function registerWeixinAccountId(accountId) {
1093
+ const dir = resolveWeixinStateDir();
1094
+ fs.mkdirSync(dir, { recursive: true });
1095
+ fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify([accountId], null, 2), "utf-8");
1096
+ }
1097
+ function resolveAccountsDir() {
1098
+ return path2.join(resolveWeixinStateDir(), "accounts");
1099
+ }
1100
+ function resolveAccountPath(accountId) {
1101
+ return path2.join(resolveAccountsDir(), `${accountId}.json`);
1102
+ }
1103
+ function loadLegacyToken() {
1104
+ const legacyPath = path2.join(resolveStateDir(), "credentials", "openclaw-weixin", "credentials.json");
1105
+ try {
1106
+ if (!fs.existsSync(legacyPath))
1107
+ return;
1108
+ const raw = fs.readFileSync(legacyPath, "utf-8");
1109
+ const parsed = JSON.parse(raw);
1110
+ return typeof parsed.token === "string" ? parsed.token : undefined;
1111
+ } catch {
1112
+ return;
1113
+ }
1114
+ }
1115
+ function readAccountFile(filePath) {
1116
+ try {
1117
+ if (fs.existsSync(filePath)) {
1118
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1119
+ }
1120
+ } catch {}
1121
+ return null;
1122
+ }
1123
+ function loadWeixinAccount(accountId) {
1124
+ const primary = readAccountFile(resolveAccountPath(accountId));
1125
+ if (primary)
1126
+ return primary;
1127
+ const rawId = deriveRawAccountId(accountId);
1128
+ if (rawId) {
1129
+ const compat = readAccountFile(resolveAccountPath(rawId));
1130
+ if (compat)
1131
+ return compat;
1132
+ }
1133
+ const token = loadLegacyToken();
1134
+ if (token)
1135
+ return { token };
1136
+ return null;
1137
+ }
1138
+ function saveWeixinAccount(accountId, update) {
1139
+ const dir = resolveAccountsDir();
1140
+ fs.mkdirSync(dir, { recursive: true });
1141
+ const existing = loadWeixinAccount(accountId) ?? {};
1142
+ const token = update.token?.trim() || existing.token;
1143
+ const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
1144
+ const userId = update.userId !== undefined ? update.userId.trim() || undefined : existing.userId?.trim() || undefined;
1145
+ const data = {
1146
+ ...token ? { token, savedAt: new Date().toISOString() } : {},
1147
+ ...baseUrl ? { baseUrl } : {},
1148
+ ...userId ? { userId } : {}
1149
+ };
1150
+ const filePath = resolveAccountPath(accountId);
1151
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
1152
+ try {
1153
+ fs.chmodSync(filePath, 384);
1154
+ } catch {}
1155
+ }
1156
+ function clearWeixinAccount(accountId) {
1157
+ try {
1158
+ fs.unlinkSync(resolveAccountPath(accountId));
1159
+ } catch {}
1160
+ }
1161
+ function clearAllWeixinAccounts() {
1162
+ const ids = listIndexedWeixinAccountIds();
1163
+ for (const id of ids) {
1164
+ clearWeixinAccount(id);
1165
+ }
1166
+ try {
1167
+ fs.writeFileSync(resolveAccountIndexPath(), "[]", "utf-8");
1168
+ } catch {}
1169
+ }
1170
+ function resolveConfigPath() {
1171
+ const envPath = process.env.OPENCLAW_CONFIG?.trim();
1172
+ if (envPath)
1173
+ return envPath;
1174
+ return path2.join(resolveStateDir(), "openclaw.json");
1175
+ }
1176
+ function loadConfigRouteTag(accountId) {
1177
+ try {
1178
+ const configPath = resolveConfigPath();
1179
+ if (!fs.existsSync(configPath))
1180
+ return;
1181
+ const raw = fs.readFileSync(configPath, "utf-8");
1182
+ const cfg = JSON.parse(raw);
1183
+ const channels = cfg.channels;
1184
+ const section = channels?.["openclaw-weixin"];
1185
+ if (!section)
1186
+ return;
1187
+ if (accountId) {
1188
+ const accounts = section.accounts;
1189
+ const tag = accounts?.[accountId]?.routeTag;
1190
+ if (typeof tag === "number")
1191
+ return String(tag);
1192
+ if (typeof tag === "string" && tag.trim())
1193
+ return tag.trim();
1194
+ }
1195
+ if (typeof section.routeTag === "number")
1196
+ return String(section.routeTag);
1197
+ return typeof section.routeTag === "string" && section.routeTag.trim() ? section.routeTag.trim() : undefined;
1198
+ } catch {
1199
+ return;
1200
+ }
1201
+ }
1202
+ function listWeixinAccountIds() {
1203
+ return listIndexedWeixinAccountIds();
1204
+ }
1205
+ function resolveWeixinAccount(accountId) {
1206
+ const raw = accountId?.trim();
1207
+ if (!raw) {
1208
+ throw new Error("weixin: accountId is required (no default account)");
1209
+ }
1210
+ const id = normalizeAccountId(raw);
1211
+ const accountData = loadWeixinAccount(id);
1212
+ const token = accountData?.token?.trim() || undefined;
1213
+ const stateBaseUrl = accountData?.baseUrl?.trim() || "";
1214
+ return {
1215
+ accountId: id,
1216
+ baseUrl: stateBaseUrl || DEFAULT_BASE_URL,
1217
+ cdnBaseUrl: CDN_BASE_URL,
1218
+ token,
1219
+ enabled: true,
1220
+ configured: Boolean(token)
1221
+ };
1222
+ }
1223
+ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com", CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
1224
+ var init_accounts = __esm(() => {
1225
+ init_state_dir();
1226
+ });
1227
+
1228
+ // src/weixin/util/logger.ts
1229
+ import fs2 from "node:fs";
1230
+ import os2 from "node:os";
1231
+ import path3 from "node:path";
1232
+ function resolveMinLevel() {
1233
+ const env = process.env.OPENCLAW_LOG_LEVEL?.toUpperCase();
1234
+ if (env && env in LEVEL_IDS)
1235
+ return LEVEL_IDS[env] ?? LEVEL_IDS[DEFAULT_LOG_LEVEL] ?? 3;
1236
+ return LEVEL_IDS[DEFAULT_LOG_LEVEL] ?? 3;
1237
+ }
1238
+ function toLocalISO(now) {
1239
+ const offsetMs = -now.getTimezoneOffset() * 60000;
1240
+ const sign = offsetMs >= 0 ? "+" : "-";
1241
+ const abs = Math.abs(now.getTimezoneOffset());
1242
+ const offStr = `${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
1243
+ return new Date(now.getTime() + offsetMs).toISOString().replace("Z", offStr);
1244
+ }
1245
+ function localDateKey(now) {
1246
+ return toLocalISO(now).slice(0, 10);
1247
+ }
1248
+ function resolveMainLogPath() {
1249
+ const dateKey = localDateKey(new Date);
1250
+ return path3.join(MAIN_LOG_DIR, `openclaw-${dateKey}.log`);
1251
+ }
1252
+ function buildLoggerName(accountId) {
1253
+ return accountId ? `${SUBSYSTEM}/${accountId}` : SUBSYSTEM;
1254
+ }
1255
+ function writeLog(level, message, accountId) {
1256
+ const levelId = LEVEL_IDS[level] ?? LEVEL_IDS.INFO ?? 3;
1257
+ if (levelId < minLevelId)
1258
+ return;
1259
+ const now = new Date;
1260
+ const loggerName = buildLoggerName(accountId);
1261
+ const prefixedMessage = accountId ? `[${accountId}] ${message}` : message;
1262
+ const entry = JSON.stringify({
1263
+ "0": loggerName,
1264
+ "1": prefixedMessage,
1265
+ _meta: {
1266
+ runtime: RUNTIME,
1267
+ runtimeVersion: RUNTIME_VERSION,
1268
+ hostname: HOSTNAME,
1269
+ name: loggerName,
1270
+ parentNames: PARENT_NAMES,
1271
+ date: now.toISOString(),
1272
+ logLevelId: levelId,
1273
+ logLevelName: level
1274
+ },
1275
+ time: toLocalISO(now)
1276
+ });
1277
+ try {
1278
+ if (!logDirEnsured) {
1279
+ fs2.mkdirSync(MAIN_LOG_DIR, { recursive: true });
1280
+ logDirEnsured = true;
1281
+ }
1282
+ fs2.appendFileSync(resolveMainLogPath(), `${entry}
1283
+ `, "utf-8");
1284
+ } catch {}
1285
+ }
1286
+ function createLogger(accountId) {
1287
+ return {
1288
+ info(message) {
1289
+ writeLog("INFO", message, accountId);
1290
+ },
1291
+ debug(message) {
1292
+ writeLog("DEBUG", message, accountId);
1293
+ },
1294
+ warn(message) {
1295
+ writeLog("WARN", message, accountId);
1296
+ },
1297
+ error(message) {
1298
+ writeLog("ERROR", message, accountId);
1299
+ },
1300
+ withAccount(id) {
1301
+ return createLogger(id);
1302
+ },
1303
+ getLogFilePath() {
1304
+ return resolveMainLogPath();
1305
+ },
1306
+ close() {}
1307
+ };
1308
+ }
1309
+ var MAIN_LOG_DIR, SUBSYSTEM = "gateway/channels/openclaw-weixin", RUNTIME = "node", RUNTIME_VERSION, HOSTNAME, PARENT_NAMES, LEVEL_IDS, DEFAULT_LOG_LEVEL = "INFO", minLevelId, logDirEnsured = false, logger;
1310
+ var init_logger = __esm(() => {
1311
+ MAIN_LOG_DIR = path3.join("/tmp", "openclaw");
1312
+ RUNTIME_VERSION = process.versions.node;
1313
+ HOSTNAME = os2.hostname() || "unknown";
1314
+ PARENT_NAMES = ["openclaw"];
1315
+ LEVEL_IDS = {
1316
+ TRACE: 1,
1317
+ DEBUG: 2,
1318
+ INFO: 3,
1319
+ WARN: 4,
1320
+ ERROR: 5,
1321
+ FATAL: 6
1322
+ };
1323
+ minLevelId = resolveMinLevel();
1324
+ logger = createLogger();
1325
+ });
1326
+
1327
+ // src/weixin/util/redact.ts
1328
+ function truncate(s, max) {
1329
+ if (!s)
1330
+ return "";
1331
+ if (s.length <= max)
1332
+ return s;
1333
+ return `${s.slice(0, max)}…(len=${s.length})`;
1334
+ }
1335
+ function redactToken(token, prefixLen = DEFAULT_TOKEN_PREFIX_LEN) {
1336
+ if (!token)
1337
+ return "(none)";
1338
+ if (token.length <= prefixLen)
1339
+ return `****(len=${token.length})`;
1340
+ return `${token.slice(0, prefixLen)}…(len=${token.length})`;
1341
+ }
1342
+ function redactBody(body, maxLen = DEFAULT_BODY_MAX_LEN) {
1343
+ if (!body)
1344
+ return "(empty)";
1345
+ if (body.length <= maxLen)
1346
+ return body;
1347
+ return `${body.slice(0, maxLen)}…(truncated, totalLen=${body.length})`;
1348
+ }
1349
+ function redactUrl(rawUrl) {
1350
+ try {
1351
+ const u = new URL(rawUrl);
1352
+ const base = `${u.origin}${u.pathname}`;
1353
+ return u.search ? `${base}?<redacted>` : base;
1354
+ } catch {
1355
+ return truncate(rawUrl, 80);
1356
+ }
1357
+ }
1358
+ var DEFAULT_BODY_MAX_LEN = 200, DEFAULT_TOKEN_PREFIX_LEN = 6;
1359
+
1360
+ // src/weixin/api/api.ts
1361
+ import crypto from "node:crypto";
1362
+ import fs3 from "node:fs";
1363
+ import path4 from "node:path";
1364
+ import { fileURLToPath } from "node:url";
1365
+ function readChannelVersion() {
1366
+ try {
1367
+ const dir = path4.dirname(fileURLToPath(import.meta.url));
1368
+ const pkgPath = path4.resolve(dir, "..", "..", "package.json");
1369
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
1370
+ return pkg.version ?? "unknown";
1371
+ } catch {
1372
+ return "unknown";
1373
+ }
1374
+ }
1375
+ function buildBaseInfo() {
1376
+ return { channel_version: CHANNEL_VERSION };
1377
+ }
1378
+ function ensureTrailingSlash(url) {
1379
+ return url.endsWith("/") ? url : `${url}/`;
1380
+ }
1381
+ function randomWechatUin() {
1382
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
1383
+ return Buffer.from(String(uint32), "utf-8").toString("base64");
1384
+ }
1385
+ function buildCommonHeaders() {
1386
+ const headers = {};
1387
+ const routeTag = loadConfigRouteTag();
1388
+ if (routeTag) {
1389
+ headers.SKRouteTag = routeTag;
1390
+ }
1391
+ return headers;
1392
+ }
1393
+ function buildHeaders(opts) {
1394
+ const headers = {
1395
+ "Content-Type": "application/json",
1396
+ AuthorizationType: "ilink_bot_token",
1397
+ "Content-Length": String(Buffer.byteLength(opts.body, "utf-8")),
1398
+ "X-WECHAT-UIN": randomWechatUin(),
1399
+ ...buildCommonHeaders()
1400
+ };
1401
+ if (opts.token?.trim()) {
1402
+ headers.Authorization = `Bearer ${opts.token.trim()}`;
1403
+ }
1404
+ logger.debug(`requestHeaders: ${JSON.stringify({ ...headers, Authorization: headers.Authorization ? "Bearer ***" : undefined })}`);
1405
+ return headers;
1406
+ }
1407
+ async function apiGetFetch(params) {
1408
+ const base = ensureTrailingSlash(params.baseUrl);
1409
+ const url = new URL(params.endpoint, base);
1410
+ const hdrs = buildCommonHeaders();
1411
+ logger.debug(`GET ${redactUrl(url.toString())}`);
1412
+ const controller = new AbortController;
1413
+ const t = setTimeout(() => controller.abort(), params.timeoutMs);
1414
+ try {
1415
+ const res = await fetch(url.toString(), {
1416
+ method: "GET",
1417
+ headers: hdrs,
1418
+ signal: controller.signal
1419
+ });
1420
+ clearTimeout(t);
1421
+ const rawText = await res.text();
1422
+ logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
1423
+ if (!res.ok) {
1424
+ throw new Error(`${params.label} ${res.status}: ${rawText}`);
1425
+ }
1426
+ return rawText;
1427
+ } catch (err) {
1428
+ clearTimeout(t);
1429
+ throw err;
1430
+ }
1431
+ }
1432
+ async function apiFetch(params) {
1433
+ const base = ensureTrailingSlash(params.baseUrl);
1434
+ const url = new URL(params.endpoint, base);
1435
+ const hdrs = buildHeaders({ token: params.token, body: params.body });
1436
+ logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
1437
+ const controller = new AbortController;
1438
+ const t = setTimeout(() => controller.abort(), params.timeoutMs);
1439
+ const onAbort = () => controller.abort();
1440
+ params.abortSignal?.addEventListener("abort", onAbort, { once: true });
1441
+ try {
1442
+ const res = await fetch(url.toString(), {
1443
+ method: "POST",
1444
+ headers: hdrs,
1445
+ body: params.body,
1446
+ signal: controller.signal
1447
+ });
1448
+ clearTimeout(t);
1449
+ const rawText = await res.text();
1450
+ logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
1451
+ if (!res.ok) {
1452
+ throw new Error(`${params.label} ${res.status}: ${rawText}`);
1453
+ }
1454
+ return rawText;
1455
+ } catch (err) {
1456
+ clearTimeout(t);
1457
+ throw err;
1458
+ } finally {
1459
+ params.abortSignal?.removeEventListener("abort", onAbort);
1460
+ }
1461
+ }
1462
+ async function getUpdates(params) {
1463
+ const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
1464
+ try {
1465
+ const rawText = await apiFetch({
1466
+ baseUrl: params.baseUrl,
1467
+ endpoint: "ilink/bot/getupdates",
1468
+ body: JSON.stringify({
1469
+ get_updates_buf: params.get_updates_buf ?? "",
1470
+ base_info: buildBaseInfo()
1471
+ }),
1472
+ token: params.token,
1473
+ timeoutMs: timeout,
1474
+ label: "getUpdates",
1475
+ abortSignal: params.abortSignal
1476
+ });
1477
+ const resp = JSON.parse(rawText);
1478
+ return resp;
1479
+ } catch (err) {
1480
+ if (err instanceof Error && err.name === "AbortError") {
1481
+ logger.debug(`getUpdates: client-side timeout after ${timeout}ms, returning empty response`);
1482
+ return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
1483
+ }
1484
+ throw err;
1485
+ }
1486
+ }
1487
+ async function getUploadUrl(params) {
1488
+ const rawText = await apiFetch({
1489
+ baseUrl: params.baseUrl,
1490
+ endpoint: "ilink/bot/getuploadurl",
1491
+ body: JSON.stringify({
1492
+ filekey: params.filekey,
1493
+ media_type: params.media_type,
1494
+ to_user_id: params.to_user_id,
1495
+ rawsize: params.rawsize,
1496
+ rawfilemd5: params.rawfilemd5,
1497
+ filesize: params.filesize,
1498
+ thumb_rawsize: params.thumb_rawsize,
1499
+ thumb_rawfilemd5: params.thumb_rawfilemd5,
1500
+ thumb_filesize: params.thumb_filesize,
1501
+ no_need_thumb: params.no_need_thumb,
1502
+ aeskey: params.aeskey,
1503
+ base_info: buildBaseInfo()
1504
+ }),
1505
+ token: params.token,
1506
+ timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
1507
+ label: "getUploadUrl"
1508
+ });
1509
+ const resp = JSON.parse(rawText);
1510
+ return resp;
1511
+ }
1512
+ async function sendMessage(params) {
1513
+ await apiFetch({
1514
+ baseUrl: params.baseUrl,
1515
+ endpoint: "ilink/bot/sendmessage",
1516
+ body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
1517
+ token: params.token,
1518
+ timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
1519
+ label: "sendMessage"
1520
+ });
1521
+ }
1522
+ async function getConfig(params) {
1523
+ const rawText = await apiFetch({
1524
+ baseUrl: params.baseUrl,
1525
+ endpoint: "ilink/bot/getconfig",
1526
+ body: JSON.stringify({
1527
+ ilink_user_id: params.ilinkUserId,
1528
+ context_token: params.contextToken,
1529
+ base_info: buildBaseInfo()
1530
+ }),
1531
+ token: params.token,
1532
+ timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
1533
+ label: "getConfig"
1534
+ });
1535
+ const resp = JSON.parse(rawText);
1536
+ return resp;
1537
+ }
1538
+ async function sendTyping(params) {
1539
+ await apiFetch({
1540
+ baseUrl: params.baseUrl,
1541
+ endpoint: "ilink/bot/sendtyping",
1542
+ body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
1543
+ token: params.token,
1544
+ timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
1545
+ label: "sendTyping"
1546
+ });
1547
+ }
1548
+ var CHANNEL_VERSION, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
1549
+ var init_api = __esm(() => {
1550
+ init_accounts();
1551
+ init_logger();
1552
+ CHANNEL_VERSION = readChannelVersion();
1553
+ });
1554
+
1555
+ // src/weixin/auth/login-qr.ts
1556
+ import { randomUUID } from "node:crypto";
1557
+ function isLoginFresh(login) {
1558
+ return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
1559
+ }
1560
+ function purgeExpiredLogins() {
1561
+ for (const [id, login] of activeLogins) {
1562
+ if (!isLoginFresh(login)) {
1563
+ activeLogins.delete(id);
1564
+ }
1565
+ }
1566
+ }
1567
+ async function fetchQRCode(apiBaseUrl, botType) {
1568
+ logger.info(`Fetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
1569
+ const rawText = await apiGetFetch({
1570
+ baseUrl: apiBaseUrl,
1571
+ endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
1572
+ timeoutMs: GET_QRCODE_TIMEOUT_MS,
1573
+ label: "fetchQRCode"
1574
+ });
1575
+ return JSON.parse(rawText);
1576
+ }
1577
+ async function pollQRStatus(apiBaseUrl, qrcode) {
1578
+ logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=***`);
1579
+ try {
1580
+ const rawText = await apiGetFetch({
1581
+ baseUrl: apiBaseUrl,
1582
+ endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
1583
+ timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
1584
+ label: "pollQRStatus"
1585
+ });
1586
+ logger.debug(`pollQRStatus: body=${rawText.substring(0, 200)}`);
1587
+ return JSON.parse(rawText);
1588
+ } catch (err) {
1589
+ if (err instanceof Error && err.name === "AbortError") {
1590
+ logger.debug(`pollQRStatus: client-side timeout after ${QR_LONG_POLL_TIMEOUT_MS}ms, returning wait`);
1591
+ return { status: "wait" };
1592
+ }
1593
+ logger.warn(`pollQRStatus: network/gateway error, will retry: ${String(err)}`);
1594
+ return { status: "wait" };
1595
+ }
1596
+ }
1597
+ async function startWeixinLoginWithQr(opts) {
1598
+ const sessionKey = opts.accountId || randomUUID();
1599
+ purgeExpiredLogins();
1600
+ const existing = activeLogins.get(sessionKey);
1601
+ if (!opts.force && existing && isLoginFresh(existing) && existing.qrcodeUrl) {
1602
+ return {
1603
+ qrcodeUrl: existing.qrcodeUrl,
1604
+ message: "二维码已就绪,请使用微信扫描。",
1605
+ sessionKey
1606
+ };
1607
+ }
1608
+ try {
1609
+ const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
1610
+ logger.info(`Starting Weixin login with bot_type=${botType}`);
1611
+ const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
1612
+ logger.info(`QR code received, qrcode=${redactToken(qrResponse.qrcode)} imgContentLen=${qrResponse.qrcode_img_content?.length ?? 0}`);
1613
+ logger.info(`二维码链接: ${qrResponse.qrcode_img_content}`);
1614
+ const login = {
1615
+ sessionKey,
1616
+ id: randomUUID(),
1617
+ qrcode: qrResponse.qrcode,
1618
+ qrcodeUrl: qrResponse.qrcode_img_content,
1619
+ startedAt: Date.now()
1620
+ };
1621
+ activeLogins.set(sessionKey, login);
1622
+ return {
1623
+ qrcodeUrl: qrResponse.qrcode_img_content,
1624
+ message: "使用微信扫描以下二维码,以完成连接。",
1625
+ sessionKey
1626
+ };
1627
+ } catch (err) {
1628
+ logger.error(`Failed to start Weixin login: ${String(err)}`);
1629
+ return {
1630
+ message: `Failed to start login: ${String(err)}`,
1631
+ sessionKey
1632
+ };
1633
+ }
1634
+ }
1635
+ async function waitForWeixinLogin(opts) {
1636
+ let activeLogin = activeLogins.get(opts.sessionKey);
1637
+ if (!activeLogin) {
1638
+ logger.warn(`waitForWeixinLogin: no active login sessionKey=${opts.sessionKey}`);
1639
+ return {
1640
+ connected: false,
1641
+ message: "当前没有进行中的登录,请先发起登录。"
1642
+ };
1643
+ }
1644
+ if (!isLoginFresh(activeLogin)) {
1645
+ logger.warn(`waitForWeixinLogin: login QR expired sessionKey=${opts.sessionKey}`);
1646
+ activeLogins.delete(opts.sessionKey);
1647
+ return {
1648
+ connected: false,
1649
+ message: "二维码已过期,请重新生成。"
1650
+ };
1651
+ }
1652
+ const timeoutMs = Math.max(opts.timeoutMs ?? 480000, 1000);
1653
+ const deadline = Date.now() + timeoutMs;
1654
+ let scannedPrinted = false;
1655
+ let qrRefreshCount = 1;
1656
+ activeLogin.currentApiBaseUrl = FIXED_BASE_URL;
1657
+ logger.info("Starting to poll QR code status...");
1658
+ while (Date.now() < deadline) {
1659
+ try {
1660
+ const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
1661
+ const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode);
1662
+ logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
1663
+ activeLogin.status = statusResponse.status;
1664
+ switch (statusResponse.status) {
1665
+ case "wait":
1666
+ if (opts.verbose) {
1667
+ process.stdout.write(".");
1668
+ }
1669
+ break;
1670
+ case "scaned":
1671
+ if (!scannedPrinted) {
1672
+ process.stdout.write(`
1673
+ \uD83D\uDC40 已扫码,在微信继续操作...
1674
+ `);
1675
+ scannedPrinted = true;
1676
+ }
1677
+ break;
1678
+ case "expired": {
1679
+ qrRefreshCount++;
1680
+ if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
1681
+ logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
1682
+ activeLogins.delete(opts.sessionKey);
1683
+ return {
1684
+ connected: false,
1685
+ message: "登录超时:二维码多次过期,请重新开始登录流程。"
1686
+ };
1687
+ }
1688
+ process.stdout.write(`
1689
+ ⏳ 二维码已过期,正在刷新...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})
1690
+ `);
1691
+ logger.info(`waitForWeixinLogin: QR expired, refreshing (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})`);
1692
+ try {
1693
+ const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
1694
+ const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
1695
+ activeLogin.qrcode = qrResponse.qrcode;
1696
+ activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
1697
+ activeLogin.startedAt = Date.now();
1698
+ scannedPrinted = false;
1699
+ logger.info(`waitForWeixinLogin: new QR code obtained qrcode=${redactToken(qrResponse.qrcode)}`);
1700
+ process.stdout.write(`\uD83D\uDD04 新二维码已生成,请重新扫描
1701
+
1702
+ `);
1703
+ try {
1704
+ const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
1705
+ qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
1706
+ process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
1707
+ `);
1708
+ process.stdout.write(`${qrResponse.qrcode_img_content}
1709
+ `);
1710
+ } catch {
1711
+ process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
1712
+ `);
1713
+ process.stdout.write(`${qrResponse.qrcode_img_content}
1714
+ `);
1715
+ }
1716
+ } catch (refreshErr) {
1717
+ logger.error(`waitForWeixinLogin: failed to refresh QR code: ${String(refreshErr)}`);
1718
+ activeLogins.delete(opts.sessionKey);
1719
+ return {
1720
+ connected: false,
1721
+ message: `刷新二维码失败: ${String(refreshErr)}`
1722
+ };
1723
+ }
1724
+ break;
1725
+ }
1726
+ case "scaned_but_redirect": {
1727
+ const redirectHost = statusResponse.redirect_host;
1728
+ if (redirectHost) {
1729
+ const newBaseUrl = `https://${redirectHost}`;
1730
+ activeLogin.currentApiBaseUrl = newBaseUrl;
1731
+ logger.info(`waitForWeixinLogin: IDC redirect, switching polling host to ${redirectHost}`);
1732
+ } else {
1733
+ logger.warn(`waitForWeixinLogin: received scaned_but_redirect but redirect_host is missing, continuing with current host`);
1734
+ }
1735
+ break;
1736
+ }
1737
+ case "confirmed": {
1738
+ if (!statusResponse.ilink_bot_id) {
1739
+ activeLogins.delete(opts.sessionKey);
1740
+ logger.error("Login confirmed but ilink_bot_id missing from response");
1741
+ return {
1742
+ connected: false,
1743
+ message: "登录失败:服务器未返回 ilink_bot_id。"
1744
+ };
1745
+ }
1746
+ activeLogin.botToken = statusResponse.bot_token;
1747
+ activeLogins.delete(opts.sessionKey);
1748
+ logger.info(`✅ Login confirmed! ilink_bot_id=${statusResponse.ilink_bot_id} ilink_user_id=${redactToken(statusResponse.ilink_user_id)}`);
1749
+ return {
1750
+ connected: true,
1751
+ botToken: statusResponse.bot_token,
1752
+ accountId: statusResponse.ilink_bot_id,
1753
+ baseUrl: statusResponse.baseurl,
1754
+ userId: statusResponse.ilink_user_id,
1755
+ message: "✅ 与微信连接成功!"
1756
+ };
1757
+ }
1758
+ }
1759
+ } catch (err) {
1760
+ logger.error(`Error polling QR status: ${String(err)}`);
1761
+ activeLogins.delete(opts.sessionKey);
1762
+ return {
1763
+ connected: false,
1764
+ message: `Login failed: ${String(err)}`
1765
+ };
1766
+ }
1767
+ await new Promise((r) => setTimeout(r, 1000));
1768
+ }
1769
+ logger.warn(`waitForWeixinLogin: timed out waiting for QR scan sessionKey=${opts.sessionKey} timeoutMs=${timeoutMs}`);
1770
+ activeLogins.delete(opts.sessionKey);
1771
+ return {
1772
+ connected: false,
1773
+ message: "登录超时,请重试。"
1774
+ };
1775
+ }
1776
+ var ACTIVE_LOGIN_TTL_MS, GET_QRCODE_TIMEOUT_MS = 5000, QR_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_ILINK_BOT_TYPE = "3", FIXED_BASE_URL = "https://ilinkai.weixin.qq.com", activeLogins, MAX_QR_REFRESH_COUNT = 3;
1777
+ var init_login_qr = __esm(() => {
1778
+ init_api();
1779
+ init_logger();
1780
+ ACTIVE_LOGIN_TTL_MS = 5 * 60000;
1781
+ activeLogins = new Map;
1782
+ });
1783
+
1784
+ // src/login.ts
1785
+ var exports_login = {};
1786
+ __export(exports_login, {
1787
+ main: () => main,
1788
+ loginWithQrRendering: () => loginWithQrRendering
1789
+ });
1790
+ async function loginWithQrRendering() {
1791
+ const apiBaseUrl = DEFAULT_BASE_URL;
1792
+ console.log("正在启动微信扫码登录...");
1793
+ const startResult = await startWeixinLoginWithQr({
1794
+ apiBaseUrl,
1795
+ botType: DEFAULT_ILINK_BOT_TYPE
1796
+ });
1797
+ if (!startResult.qrcodeUrl) {
1798
+ throw new Error(startResult.message);
1799
+ }
1800
+ console.log(`
1801
+ 使用微信扫描以下二维码,以完成连接:
1802
+ `);
1803
+ await new Promise((resolve) => {
1804
+ import_qrcode_terminal.default.generate(startResult.qrcodeUrl, { small: true }, (qr) => {
1805
+ console.log(qr);
1806
+ resolve();
1807
+ });
1808
+ });
1809
+ console.log(`
1810
+ 等待扫码...
1811
+ `);
1812
+ const waitResult = await waitForWeixinLogin({
1813
+ sessionKey: startResult.sessionKey,
1814
+ apiBaseUrl,
1815
+ timeoutMs: 480000,
1816
+ botType: DEFAULT_ILINK_BOT_TYPE
1817
+ });
1818
+ if (!waitResult.connected || !waitResult.botToken || !waitResult.accountId) {
1819
+ throw new Error(waitResult.message);
1820
+ }
1821
+ const normalizedId = normalizeAccountId(waitResult.accountId);
1822
+ saveWeixinAccount(normalizedId, {
1823
+ token: waitResult.botToken,
1824
+ baseUrl: waitResult.baseUrl,
1825
+ userId: waitResult.userId
1826
+ });
1827
+ registerWeixinAccountId(normalizedId);
1828
+ console.log(`
1829
+ ✅ 与微信连接成功!`);
1830
+ }
1831
+ async function main() {
1832
+ await loginWithQrRendering();
1833
+ }
1834
+ var import_qrcode_terminal;
1835
+ var init_login = __esm(async () => {
1836
+ init_accounts();
1837
+ init_login_qr();
1838
+ import_qrcode_terminal = __toESM(require_main(), 1);
1839
+ if (false) {}
1840
+ });
1841
+
1842
+ // src/weixin/api/config-cache.ts
1843
+ class WeixinConfigManager {
1844
+ apiOpts;
1845
+ log;
1846
+ cache = new Map;
1847
+ constructor(apiOpts, log) {
1848
+ this.apiOpts = apiOpts;
1849
+ this.log = log;
1850
+ }
1851
+ async getForUser(userId, contextToken) {
1852
+ const now = Date.now();
1853
+ const entry = this.cache.get(userId);
1854
+ const shouldFetch = !entry || now >= entry.nextFetchAt;
1855
+ if (shouldFetch) {
1856
+ let fetchOk = false;
1857
+ try {
1858
+ const resp = await getConfig({
1859
+ baseUrl: this.apiOpts.baseUrl,
1860
+ token: this.apiOpts.token,
1861
+ ilinkUserId: userId,
1862
+ contextToken
1863
+ });
1864
+ if (resp.ret === 0) {
1865
+ this.cache.set(userId, {
1866
+ config: { typingTicket: resp.typing_ticket ?? "" },
1867
+ everSucceeded: true,
1868
+ nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
1869
+ retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
1870
+ });
1871
+ this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
1872
+ fetchOk = true;
1873
+ }
1874
+ } catch (err) {
1875
+ this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
1876
+ }
1877
+ if (!fetchOk) {
1878
+ const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
1879
+ const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
1880
+ if (entry) {
1881
+ entry.nextFetchAt = now + nextDelay;
1882
+ entry.retryDelayMs = nextDelay;
1883
+ } else {
1884
+ this.cache.set(userId, {
1885
+ config: { typingTicket: "" },
1886
+ everSucceeded: false,
1887
+ nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
1888
+ retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
1889
+ });
1890
+ }
1891
+ }
1892
+ }
1893
+ return this.cache.get(userId)?.config ?? { typingTicket: "" };
1894
+ }
1895
+ }
1896
+ var CONFIG_CACHE_TTL_MS, CONFIG_CACHE_INITIAL_RETRY_MS = 2000, CONFIG_CACHE_MAX_RETRY_MS;
1897
+ var init_config_cache = __esm(() => {
1898
+ init_api();
1899
+ CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
1900
+ CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
1901
+ });
1902
+
1903
+ // src/weixin/api/session-guard.ts
1904
+ function pauseSession(accountId) {
1905
+ const until = Date.now() + SESSION_PAUSE_DURATION_MS;
1906
+ pauseUntilMap.set(accountId, until);
1907
+ logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
1908
+ }
1909
+ function getRemainingPauseMs(accountId) {
1910
+ const until = pauseUntilMap.get(accountId);
1911
+ if (until === undefined)
1912
+ return 0;
1913
+ const remaining = until - Date.now();
1914
+ if (remaining <= 0) {
1915
+ pauseUntilMap.delete(accountId);
1916
+ return 0;
1917
+ }
1918
+ return remaining;
1919
+ }
1920
+ var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
1921
+ var init_session_guard = __esm(() => {
1922
+ init_logger();
1923
+ SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
1924
+ pauseUntilMap = new Map;
1925
+ });
1926
+
1927
+ // src/weixin/api/types.ts
1928
+ var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
1929
+ var init_types = __esm(() => {
1930
+ UploadMediaType = {
1931
+ IMAGE: 1,
1932
+ VIDEO: 2,
1933
+ FILE: 3,
1934
+ VOICE: 4
1935
+ };
1936
+ MessageType = {
1937
+ NONE: 0,
1938
+ USER: 1,
1939
+ BOT: 2
1940
+ };
1941
+ MessageItemType = {
1942
+ NONE: 0,
1943
+ TEXT: 1,
1944
+ IMAGE: 2,
1945
+ VOICE: 3,
1946
+ FILE: 4,
1947
+ VIDEO: 5
1948
+ };
1949
+ MessageState = {
1950
+ NEW: 0,
1951
+ GENERATING: 1,
1952
+ FINISH: 2
1953
+ };
1954
+ TypingStatus = {
1955
+ TYPING: 1,
1956
+ CANCEL: 2
1957
+ };
1958
+ });
1959
+
1960
+ // src/weixin/cdn/aes-ecb.ts
1961
+ import { createCipheriv, createDecipheriv } from "node:crypto";
1962
+ function encryptAesEcb(plaintext, key) {
1963
+ const cipher = createCipheriv("aes-128-ecb", key, null);
1964
+ return Buffer.concat([cipher.update(plaintext), cipher.final()]);
1965
+ }
1966
+ function decryptAesEcb(ciphertext, key) {
1967
+ const decipher = createDecipheriv("aes-128-ecb", key, null);
1968
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1969
+ }
1970
+ function aesEcbPaddedSize(plaintextSize) {
1971
+ return Math.ceil((plaintextSize + 1) / 16) * 16;
1972
+ }
1973
+ var init_aes_ecb = () => {};
1974
+
1975
+ // src/weixin/cdn/cdn-url.ts
1976
+ function buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl) {
1977
+ return `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`;
1978
+ }
1979
+ function buildCdnUploadUrl(params) {
1980
+ return `${params.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(params.uploadParam)}&filekey=${encodeURIComponent(params.filekey)}`;
1981
+ }
1982
+
1983
+ // src/weixin/cdn/cdn-upload.ts
1984
+ async function uploadBufferToCdn(params) {
1985
+ const { buf, uploadFullUrl, uploadParam, filekey, cdnBaseUrl, label, aeskey } = params;
1986
+ const ciphertext = encryptAesEcb(buf, aeskey);
1987
+ const trimmedFull = uploadFullUrl?.trim();
1988
+ let cdnUrl;
1989
+ if (trimmedFull) {
1990
+ cdnUrl = trimmedFull;
1991
+ } else if (uploadParam) {
1992
+ cdnUrl = buildCdnUploadUrl({ cdnBaseUrl, uploadParam, filekey });
1993
+ } else {
1994
+ throw new Error(`${label}: CDN upload URL missing (need upload_full_url or upload_param)`);
1995
+ }
1996
+ logger.debug(`${label}: CDN POST url=${redactUrl(cdnUrl)} ciphertextSize=${ciphertext.length}`);
1997
+ let downloadParam;
1998
+ let lastError;
1999
+ for (let attempt = 1;attempt <= UPLOAD_MAX_RETRIES; attempt++) {
2000
+ try {
2001
+ const res = await fetch(cdnUrl, {
2002
+ method: "POST",
2003
+ headers: { "Content-Type": "application/octet-stream" },
2004
+ body: new Uint8Array(ciphertext)
2005
+ });
2006
+ if (res.status >= 400 && res.status < 500) {
2007
+ const errMsg = res.headers.get("x-error-message") ?? await res.text();
2008
+ logger.error(`${label}: CDN client error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
2009
+ throw new Error(`CDN upload client error ${res.status}: ${errMsg}`);
2010
+ }
2011
+ if (res.status !== 200) {
2012
+ const errMsg = res.headers.get("x-error-message") ?? `status ${res.status}`;
2013
+ logger.error(`${label}: CDN server error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
2014
+ throw new Error(`CDN upload server error: ${errMsg}`);
2015
+ }
2016
+ downloadParam = res.headers.get("x-encrypted-param") ?? undefined;
2017
+ if (!downloadParam) {
2018
+ logger.error(`${label}: CDN response missing x-encrypted-param header attempt=${attempt}`);
2019
+ throw new Error("CDN upload response missing x-encrypted-param header");
2020
+ }
2021
+ logger.debug(`${label}: CDN upload success attempt=${attempt}`);
2022
+ break;
2023
+ } catch (err) {
2024
+ lastError = err;
2025
+ if (err instanceof Error && err.message.includes("client error"))
2026
+ throw err;
2027
+ if (attempt < UPLOAD_MAX_RETRIES) {
2028
+ logger.error(`${label}: attempt ${attempt} failed, retrying... err=${String(err)}`);
2029
+ } else {
2030
+ logger.error(`${label}: all ${UPLOAD_MAX_RETRIES} attempts failed err=${String(err)}`);
2031
+ }
2032
+ }
2033
+ }
2034
+ if (!downloadParam) {
2035
+ throw lastError instanceof Error ? lastError : new Error(`CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts`);
2036
+ }
2037
+ return { downloadParam };
2038
+ }
2039
+ var UPLOAD_MAX_RETRIES = 3;
2040
+ var init_cdn_upload = __esm(() => {
2041
+ init_aes_ecb();
2042
+ init_logger();
2043
+ });
2044
+
2045
+ // src/weixin/media/mime.ts
2046
+ import path5 from "node:path";
2047
+ function getMimeFromFilename(filename) {
2048
+ const ext = path5.extname(filename).toLowerCase();
2049
+ return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
2050
+ }
2051
+ function getExtensionFromMime(mimeType) {
2052
+ const ct = (mimeType.split(";")[0] ?? "").trim().toLowerCase();
2053
+ return MIME_TO_EXTENSION[ct] ?? ".bin";
2054
+ }
2055
+ function getExtensionFromContentTypeOrUrl(contentType, url) {
2056
+ if (contentType) {
2057
+ const ext2 = getExtensionFromMime(contentType);
2058
+ if (ext2 !== ".bin")
2059
+ return ext2;
2060
+ }
2061
+ const ext = path5.extname(new URL(url).pathname).toLowerCase();
2062
+ const knownExts = new Set(Object.keys(EXTENSION_TO_MIME));
2063
+ return knownExts.has(ext) ? ext : ".bin";
2064
+ }
2065
+ var EXTENSION_TO_MIME, MIME_TO_EXTENSION;
2066
+ var init_mime = __esm(() => {
2067
+ EXTENSION_TO_MIME = {
2068
+ ".pdf": "application/pdf",
2069
+ ".doc": "application/msword",
2070
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2071
+ ".xls": "application/vnd.ms-excel",
2072
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2073
+ ".ppt": "application/vnd.ms-powerpoint",
2074
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2075
+ ".txt": "text/plain",
2076
+ ".csv": "text/csv",
2077
+ ".zip": "application/zip",
2078
+ ".tar": "application/x-tar",
2079
+ ".gz": "application/gzip",
2080
+ ".mp3": "audio/mpeg",
2081
+ ".ogg": "audio/ogg",
2082
+ ".wav": "audio/wav",
2083
+ ".mp4": "video/mp4",
2084
+ ".mov": "video/quicktime",
2085
+ ".webm": "video/webm",
2086
+ ".mkv": "video/x-matroska",
2087
+ ".avi": "video/x-msvideo",
2088
+ ".png": "image/png",
2089
+ ".jpg": "image/jpeg",
2090
+ ".jpeg": "image/jpeg",
2091
+ ".gif": "image/gif",
2092
+ ".webp": "image/webp",
2093
+ ".bmp": "image/bmp"
2094
+ };
2095
+ MIME_TO_EXTENSION = {
2096
+ "image/jpeg": ".jpg",
2097
+ "image/jpg": ".jpg",
2098
+ "image/png": ".png",
2099
+ "image/gif": ".gif",
2100
+ "image/webp": ".webp",
2101
+ "image/bmp": ".bmp",
2102
+ "video/mp4": ".mp4",
2103
+ "video/quicktime": ".mov",
2104
+ "video/webm": ".webm",
2105
+ "video/x-matroska": ".mkv",
2106
+ "video/x-msvideo": ".avi",
2107
+ "audio/mpeg": ".mp3",
2108
+ "audio/ogg": ".ogg",
2109
+ "audio/wav": ".wav",
2110
+ "application/pdf": ".pdf",
2111
+ "application/zip": ".zip",
2112
+ "application/x-tar": ".tar",
2113
+ "application/gzip": ".gz",
2114
+ "text/plain": ".txt",
2115
+ "text/csv": ".csv"
2116
+ };
2117
+ });
2118
+
2119
+ // src/weixin/util/random.ts
2120
+ import crypto2 from "node:crypto";
2121
+ function generateId(prefix) {
2122
+ return `${prefix}:${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
2123
+ }
2124
+ function tempFileName(prefix, ext) {
2125
+ return `${prefix}-${Date.now()}-${crypto2.randomBytes(4).toString("hex")}${ext}`;
2126
+ }
2127
+ var init_random = () => {};
2128
+
2129
+ // src/weixin/cdn/upload.ts
2130
+ import crypto3 from "node:crypto";
2131
+ import fs4 from "node:fs/promises";
2132
+ import path6 from "node:path";
2133
+ async function downloadRemoteImageToTemp(url, destDir) {
2134
+ logger.debug(`downloadRemoteImageToTemp: fetching url=${url}`);
2135
+ const res = await fetch(url);
2136
+ if (!res.ok) {
2137
+ const msg = `remote media download failed: ${res.status} ${res.statusText} url=${url}`;
2138
+ logger.error(`downloadRemoteImageToTemp: ${msg}`);
2139
+ throw new Error(msg);
2140
+ }
2141
+ const buf = Buffer.from(await res.arrayBuffer());
2142
+ logger.debug(`downloadRemoteImageToTemp: downloaded ${buf.length} bytes`);
2143
+ await fs4.mkdir(destDir, { recursive: true });
2144
+ const ext = getExtensionFromContentTypeOrUrl(res.headers.get("content-type"), url);
2145
+ const name = tempFileName("weixin-remote", ext);
2146
+ const filePath = path6.join(destDir, name);
2147
+ await fs4.writeFile(filePath, buf);
2148
+ logger.debug(`downloadRemoteImageToTemp: saved to ${filePath} ext=${ext}`);
2149
+ return filePath;
2150
+ }
2151
+ async function uploadMediaToCdn(params) {
2152
+ const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
2153
+ const plaintext = await fs4.readFile(filePath);
2154
+ const rawsize = plaintext.length;
2155
+ const rawfilemd5 = crypto3.createHash("md5").update(plaintext).digest("hex");
2156
+ const filesize = aesEcbPaddedSize(rawsize);
2157
+ const filekey = crypto3.randomBytes(16).toString("hex");
2158
+ const aeskey = crypto3.randomBytes(16);
2159
+ logger.debug(`${label}: file=${filePath} rawsize=${rawsize} filesize=${filesize} md5=${rawfilemd5} filekey=${filekey}`);
2160
+ const uploadUrlResp = await getUploadUrl({
2161
+ ...opts,
2162
+ filekey,
2163
+ media_type: mediaType,
2164
+ to_user_id: toUserId,
2165
+ rawsize,
2166
+ rawfilemd5,
2167
+ filesize,
2168
+ no_need_thumb: true,
2169
+ aeskey: aeskey.toString("hex")
2170
+ });
2171
+ const uploadFullUrl = uploadUrlResp.upload_full_url?.trim();
2172
+ const uploadParam = uploadUrlResp.upload_param;
2173
+ if (!uploadFullUrl && !uploadParam) {
2174
+ logger.error(`${label}: getUploadUrl returned no upload URL (need upload_full_url or upload_param), resp=${JSON.stringify(uploadUrlResp)}`);
2175
+ throw new Error(`${label}: getUploadUrl returned no upload URL`);
2176
+ }
2177
+ const { downloadParam: downloadEncryptedQueryParam } = await uploadBufferToCdn({
2178
+ buf: plaintext,
2179
+ uploadFullUrl: uploadFullUrl || undefined,
2180
+ uploadParam: uploadParam ?? undefined,
2181
+ filekey,
2182
+ cdnBaseUrl,
2183
+ aeskey,
2184
+ label: `${label}[orig filekey=${filekey}]`
2185
+ });
2186
+ return {
2187
+ filekey,
2188
+ downloadEncryptedQueryParam,
2189
+ aeskey: aeskey.toString("hex"),
2190
+ fileSize: rawsize,
2191
+ fileSizeCiphertext: filesize
2192
+ };
2193
+ }
2194
+ async function uploadFileToWeixin(params) {
2195
+ return uploadMediaToCdn({
2196
+ ...params,
2197
+ mediaType: UploadMediaType.IMAGE,
2198
+ label: "uploadFileToWeixin"
2199
+ });
2200
+ }
2201
+ async function uploadVideoToWeixin(params) {
2202
+ return uploadMediaToCdn({
2203
+ ...params,
2204
+ mediaType: UploadMediaType.VIDEO,
2205
+ label: "uploadVideoToWeixin"
2206
+ });
2207
+ }
2208
+ async function uploadFileAttachmentToWeixin(params) {
2209
+ return uploadMediaToCdn({
2210
+ ...params,
2211
+ mediaType: UploadMediaType.FILE,
2212
+ label: "uploadFileAttachmentToWeixin"
2213
+ });
2214
+ }
2215
+ var init_upload = __esm(() => {
2216
+ init_api();
2217
+ init_aes_ecb();
2218
+ init_cdn_upload();
2219
+ init_logger();
2220
+ init_mime();
2221
+ init_random();
2222
+ init_types();
2223
+ });
2224
+
2225
+ // src/weixin/cdn/pic-decrypt.ts
2226
+ async function fetchCdnBytes(url, label) {
2227
+ let res;
2228
+ try {
2229
+ res = await fetch(url);
2230
+ } catch (err) {
2231
+ const cause = err.cause ?? err.code ?? "(no cause)";
2232
+ logger.error(`${label}: fetch network error url=${url} err=${String(err)} cause=${String(cause)}`);
2233
+ throw err;
2234
+ }
2235
+ logger.debug(`${label}: response status=${res.status} ok=${res.ok}`);
2236
+ if (!res.ok) {
2237
+ const body = await res.text().catch(() => "(unreadable)");
2238
+ const msg = `${label}: CDN download ${res.status} ${res.statusText} body=${body}`;
2239
+ logger.error(msg);
2240
+ throw new Error(msg);
2241
+ }
2242
+ return Buffer.from(await res.arrayBuffer());
2243
+ }
2244
+ function parseAesKey(aesKeyBase64, label) {
2245
+ const decoded = Buffer.from(aesKeyBase64, "base64");
2246
+ if (decoded.length === 16) {
2247
+ return decoded;
2248
+ }
2249
+ if (decoded.length === 32 && /^[0-9a-fA-F]{32}$/.test(decoded.toString("ascii"))) {
2250
+ return Buffer.from(decoded.toString("ascii"), "hex");
2251
+ }
2252
+ const msg = `${label}: aes_key must decode to 16 raw bytes or 32-char hex string, got ${decoded.length} bytes (base64="${aesKeyBase64}")`;
2253
+ logger.error(msg);
2254
+ throw new Error(msg);
2255
+ }
2256
+ async function downloadAndDecryptBuffer(encryptedQueryParam, aesKeyBase64, cdnBaseUrl, label, fullUrl) {
2257
+ const key = parseAesKey(aesKeyBase64, label);
2258
+ const url = fullUrl || buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
2259
+ logger.debug(`${label}: fetching url=${url}`);
2260
+ const encrypted = await fetchCdnBytes(url, label);
2261
+ logger.debug(`${label}: downloaded ${encrypted.byteLength} bytes, decrypting`);
2262
+ const decrypted = decryptAesEcb(encrypted, key);
2263
+ logger.debug(`${label}: decrypted ${decrypted.length} bytes`);
2264
+ return decrypted;
2265
+ }
2266
+ async function downloadPlainCdnBuffer(encryptedQueryParam, cdnBaseUrl, label, fullUrl) {
2267
+ const url = fullUrl || buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
2268
+ logger.debug(`${label}: fetching url=${url}`);
2269
+ return fetchCdnBytes(url, label);
2270
+ }
2271
+ var init_pic_decrypt = __esm(() => {
2272
+ init_aes_ecb();
2273
+ init_logger();
2274
+ });
2275
+
2276
+ // src/weixin/media/silk-transcode.ts
2277
+ function pcmBytesToWav(pcm, sampleRate) {
2278
+ const pcmBytes = pcm.byteLength;
2279
+ const totalSize = 44 + pcmBytes;
2280
+ const buf = Buffer.allocUnsafe(totalSize);
2281
+ let offset = 0;
2282
+ buf.write("RIFF", offset);
2283
+ offset += 4;
2284
+ buf.writeUInt32LE(totalSize - 8, offset);
2285
+ offset += 4;
2286
+ buf.write("WAVE", offset);
2287
+ offset += 4;
2288
+ buf.write("fmt ", offset);
2289
+ offset += 4;
2290
+ buf.writeUInt32LE(16, offset);
2291
+ offset += 4;
2292
+ buf.writeUInt16LE(1, offset);
2293
+ offset += 2;
2294
+ buf.writeUInt16LE(1, offset);
2295
+ offset += 2;
2296
+ buf.writeUInt32LE(sampleRate, offset);
2297
+ offset += 4;
2298
+ buf.writeUInt32LE(sampleRate * 2, offset);
2299
+ offset += 4;
2300
+ buf.writeUInt16LE(2, offset);
2301
+ offset += 2;
2302
+ buf.writeUInt16LE(16, offset);
2303
+ offset += 2;
2304
+ buf.write("data", offset);
2305
+ offset += 4;
2306
+ buf.writeUInt32LE(pcmBytes, offset);
2307
+ offset += 4;
2308
+ Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength).copy(buf, offset);
2309
+ return buf;
2310
+ }
2311
+ async function silkToWav(silkBuf) {
2312
+ try {
2313
+ const { decode } = await import("silk-wasm");
2314
+ logger.debug(`silkToWav: decoding ${silkBuf.length} bytes of SILK`);
2315
+ const result = await decode(silkBuf, SILK_SAMPLE_RATE);
2316
+ logger.debug(`silkToWav: decoded duration=${result.duration}ms pcmBytes=${result.data.byteLength}`);
2317
+ const wav = pcmBytesToWav(result.data, SILK_SAMPLE_RATE);
2318
+ logger.debug(`silkToWav: WAV size=${wav.length}`);
2319
+ return wav;
2320
+ } catch (err) {
2321
+ logger.warn(`silkToWav: transcode failed, will use raw silk err=${String(err)}`);
2322
+ return null;
2323
+ }
2324
+ }
2325
+ var SILK_SAMPLE_RATE = 24000;
2326
+ var init_silk_transcode = __esm(() => {
2327
+ init_logger();
2328
+ });
2329
+
2330
+ // src/weixin/media/media-download.ts
2331
+ async function downloadMediaFromItem(item, deps) {
2332
+ const { cdnBaseUrl, saveMedia, log, errLog, label } = deps;
2333
+ const result = {};
2334
+ if (item.type === MessageItemType.IMAGE) {
2335
+ const img = item.image_item;
2336
+ if (!img?.media?.encrypt_query_param && !img?.media?.full_url)
2337
+ return result;
2338
+ const aesKeyBase64 = img.aeskey ? Buffer.from(img.aeskey, "hex").toString("base64") : img.media.aes_key;
2339
+ logger.debug(`${label} image: encrypt_query_param=${(img.media.encrypt_query_param ?? "").slice(0, 40)}... hasAesKey=${Boolean(aesKeyBase64)} aeskeySource=${img.aeskey ? "image_item.aeskey" : "media.aes_key"} full_url=${Boolean(img.media.full_url)}`);
2340
+ try {
2341
+ const buf = aesKeyBase64 ? await downloadAndDecryptBuffer(img.media.encrypt_query_param ?? "", aesKeyBase64, cdnBaseUrl, `${label} image`, img.media.full_url) : await downloadPlainCdnBuffer(img.media.encrypt_query_param ?? "", cdnBaseUrl, `${label} image-plain`, img.media.full_url);
2342
+ const saved = await saveMedia(buf, undefined, "inbound", WEIXIN_MEDIA_MAX_BYTES);
2343
+ result.decryptedPicPath = saved.path;
2344
+ logger.debug(`${label} image saved: ${saved.path}`);
2345
+ } catch (err) {
2346
+ logger.error(`${label} image download/decrypt failed: ${String(err)}`);
2347
+ errLog(`weixin ${label} image download/decrypt failed: ${String(err)}`);
2348
+ }
2349
+ } else if (item.type === MessageItemType.VOICE) {
2350
+ const voice = item.voice_item;
2351
+ if (!voice?.media?.encrypt_query_param && !voice?.media?.full_url || !voice?.media?.aes_key)
2352
+ return result;
2353
+ try {
2354
+ const silkBuf = await downloadAndDecryptBuffer(voice.media.encrypt_query_param ?? "", voice.media.aes_key, cdnBaseUrl, `${label} voice`, voice.media.full_url);
2355
+ logger.debug(`${label} voice: decrypted ${silkBuf.length} bytes, attempting silk transcode`);
2356
+ const wavBuf = await silkToWav(silkBuf);
2357
+ if (wavBuf) {
2358
+ const saved = await saveMedia(wavBuf, "audio/wav", "inbound", WEIXIN_MEDIA_MAX_BYTES);
2359
+ result.decryptedVoicePath = saved.path;
2360
+ result.voiceMediaType = "audio/wav";
2361
+ logger.debug(`${label} voice: saved WAV to ${saved.path}`);
2362
+ } else {
2363
+ const saved = await saveMedia(silkBuf, "audio/silk", "inbound", WEIXIN_MEDIA_MAX_BYTES);
2364
+ result.decryptedVoicePath = saved.path;
2365
+ result.voiceMediaType = "audio/silk";
2366
+ logger.debug(`${label} voice: silk transcode unavailable, saved raw SILK to ${saved.path}`);
2367
+ }
2368
+ } catch (err) {
2369
+ logger.error(`${label} voice download/transcode failed: ${String(err)}`);
2370
+ errLog(`weixin ${label} voice download/transcode failed: ${String(err)}`);
2371
+ }
2372
+ } else if (item.type === MessageItemType.FILE) {
2373
+ const fileItem = item.file_item;
2374
+ if (!fileItem?.media?.encrypt_query_param && !fileItem?.media?.full_url || !fileItem?.media?.aes_key)
2375
+ return result;
2376
+ try {
2377
+ const buf = await downloadAndDecryptBuffer(fileItem.media.encrypt_query_param ?? "", fileItem.media.aes_key, cdnBaseUrl, `${label} file`, fileItem.media.full_url);
2378
+ const mime = getMimeFromFilename(fileItem.file_name ?? "file.bin");
2379
+ const saved = await saveMedia(buf, mime, "inbound", WEIXIN_MEDIA_MAX_BYTES, fileItem.file_name ?? undefined);
2380
+ result.decryptedFilePath = saved.path;
2381
+ result.fileMediaType = mime;
2382
+ logger.debug(`${label} file: saved to ${saved.path} mime=${mime}`);
2383
+ } catch (err) {
2384
+ logger.error(`${label} file download failed: ${String(err)}`);
2385
+ errLog(`weixin ${label} file download failed: ${String(err)}`);
2386
+ }
2387
+ } else if (item.type === MessageItemType.VIDEO) {
2388
+ const videoItem = item.video_item;
2389
+ if (!videoItem?.media?.encrypt_query_param && !videoItem?.media?.full_url || !videoItem?.media?.aes_key)
2390
+ return result;
2391
+ try {
2392
+ const buf = await downloadAndDecryptBuffer(videoItem.media.encrypt_query_param ?? "", videoItem.media.aes_key, cdnBaseUrl, `${label} video`, videoItem.media.full_url);
2393
+ const saved = await saveMedia(buf, "video/mp4", "inbound", WEIXIN_MEDIA_MAX_BYTES);
2394
+ result.decryptedVideoPath = saved.path;
2395
+ logger.debug(`${label} video: saved to ${saved.path}`);
2396
+ } catch (err) {
2397
+ logger.error(`${label} video download failed: ${String(err)}`);
2398
+ errLog(`weixin ${label} video download failed: ${String(err)}`);
2399
+ }
2400
+ }
2401
+ return result;
2402
+ }
2403
+ var WEIXIN_MEDIA_MAX_BYTES;
2404
+ var init_media_download = __esm(() => {
2405
+ init_logger();
2406
+ init_mime();
2407
+ init_pic_decrypt();
2408
+ init_silk_transcode();
2409
+ init_types();
2410
+ WEIXIN_MEDIA_MAX_BYTES = 100 * 1024 * 1024;
2411
+ });
2412
+
2413
+ // src/weixin/messaging/inbound.ts
2414
+ function contextTokenKey(accountId, userId) {
2415
+ return `${accountId}:${userId}`;
2416
+ }
2417
+ function setContextToken(accountId, userId, token) {
2418
+ const k = contextTokenKey(accountId, userId);
2419
+ logger.debug(`setContextToken: key=${k}`);
2420
+ contextTokenStore.set(k, token);
2421
+ }
2422
+ function getContextToken(accountId, userId) {
2423
+ const k = contextTokenKey(accountId, userId);
2424
+ const val = contextTokenStore.get(k);
2425
+ logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
2426
+ return val;
2427
+ }
2428
+ function isMediaItem(item) {
2429
+ return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
2430
+ }
2431
+ function bodyFromItemList(itemList) {
2432
+ if (!itemList?.length)
2433
+ return "";
2434
+ for (const item of itemList) {
2435
+ if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
2436
+ const text = String(item.text_item.text);
2437
+ const ref = item.ref_msg;
2438
+ if (!ref)
2439
+ return text;
2440
+ if (ref.message_item && isMediaItem(ref.message_item))
2441
+ return text;
2442
+ const parts = [];
2443
+ if (ref.title)
2444
+ parts.push(ref.title);
2445
+ if (ref.message_item) {
2446
+ const refBody = bodyFromItemList([ref.message_item]);
2447
+ if (refBody)
2448
+ parts.push(refBody);
2449
+ }
2450
+ if (!parts.length)
2451
+ return text;
2452
+ return `[引用: ${parts.join(" | ")}]
2453
+ ${text}`;
2454
+ }
2455
+ if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
2456
+ return item.voice_item.text;
2457
+ }
2458
+ }
2459
+ return "";
2460
+ }
2461
+ var contextTokenStore;
2462
+ var init_inbound = __esm(() => {
2463
+ init_logger();
2464
+ init_random();
2465
+ init_types();
2466
+ contextTokenStore = new Map;
2467
+ });
2468
+
2469
+ // src/weixin/messaging/send.ts
2470
+ function generateClientId() {
2471
+ return generateId("openclaw-weixin");
2472
+ }
2473
+ function markdownToPlainText(text) {
2474
+ let result = text;
2475
+ result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
2476
+ result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
2477
+ result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
2478
+ result = result.replace(/^\|[\s:|-]+\|$/gm, "");
2479
+ result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split("|").map((cell) => cell.trim()).join(" "));
2480
+ result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
2481
+ return result;
2482
+ }
2483
+ function buildTextMessageReq(params) {
2484
+ const { to, text, contextToken, clientId } = params;
2485
+ const item_list = text ? [{ type: MessageItemType.TEXT, text_item: { text } }] : [];
2486
+ return {
2487
+ msg: {
2488
+ from_user_id: "",
2489
+ to_user_id: to,
2490
+ client_id: clientId,
2491
+ message_type: MessageType.BOT,
2492
+ message_state: MessageState.FINISH,
2493
+ item_list: item_list.length ? item_list : undefined,
2494
+ context_token: contextToken ?? undefined
2495
+ }
2496
+ };
2497
+ }
2498
+ function buildSendMessageReq(params) {
2499
+ const { to, contextToken, text, clientId } = params;
2500
+ return buildTextMessageReq({ to, text, contextToken, clientId });
2501
+ }
2502
+ async function sendMessageWeixin(params) {
2503
+ const { to, text, opts } = params;
2504
+ if (!opts.contextToken) {
2505
+ logger.error(`sendMessageWeixin: contextToken missing, refusing to send to=${to}`);
2506
+ throw new Error("sendMessageWeixin: contextToken is required");
2507
+ }
2508
+ const clientId = generateClientId();
2509
+ const req = buildSendMessageReq({
2510
+ to,
2511
+ contextToken: opts.contextToken,
2512
+ text,
2513
+ clientId
2514
+ });
2515
+ try {
2516
+ await sendMessage({
2517
+ baseUrl: opts.baseUrl,
2518
+ token: opts.token,
2519
+ timeoutMs: opts.timeoutMs,
2520
+ body: req
2521
+ });
2522
+ } catch (err) {
2523
+ logger.error(`sendMessageWeixin: failed to=${to} clientId=${clientId} err=${String(err)}`);
2524
+ throw err;
2525
+ }
2526
+ return { messageId: clientId };
2527
+ }
2528
+ async function sendMediaItems(params) {
2529
+ const { to, text, mediaItem, opts, label } = params;
2530
+ const items = [];
2531
+ if (text) {
2532
+ items.push({ type: MessageItemType.TEXT, text_item: { text } });
2533
+ }
2534
+ items.push(mediaItem);
2535
+ let lastClientId = "";
2536
+ for (const item of items) {
2537
+ lastClientId = generateClientId();
2538
+ const req = {
2539
+ msg: {
2540
+ from_user_id: "",
2541
+ to_user_id: to,
2542
+ client_id: lastClientId,
2543
+ message_type: MessageType.BOT,
2544
+ message_state: MessageState.FINISH,
2545
+ item_list: [item],
2546
+ context_token: opts.contextToken ?? undefined
2547
+ }
2548
+ };
2549
+ try {
2550
+ await sendMessage({
2551
+ baseUrl: opts.baseUrl,
2552
+ token: opts.token,
2553
+ timeoutMs: opts.timeoutMs,
2554
+ body: req
2555
+ });
2556
+ } catch (err) {
2557
+ logger.error(`${label}: failed to=${to} clientId=${lastClientId} err=${String(err)}`);
2558
+ throw err;
2559
+ }
2560
+ }
2561
+ logger.debug(`${label}: success to=${to} clientId=${lastClientId}`);
2562
+ return { messageId: lastClientId };
2563
+ }
2564
+ async function sendImageMessageWeixin(params) {
2565
+ const { to, text, uploaded, opts } = params;
2566
+ if (!opts.contextToken) {
2567
+ logger.error(`sendImageMessageWeixin: contextToken missing, refusing to send to=${to}`);
2568
+ throw new Error("sendImageMessageWeixin: contextToken is required");
2569
+ }
2570
+ logger.debug(`sendImageMessageWeixin: to=${to} filekey=${uploaded.filekey} fileSize=${uploaded.fileSize} aeskey=present`);
2571
+ const imageItem = {
2572
+ type: MessageItemType.IMAGE,
2573
+ image_item: {
2574
+ media: {
2575
+ encrypt_query_param: uploaded.downloadEncryptedQueryParam,
2576
+ aes_key: Buffer.from(uploaded.aeskey).toString("base64"),
2577
+ encrypt_type: 1
2578
+ },
2579
+ mid_size: uploaded.fileSizeCiphertext
2580
+ }
2581
+ };
2582
+ return sendMediaItems({ to, text, mediaItem: imageItem, opts, label: "sendImageMessageWeixin" });
2583
+ }
2584
+ async function sendVideoMessageWeixin(params) {
2585
+ const { to, text, uploaded, opts } = params;
2586
+ if (!opts.contextToken) {
2587
+ logger.error(`sendVideoMessageWeixin: contextToken missing, refusing to send to=${to}`);
2588
+ throw new Error("sendVideoMessageWeixin: contextToken is required");
2589
+ }
2590
+ const videoItem = {
2591
+ type: MessageItemType.VIDEO,
2592
+ video_item: {
2593
+ media: {
2594
+ encrypt_query_param: uploaded.downloadEncryptedQueryParam,
2595
+ aes_key: Buffer.from(uploaded.aeskey).toString("base64"),
2596
+ encrypt_type: 1
2597
+ },
2598
+ video_size: uploaded.fileSizeCiphertext
2599
+ }
2600
+ };
2601
+ return sendMediaItems({ to, text, mediaItem: videoItem, opts, label: "sendVideoMessageWeixin" });
2602
+ }
2603
+ async function sendFileMessageWeixin(params) {
2604
+ const { to, text, fileName, uploaded, opts } = params;
2605
+ if (!opts.contextToken) {
2606
+ logger.error(`sendFileMessageWeixin: contextToken missing, refusing to send to=${to}`);
2607
+ throw new Error("sendFileMessageWeixin: contextToken is required");
2608
+ }
2609
+ const fileItem = {
2610
+ type: MessageItemType.FILE,
2611
+ file_item: {
2612
+ media: {
2613
+ encrypt_query_param: uploaded.downloadEncryptedQueryParam,
2614
+ aes_key: Buffer.from(uploaded.aeskey).toString("base64"),
2615
+ encrypt_type: 1
2616
+ },
2617
+ file_name: fileName,
2618
+ len: String(uploaded.fileSize)
2619
+ }
2620
+ };
2621
+ return sendMediaItems({ to, text, mediaItem: fileItem, opts, label: "sendFileMessageWeixin" });
2622
+ }
2623
+ var init_send = __esm(() => {
2624
+ init_api();
2625
+ init_logger();
2626
+ init_random();
2627
+ init_types();
2628
+ });
2629
+
2630
+ // src/weixin/messaging/error-notice.ts
2631
+ async function sendWeixinErrorNotice(params) {
2632
+ if (!params.contextToken) {
2633
+ logger.warn(`sendWeixinErrorNotice: no contextToken for to=${params.to}, cannot notify user`);
2634
+ return;
2635
+ }
2636
+ try {
2637
+ await sendMessageWeixin({ to: params.to, text: params.message, opts: {
2638
+ baseUrl: params.baseUrl,
2639
+ token: params.token,
2640
+ contextToken: params.contextToken
2641
+ } });
2642
+ logger.debug(`sendWeixinErrorNotice: sent to=${params.to}`);
2643
+ } catch (err) {
2644
+ params.errLog(`[weixin] sendWeixinErrorNotice failed to=${params.to}: ${String(err)}`);
2645
+ }
2646
+ }
2647
+ var init_error_notice = __esm(() => {
2648
+ init_logger();
2649
+ init_send();
2650
+ });
2651
+
2652
+ // src/weixin/messaging/send-media.ts
2653
+ import path7 from "node:path";
2654
+ async function sendWeixinMediaFile(params) {
2655
+ const { filePath, to, text, opts, cdnBaseUrl } = params;
2656
+ const mime = getMimeFromFilename(filePath);
2657
+ const uploadOpts = { baseUrl: opts.baseUrl, token: opts.token };
2658
+ if (mime.startsWith("video/")) {
2659
+ logger.info(`[weixin] sendWeixinMediaFile: uploading video filePath=${filePath} to=${to}`);
2660
+ const uploaded2 = await uploadVideoToWeixin({
2661
+ filePath,
2662
+ toUserId: to,
2663
+ opts: uploadOpts,
2664
+ cdnBaseUrl
2665
+ });
2666
+ logger.info(`[weixin] sendWeixinMediaFile: video upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
2667
+ return sendVideoMessageWeixin({ to, text, uploaded: uploaded2, opts });
2668
+ }
2669
+ if (mime.startsWith("image/")) {
2670
+ logger.info(`[weixin] sendWeixinMediaFile: uploading image filePath=${filePath} to=${to}`);
2671
+ const uploaded2 = await uploadFileToWeixin({
2672
+ filePath,
2673
+ toUserId: to,
2674
+ opts: uploadOpts,
2675
+ cdnBaseUrl
2676
+ });
2677
+ logger.info(`[weixin] sendWeixinMediaFile: image upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
2678
+ return sendImageMessageWeixin({ to, text, uploaded: uploaded2, opts });
2679
+ }
2680
+ const fileName = path7.basename(filePath);
2681
+ logger.info(`[weixin] sendWeixinMediaFile: uploading file attachment filePath=${filePath} name=${fileName} to=${to}`);
2682
+ const uploaded = await uploadFileAttachmentToWeixin({
2683
+ filePath,
2684
+ fileName,
2685
+ toUserId: to,
2686
+ opts: uploadOpts,
2687
+ cdnBaseUrl
2688
+ });
2689
+ logger.info(`[weixin] sendWeixinMediaFile: file upload done filekey=${uploaded.filekey} size=${uploaded.fileSize}`);
2690
+ return sendFileMessageWeixin({ to, text, fileName, uploaded, opts });
2691
+ }
2692
+ var init_send_media = __esm(() => {
2693
+ init_logger();
2694
+ init_mime();
2695
+ init_send();
2696
+ init_upload();
2697
+ });
2698
+
2699
+ // src/weixin/messaging/debug-mode.ts
2700
+ import fs5 from "node:fs";
2701
+ import path8 from "node:path";
2702
+ function resolveDebugModePath() {
2703
+ return path8.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
2704
+ }
2705
+ function loadState() {
2706
+ try {
2707
+ const raw = fs5.readFileSync(resolveDebugModePath(), "utf-8");
2708
+ const parsed = JSON.parse(raw);
2709
+ if (parsed && typeof parsed.accounts === "object")
2710
+ return parsed;
2711
+ } catch {}
2712
+ return { accounts: {} };
2713
+ }
2714
+ function saveState(state) {
2715
+ const filePath = resolveDebugModePath();
2716
+ fs5.mkdirSync(path8.dirname(filePath), { recursive: true });
2717
+ fs5.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
2718
+ }
2719
+ function toggleDebugMode(accountId) {
2720
+ const state = loadState();
2721
+ const next = !state.accounts[accountId];
2722
+ state.accounts[accountId] = next;
2723
+ try {
2724
+ saveState(state);
2725
+ } catch (err) {
2726
+ logger.error(`debug-mode: failed to persist state: ${String(err)}`);
2727
+ }
2728
+ return next;
2729
+ }
2730
+ var init_debug_mode = __esm(() => {
2731
+ init_state_dir();
2732
+ init_logger();
1055
2733
  });
1056
- function buildWeixinSdkImportCandidates(explicitPath, _moduleUrl = import.meta.url) {
1057
- const candidates = [];
1058
- if (explicitPath) {
1059
- candidates.push(explicitPath);
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)}`);
2798
+ try {
2799
+ await sendReply(ctx, `❌ 指令执行失败: ${String(err).slice(0, 200)}`);
2800
+ } catch {}
2801
+ return { handled: true };
1060
2802
  }
1061
- candidates.push("weixin-agent-sdk");
1062
- return candidates;
1063
2803
  }
1064
- function buildWeixinSdkSourceCandidates(explicitPath, _moduleUrl = import.meta.url) {
1065
- if (explicitPath) {
1066
- return [explicitPath];
2804
+ var init_slash_commands = __esm(() => {
2805
+ init_accounts();
2806
+ init_logger();
2807
+ init_debug_mode();
2808
+ init_send();
2809
+ });
2810
+
2811
+ // src/weixin/messaging/process-message.ts
2812
+ import crypto4 from "node:crypto";
2813
+ import fs6 from "node:fs/promises";
2814
+ import path9 from "node:path";
2815
+ async function saveMediaBuffer(buffer, contentType, subdir, _maxBytes, originalFilename) {
2816
+ const dir = path9.join(MEDIA_TEMP_DIR, subdir ?? "");
2817
+ await fs6.mkdir(dir, { recursive: true });
2818
+ let ext = ".bin";
2819
+ if (originalFilename) {
2820
+ ext = path9.extname(originalFilename) || ".bin";
2821
+ } else if (contentType) {
2822
+ ext = getExtensionFromMime(contentType);
2823
+ }
2824
+ const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
2825
+ const filePath = path9.join(dir, name);
2826
+ await fs6.writeFile(filePath, buffer);
2827
+ return { path: filePath };
2828
+ }
2829
+ function extractTextBody(itemList) {
2830
+ if (!itemList?.length)
2831
+ return "";
2832
+ for (const item of itemList) {
2833
+ if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
2834
+ return String(item.text_item.text);
2835
+ }
1067
2836
  }
1068
- return [];
2837
+ return "";
1069
2838
  }
1070
- async function loadWeixinSdk() {
1071
- const candidates = buildWeixinSdkImportCandidates(process.env.WEACPX_WEIXIN_SDK, import.meta.url);
1072
- const errors = [];
1073
- for (const candidate of candidates) {
2839
+ function findMediaItem(itemList) {
2840
+ if (!itemList?.length)
2841
+ return;
2842
+ const direct = itemList.find((i) => i.type === MessageItemType.IMAGE && hasDownloadableMedia(i.image_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.VIDEO && hasDownloadableMedia(i.video_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.FILE && hasDownloadableMedia(i.file_item?.media)) ?? itemList.find((i) => i.type === MessageItemType.VOICE && hasDownloadableMedia(i.voice_item?.media) && !i.voice_item?.text);
2843
+ if (direct)
2844
+ return direct;
2845
+ const refItem = itemList.find((i) => i.type === MessageItemType.TEXT && i.ref_msg?.message_item && isMediaItem(i.ref_msg.message_item));
2846
+ return refItem?.ref_msg?.message_item ?? undefined;
2847
+ }
2848
+ async function processOneMessage(full, deps) {
2849
+ const receivedAt = Date.now();
2850
+ const textBody = extractTextBody(full.item_list);
2851
+ if (textBody.startsWith("/")) {
2852
+ const conversationId = full.from_user_id ?? "";
2853
+ const slashResult = await handleSlashCommand(textBody, {
2854
+ to: conversationId,
2855
+ contextToken: full.context_token,
2856
+ baseUrl: deps.baseUrl,
2857
+ token: deps.token,
2858
+ accountId: deps.accountId,
2859
+ log: deps.log,
2860
+ errLog: deps.errLog,
2861
+ onClear: () => deps.agent.clearSession?.(conversationId)
2862
+ }, receivedAt, full.create_time_ms);
2863
+ if (slashResult.handled)
2864
+ return;
2865
+ }
2866
+ const contextToken = full.context_token;
2867
+ if (contextToken) {
2868
+ setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
2869
+ }
2870
+ let media;
2871
+ const mediaItem = findMediaItem(full.item_list);
2872
+ if (mediaItem) {
1074
2873
  try {
1075
- return await import(candidate);
1076
- } catch (error) {
1077
- const message = error instanceof Error ? error.message : String(error);
1078
- errors.push(`${candidate}: ${message}`);
2874
+ const downloaded = await downloadMediaFromItem(mediaItem, {
2875
+ cdnBaseUrl: deps.cdnBaseUrl,
2876
+ saveMedia: saveMediaBuffer,
2877
+ log: deps.log,
2878
+ errLog: deps.errLog,
2879
+ label: "inbound"
2880
+ });
2881
+ if (downloaded.decryptedPicPath) {
2882
+ media = { type: "image", filePath: downloaded.decryptedPicPath, mimeType: "image/*" };
2883
+ } else if (downloaded.decryptedVideoPath) {
2884
+ media = { type: "video", filePath: downloaded.decryptedVideoPath, mimeType: "video/mp4" };
2885
+ } else if (downloaded.decryptedFilePath) {
2886
+ media = {
2887
+ type: "file",
2888
+ filePath: downloaded.decryptedFilePath,
2889
+ mimeType: downloaded.fileMediaType ?? "application/octet-stream"
2890
+ };
2891
+ } else if (downloaded.decryptedVoicePath) {
2892
+ media = {
2893
+ type: "audio",
2894
+ filePath: downloaded.decryptedVoicePath,
2895
+ mimeType: downloaded.voiceMediaType ?? "audio/wav"
2896
+ };
2897
+ }
2898
+ } catch (err) {
2899
+ logger.error(`media download failed: ${String(err)}`);
2900
+ }
2901
+ }
2902
+ const to = full.from_user_id ?? "";
2903
+ const reply = async (text) => {
2904
+ try {
2905
+ await sendMessageWeixin({
2906
+ to,
2907
+ text: markdownToPlainText(text),
2908
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
2909
+ });
2910
+ } catch (err) {
2911
+ logger.error(`intermediate reply failed: ${String(err)}`);
2912
+ }
2913
+ };
2914
+ const request = {
2915
+ conversationId: full.from_user_id ?? "",
2916
+ text: bodyFromItemList(full.item_list),
2917
+ media,
2918
+ reply
2919
+ };
2920
+ let typingTimer;
2921
+ const startTyping = () => {
2922
+ if (!deps.typingTicket)
2923
+ return;
2924
+ sendTyping({
2925
+ baseUrl: deps.baseUrl,
2926
+ token: deps.token,
2927
+ body: {
2928
+ ilink_user_id: to,
2929
+ typing_ticket: deps.typingTicket,
2930
+ status: TypingStatus.TYPING
2931
+ }
2932
+ }).catch(() => {});
2933
+ };
2934
+ if (deps.typingTicket) {
2935
+ startTyping();
2936
+ typingTimer = setInterval(startTyping, 1e4);
2937
+ }
2938
+ try {
2939
+ const response = await deps.agent.chat(request);
2940
+ if (response.media) {
2941
+ let filePath;
2942
+ const mediaUrl = response.media.url;
2943
+ if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
2944
+ filePath = await downloadRemoteImageToTemp(mediaUrl, path9.join(MEDIA_TEMP_DIR, "outbound"));
2945
+ } else {
2946
+ filePath = path9.isAbsolute(mediaUrl) ? mediaUrl : path9.resolve(mediaUrl);
2947
+ }
2948
+ await sendWeixinMediaFile({
2949
+ filePath,
2950
+ to,
2951
+ text: response.text ? markdownToPlainText(response.text) : "",
2952
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
2953
+ cdnBaseUrl: deps.cdnBaseUrl
2954
+ });
2955
+ } else if (response.text) {
2956
+ await sendMessageWeixin({
2957
+ to,
2958
+ text: markdownToPlainText(response.text),
2959
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
2960
+ });
2961
+ }
2962
+ } catch (err) {
2963
+ logger.error(`processOneMessage: agent or send failed: ${err instanceof Error ? err.stack ?? err.message : JSON.stringify(err)}`);
2964
+ sendWeixinErrorNotice({
2965
+ to,
2966
+ contextToken,
2967
+ message: `⚠️ 过程失败:${err instanceof Error ? err.message : JSON.stringify(err)}`,
2968
+ baseUrl: deps.baseUrl,
2969
+ token: deps.token,
2970
+ errLog: deps.errLog
2971
+ });
2972
+ } finally {
2973
+ if (typingTimer)
2974
+ clearInterval(typingTimer);
2975
+ if (deps.typingTicket) {
2976
+ sendTyping({
2977
+ baseUrl: deps.baseUrl,
2978
+ token: deps.token,
2979
+ body: {
2980
+ ilink_user_id: to,
2981
+ typing_ticket: deps.typingTicket,
2982
+ status: TypingStatus.CANCEL
2983
+ }
2984
+ }).catch(() => {});
1079
2985
  }
1080
2986
  }
1081
- throw new Error([
1082
- "Unable to load weixin-agent-sdk.",
1083
- "Tried:",
1084
- ...errors.map((entry) => `- ${entry}`),
1085
- 'Set WEACPX_WEIXIN_SDK to a local SDK entry file, or install the "weixin-agent-sdk" package.'
1086
- ].join(`
1087
- `));
1088
2987
  }
2988
+ var MEDIA_TEMP_DIR = "/tmp/weixin-agent/media", hasDownloadableMedia = (m) => m?.encrypt_query_param || m?.full_url;
2989
+ var init_process_message = __esm(() => {
2990
+ init_api();
2991
+ init_types();
2992
+ init_upload();
2993
+ init_media_download();
2994
+ init_mime();
2995
+ init_logger();
2996
+ init_inbound();
2997
+ init_error_notice();
2998
+ init_send_media();
2999
+ init_send();
3000
+ init_slash_commands();
3001
+ });
1089
3002
 
1090
- // src/login.ts
1091
- var exports_login = {};
1092
- __export(exports_login, {
1093
- main: () => main,
1094
- loginWithQrRendering: () => loginWithQrRendering
3003
+ // src/weixin/storage/sync-buf.ts
3004
+ import fs7 from "node:fs";
3005
+ import path10 from "node:path";
3006
+ function resolveAccountsDir2() {
3007
+ return path10.join(resolveStateDir(), "openclaw-weixin", "accounts");
3008
+ }
3009
+ function getSyncBufFilePath(accountId) {
3010
+ return path10.join(resolveAccountsDir2(), `${accountId}.sync.json`);
3011
+ }
3012
+ function getLegacySyncBufDefaultJsonPath() {
3013
+ return path10.join(resolveStateDir(), "agents", "default", "sessions", ".openclaw-weixin-sync", "default.json");
3014
+ }
3015
+ function readSyncBufFile(filePath) {
3016
+ try {
3017
+ const raw = fs7.readFileSync(filePath, "utf-8");
3018
+ const data = JSON.parse(raw);
3019
+ if (typeof data.get_updates_buf === "string") {
3020
+ return data.get_updates_buf;
3021
+ }
3022
+ } catch {}
3023
+ return;
3024
+ }
3025
+ function loadGetUpdatesBuf(filePath) {
3026
+ const value = readSyncBufFile(filePath);
3027
+ if (value !== undefined)
3028
+ return value;
3029
+ const accountId = path10.basename(filePath, ".sync.json");
3030
+ const rawId = deriveRawAccountId(accountId);
3031
+ if (rawId) {
3032
+ const compatPath = path10.join(resolveAccountsDir2(), `${rawId}.sync.json`);
3033
+ const compatValue = readSyncBufFile(compatPath);
3034
+ if (compatValue !== undefined)
3035
+ return compatValue;
3036
+ }
3037
+ return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
3038
+ }
3039
+ function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
3040
+ const dir = path10.dirname(filePath);
3041
+ fs7.mkdirSync(dir, { recursive: true });
3042
+ fs7.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
3043
+ }
3044
+ var init_sync_buf = __esm(() => {
3045
+ init_accounts();
3046
+ init_state_dir();
1095
3047
  });
1096
- async function loadQrLoginSupport() {
1097
- const candidates = buildWeixinSdkSourceCandidates(process.env.WEACPX_WEIXIN_SDK);
1098
- for (const candidate of candidates) {
3048
+
3049
+ // src/weixin/monitor/monitor.ts
3050
+ async function monitorWeixinProvider(opts) {
3051
+ const {
3052
+ baseUrl,
3053
+ cdnBaseUrl,
3054
+ token,
3055
+ accountId,
3056
+ agent,
3057
+ abortSignal,
3058
+ longPollTimeoutMs
3059
+ } = opts;
3060
+ const log = opts.log ?? ((msg) => console.log(msg));
3061
+ const errLog = (msg) => {
3062
+ log(msg);
3063
+ logger.error(msg);
3064
+ };
3065
+ const aLog = logger.withAccount(accountId);
3066
+ log(`[weixin] monitor started (${baseUrl}, account=${accountId})`);
3067
+ aLog.info(`Monitor started: baseUrl=${baseUrl}`);
3068
+ const syncFilePath = getSyncBufFilePath(accountId);
3069
+ const previousGetUpdatesBuf = loadGetUpdatesBuf(syncFilePath);
3070
+ let getUpdatesBuf = previousGetUpdatesBuf ?? "";
3071
+ if (previousGetUpdatesBuf) {
3072
+ log(`[weixin] resuming from previous sync buf (${getUpdatesBuf.length} bytes)`);
3073
+ } else {
3074
+ log(`[weixin] no previous sync buf, starting fresh`);
3075
+ }
3076
+ const configManager = new WeixinConfigManager({ baseUrl, token }, log);
3077
+ let nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
3078
+ let consecutiveFailures = 0;
3079
+ while (!abortSignal?.aborted) {
1099
3080
  try {
1100
- const sourceUrl = candidate.startsWith("file:") ? candidate : new URL(candidate, import.meta.url).href;
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 };
1104
- } catch {}
3081
+ const resp = await getUpdates({
3082
+ baseUrl,
3083
+ token,
3084
+ get_updates_buf: getUpdatesBuf,
3085
+ timeoutMs: nextTimeoutMs,
3086
+ abortSignal
3087
+ });
3088
+ if (resp.longpolling_timeout_ms != null && resp.longpolling_timeout_ms > 0) {
3089
+ nextTimeoutMs = resp.longpolling_timeout_ms;
3090
+ }
3091
+ const isApiError = resp.ret !== undefined && resp.ret !== 0 || resp.errcode !== undefined && resp.errcode !== 0;
3092
+ if (isApiError) {
3093
+ const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
3094
+ if (isSessionExpired) {
3095
+ pauseSession(accountId);
3096
+ const pauseMs = getRemainingPauseMs(accountId);
3097
+ errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), pausing for ${Math.ceil(pauseMs / 60000)} min. Please run \`npx weixin-acp login\` to re-login.`);
3098
+ consecutiveFailures = 0;
3099
+ await sleep(pauseMs, abortSignal);
3100
+ continue;
3101
+ }
3102
+ consecutiveFailures += 1;
3103
+ errLog(`[weixin] getUpdates failed: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? ""} (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
3104
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
3105
+ errLog(`[weixin] ${MAX_CONSECUTIVE_FAILURES} consecutive failures, backing off 30s`);
3106
+ consecutiveFailures = 0;
3107
+ await sleep(BACKOFF_DELAY_MS, abortSignal);
3108
+ } else {
3109
+ await sleep(RETRY_DELAY_MS, abortSignal);
3110
+ }
3111
+ continue;
3112
+ }
3113
+ consecutiveFailures = 0;
3114
+ if (resp.get_updates_buf != null && resp.get_updates_buf !== "") {
3115
+ saveGetUpdatesBuf(syncFilePath, resp.get_updates_buf);
3116
+ getUpdatesBuf = resp.get_updates_buf;
3117
+ }
3118
+ const list = resp.msgs ?? [];
3119
+ for (const full of list) {
3120
+ aLog.info(`inbound: from=${full.from_user_id} types=${full.item_list?.map((i) => i.type).join(",") ?? "none"}`);
3121
+ const fromUserId = full.from_user_id ?? "";
3122
+ const cachedConfig = await configManager.getForUser(fromUserId, full.context_token);
3123
+ await processOneMessage(full, {
3124
+ accountId,
3125
+ agent,
3126
+ baseUrl,
3127
+ cdnBaseUrl,
3128
+ token,
3129
+ typingTicket: cachedConfig.typingTicket,
3130
+ log,
3131
+ errLog
3132
+ });
3133
+ }
3134
+ } catch (err) {
3135
+ if (abortSignal?.aborted) {
3136
+ aLog.info(`Monitor stopped (aborted)`);
3137
+ return;
3138
+ }
3139
+ consecutiveFailures += 1;
3140
+ errLog(`[weixin] getUpdates error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${String(err)}`);
3141
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
3142
+ consecutiveFailures = 0;
3143
+ await sleep(BACKOFF_DELAY_MS, abortSignal);
3144
+ } else {
3145
+ await sleep(RETRY_DELAY_MS, abortSignal);
3146
+ }
3147
+ }
1105
3148
  }
1106
- return null;
3149
+ aLog.info(`Monitor ended`);
1107
3150
  }
1108
- async function loginWithQrRendering() {
1109
- const support = await loadQrLoginSupport();
1110
- if (!support) {
1111
- const { login } = await loadWeixinSdk();
1112
- await login();
1113
- return;
1114
- }
1115
- const { accounts, loginQr } = support;
1116
- const apiBaseUrl = accounts.DEFAULT_BASE_URL;
1117
- console.log("正在启动微信扫码登录...");
1118
- const startResult = await loginQr.startWeixinLoginWithQr({
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: loginQr.DEFAULT_ILINK_BOT_TYPE
3177
+ botType: DEFAULT_ILINK_BOT_TYPE
1121
3178
  });
1122
3179
  if (!startResult.qrcodeUrl) {
1123
3180
  throw new Error(startResult.message);
1124
3181
  }
1125
- console.log(`
3182
+ log(`
1126
3183
  使用微信扫描以下二维码,以完成连接:
1127
3184
  `);
1128
- await new Promise((resolve) => {
1129
- import_qrcode_terminal.default.generate(startResult.qrcodeUrl, { small: true }, (qr) => {
1130
- console.log(qr);
1131
- resolve();
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
- console.log(`
3193
+ } catch {
3194
+ log(`二维码链接: ${startResult.qrcodeUrl}`);
3195
+ }
3196
+ log(`
1135
3197
  等待扫码...
1136
3198
  `);
1137
- const waitResult = await loginQr.waitForWeixinLogin({
3199
+ const waitResult = await waitForWeixinLogin({
1138
3200
  sessionKey: startResult.sessionKey,
1139
3201
  apiBaseUrl,
1140
3202
  timeoutMs: 480000,
1141
- botType: loginQr.DEFAULT_ILINK_BOT_TYPE
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 = accounts.normalizeAccountId(waitResult.accountId);
1147
- accounts.saveWeixinAccount(normalizedId, {
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
- accounts.registerWeixinAccountId(normalizedId);
1153
- console.log(`
3214
+ registerWeixinAccountId(normalizedId);
3215
+ log(`
1154
3216
  ✅ 与微信连接成功!`);
3217
+ return normalizedId;
1155
3218
  }
1156
- async function main() {
1157
- await loginWithQrRendering();
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
- var import_qrcode_terminal;
1160
- var init_login = __esm(async () => {
1161
- import_qrcode_terminal = __toESM(require_main(), 1);
1162
- if (false) {}
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 writeLog("debug", event, message, context);
3395
+ await writeLog2("debug", event, message, context);
1246
3396
  },
1247
3397
  info: async (event, message, context) => {
1248
- await writeLog("info", event, message, context);
3398
+ await writeLog2("info", event, message, context);
1249
3399
  },
1250
3400
  error: async (event, message, context) => {
1251
- await writeLog("error", event, message, context);
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 writeLog(level, event, message, context = {}) {
3407
+ async function writeLog2(level, event, message, context = {}) {
1258
3408
  if (LEVEL_ORDER[level] > LEVEL_ORDER[options.level]) {
1259
3409
  return;
1260
3410
  }
@@ -1326,24 +3476,156 @@ function formatLogLine(time, level, event, message, context) {
1326
3476
  return `${time.toISOString()} ${level.toUpperCase()} ${event} message=${formatValue(message)}${suffix}
1327
3477
  `;
1328
3478
  }
1329
- function formatValue(value) {
1330
- if (value === null) {
1331
- return "null";
3479
+ function formatValue(value) {
3480
+ if (value === null) {
3481
+ return "null";
3482
+ }
3483
+ if (typeof value === "number" || typeof value === "boolean") {
3484
+ return String(value);
3485
+ }
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;
1332
3587
  }
1333
- if (typeof value === "number" || typeof value === "boolean") {
1334
- return String(value);
3588
+ const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
3589
+ if (!looksLikeWorkflowPreamble(firstParagraph)) {
3590
+ return trimmed;
1335
3591
  }
1336
- return JSON.stringify(value);
3592
+ return paragraphs.slice(1).join(`
3593
+
3594
+ `).trim();
1337
3595
  }
1338
- function isMissingFileError(error) {
1339
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
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");
1340
3601
  }
1341
- var LEVEL_ORDER;
1342
- var init_app_logger = __esm(() => {
1343
- LEVEL_ORDER = {
1344
- error: 0,
1345
- info: 1,
1346
- debug: 2
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
+ });
3615
+ }
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
  }
@@ -1461,6 +3760,8 @@ function normalizeCommand(command) {
1461
3760
  return "/session";
1462
3761
  if (command === "/ws")
1463
3762
  return "/workspace";
3763
+ if (command === "/pm")
3764
+ return "/permission";
1464
3765
  if (command === "/stop")
1465
3766
  return "/cancel";
1466
3767
  return command;
@@ -1468,6 +3769,21 @@ function normalizeCommand(command) {
1468
3769
  function isRecognizedCommand(command) {
1469
3770
  return RECOGNIZED_COMMANDS.has(command);
1470
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
+ }
1471
3787
  function tokenizeCommand(input) {
1472
3788
  const tokens = [];
1473
3789
  let current = "";
@@ -1508,6 +3824,7 @@ var init_parse_command = __esm(() => {
1508
3824
  "/sessions",
1509
3825
  "/status",
1510
3826
  "/cancel",
3827
+ "/permission",
1511
3828
  "/session",
1512
3829
  "/workspace",
1513
3830
  "/use",
@@ -1526,14 +3843,14 @@ class CommandRouter {
1526
3843
  config;
1527
3844
  configStore;
1528
3845
  logger;
1529
- constructor(sessions, transport, config, configStore, logger) {
3846
+ constructor(sessions, transport, config, configStore, logger2) {
1530
3847
  this.sessions = sessions;
1531
3848
  this.transport = transport;
1532
3849
  this.config = config;
1533
3850
  this.configStore = configStore;
1534
- this.logger = logger ?? createNoopAppLogger();
3851
+ this.logger = logger2 ?? createNoopAppLogger();
1535
3852
  }
1536
- async handle(chatKey, input) {
3853
+ async handle(chatKey, input, reply) {
1537
3854
  const startedAt = Date.now();
1538
3855
  const command = parseCommand(input);
1539
3856
  await this.logger.debug("command.parsed", "parsed inbound command", {
@@ -1582,6 +3899,30 @@ class CommandRouter {
1582
3899
  this.replaceConfig(updated);
1583
3900
  return { text: `Agent「${command.name}」已删除` };
1584
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
+ }
1585
3926
  case "workspaces":
1586
3927
  return { text: this.config ? renderWorkspaces(this.config) : "No config loaded." };
1587
3928
  case "workspace.new": {
@@ -1703,8 +4044,8 @@ class CommandRouter {
1703
4044
  return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
1704
4045
  }
1705
4046
  try {
1706
- const reply = await this.promptTransportSession(session, command.text);
1707
- return { text: reply.text };
4047
+ const result = await this.promptTransportSession(session, command.text, reply);
4048
+ return { text: result.text };
1708
4049
  } catch (error) {
1709
4050
  return this.renderTransportError(session, error);
1710
4051
  }
@@ -1779,6 +4120,12 @@ class CommandRouter {
1779
4120
  this.config.agents = { ...updated.agents };
1780
4121
  this.config.workspaces = { ...updated.workspaces };
1781
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
+ }
1782
4129
  renderTransportError(session, error) {
1783
4130
  const message = error instanceof Error ? error.message : String(error);
1784
4131
  if (message.includes("No acpx session found")) {
@@ -1897,8 +4244,8 @@ class CommandRouter {
1897
4244
  async checkTransportSession(session) {
1898
4245
  return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
1899
4246
  }
1900
- async promptTransportSession(session, text) {
1901
- 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));
1902
4249
  }
1903
4250
  async cancelTransportSession(session) {
1904
4251
  return await this.measureTransportCall("cancel", session, () => this.transport.cancel(session));
@@ -1916,28 +4263,40 @@ class CommandRouter {
1916
4263
  });
1917
4264
  return result;
1918
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
+ } : {};
1919
4277
  await this.logger.error(`transport.${operation}.failed`, "transport operation failed", {
1920
4278
  operation,
1921
4279
  agent: session.agent,
1922
4280
  workspace: session.workspace,
1923
4281
  alias: session.alias,
1924
4282
  durationMs: Date.now() - startedAt,
1925
- error: error instanceof Error ? error.message : String(error)
4283
+ error: error instanceof Error ? error.message : String(error),
4284
+ ...diagnosticContext
1926
4285
  });
1927
4286
  throw error;
1928
4287
  }
1929
4288
  }
1930
4289
  }
1931
- async function pathExists(path) {
4290
+ async function pathExists(path11) {
1932
4291
  try {
1933
- await access(path);
4292
+ await access(path11);
1934
4293
  return true;
1935
4294
  } catch {
1936
4295
  return false;
1937
4296
  }
1938
4297
  }
1939
- function normalizePathForWorkspace(path) {
1940
- const expanded = path.startsWith("~") ? homedir() + path.slice(1) : path;
4298
+ function normalizePathForWorkspace(path11) {
4299
+ const expanded = path11.startsWith("~") ? homedir() + path11.slice(1) : path11;
1941
4300
  return normalize(expanded);
1942
4301
  }
1943
4302
  function sameWorkspacePath(left, right) {
@@ -1951,12 +4310,66 @@ function sameWorkspacePath(left, right) {
1951
4310
  function summarizeTransportError(message) {
1952
4311
  return message.replace(/\s+/g, " ").trim().slice(0, 200);
1953
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
+ }
1954
4366
  function isPartialPromptOutputError(message) {
1955
4367
  return message.includes("未收到最终回复");
1956
4368
  }
1957
4369
  var init_command_router = __esm(() => {
1958
4370
  init_agent_templates();
1959
4371
  init_app_logger();
4372
+ init_prompt_output();
1960
4373
  init_parse_command();
1961
4374
  });
1962
4375
 
@@ -1980,8 +4393,8 @@ import { readFile as readFile3 } from "node:fs/promises";
1980
4393
  function isRecord(value) {
1981
4394
  return typeof value === "object" && value !== null;
1982
4395
  }
1983
- async function loadConfig(path, options = {}) {
1984
- const raw = JSON.parse(await readFile3(path, "utf8"));
4396
+ async function loadConfig(path11, options = {}) {
4397
+ const raw = JSON.parse(await readFile3(path11, "utf8"));
1985
4398
  return parseConfig(raw, options);
1986
4399
  }
1987
4400
  function parseConfig(raw, options = {}) {
@@ -1998,6 +4411,12 @@ function parseConfig(raw, options = {}) {
1998
4411
  if ("sessionInitTimeoutMs" in transport && (typeof transport.sessionInitTimeoutMs !== "number" || !Number.isFinite(transport.sessionInitTimeoutMs) || transport.sessionInitTimeoutMs <= 0)) {
1999
4412
  throw new Error("transport.sessionInitTimeoutMs must be a positive number");
2000
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
+ }
2001
4420
  if (!isRecord(raw.agents)) {
2002
4421
  throw new Error("agents must be an object");
2003
4422
  }
@@ -2032,8 +4451,9 @@ function parseConfig(raw, options = {}) {
2032
4451
  throw new Error(`workspace "${name}" allowed_agents must be an array of strings`);
2033
4452
  }
2034
4453
  }
4454
+ const rawAgents = raw.agents;
2035
4455
  const agents = {};
2036
- for (const [name, agent] of Object.entries(raw.agents)) {
4456
+ for (const [name, agent] of Object.entries(rawAgents)) {
2037
4457
  const driver = agent.driver;
2038
4458
  const command = typeof agent.command === "string" ? resolveAgentCommand(driver, agent.command) : undefined;
2039
4459
  agents[name] = {
@@ -2041,21 +4461,29 @@ function parseConfig(raw, options = {}) {
2041
4461
  ...command ? { command } : {}
2042
4462
  };
2043
4463
  }
4464
+ const rawWorkspaces = raw.workspaces;
2044
4465
  const workspaces = {};
2045
- for (const [name, workspace] of Object.entries(raw.workspaces)) {
4466
+ for (const [name, workspace] of Object.entries(rawWorkspaces)) {
2046
4467
  workspaces[name] = {
2047
4468
  cwd: workspace.cwd,
2048
4469
  ...typeof workspace.description === "string" ? { description: workspace.description } : {}
2049
4470
  };
2050
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;
2051
4477
  return {
2052
4478
  transport: {
2053
4479
  ...typeof transport.command === "string" ? { command: transport.command } : {},
2054
4480
  ...typeof transport.sessionInitTimeoutMs === "number" ? { sessionInitTimeoutMs: transport.sessionInitTimeoutMs } : {},
2055
- type: transport.type ?? "acpx-bridge"
4481
+ type: transportType,
4482
+ permissionMode,
4483
+ nonInteractivePermissions
2056
4484
  },
2057
4485
  logging: {
2058
- level: typeof logging?.level === "string" ? logging.level : options.defaultLoggingLevel ?? DEFAULT_LOGGING_CONFIG.level,
4486
+ level: resolvedLoggingLevel,
2059
4487
  maxSizeBytes: typeof logging?.maxSizeBytes === "number" ? logging.maxSizeBytes : DEFAULT_LOGGING_CONFIG.maxSizeBytes,
2060
4488
  maxFiles: typeof logging?.maxFiles === "number" ? logging.maxFiles : DEFAULT_LOGGING_CONFIG.maxFiles,
2061
4489
  retentionDays: typeof logging?.retentionDays === "number" ? logging.retentionDays : DEFAULT_LOGGING_CONFIG.retentionDays
@@ -2064,7 +4492,7 @@ function parseConfig(raw, options = {}) {
2064
4492
  workspaces
2065
4493
  };
2066
4494
  }
2067
- var DEFAULT_LOGGING_CONFIG;
4495
+ var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "fail";
2068
4496
  var init_load_config = __esm(() => {
2069
4497
  DEFAULT_LOGGING_CONFIG = {
2070
4498
  level: "info",
@@ -2080,8 +4508,8 @@ import { dirname as dirname5 } from "node:path";
2080
4508
 
2081
4509
  class ConfigStore {
2082
4510
  path;
2083
- constructor(path) {
2084
- this.path = path;
4511
+ constructor(path11) {
4512
+ this.path = path11;
2085
4513
  }
2086
4514
  async load() {
2087
4515
  return await loadConfig(this.path);
@@ -2119,6 +4547,15 @@ class ConfigStore {
2119
4547
  await this.save(config);
2120
4548
  return config;
2121
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
+ }
2122
4559
  }
2123
4560
  var init_config_store = __esm(() => {
2124
4561
  init_load_config();
@@ -2126,14 +4563,14 @@ var init_config_store = __esm(() => {
2126
4563
 
2127
4564
  // src/config/ensure-config.ts
2128
4565
  import { readFile as readFile4 } from "node:fs/promises";
2129
- async function ensureConfigExists(path) {
4566
+ async function ensureConfigExists(path11) {
2130
4567
  try {
2131
- await loadConfig(path);
4568
+ await loadConfig(path11);
2132
4569
  } catch (error) {
2133
4570
  if (!isMissingFileError2(error)) {
2134
4571
  throw error;
2135
4572
  }
2136
- const store = new ConfigStore(path);
4573
+ const store = new ConfigStore(path11);
2137
4574
  await store.save(await loadDefaultConfigTemplate());
2138
4575
  }
2139
4576
  }
@@ -2170,7 +4607,7 @@ function resolveAcpxCommand(options = {}) {
2170
4607
  }
2171
4608
  const platform = options.platform ?? process.platform;
2172
4609
  const resolvePackageJson = options.resolvePackageJson ?? ((id) => require2.resolve(id));
2173
- const readPackageJson = options.readPackageJson ?? ((path) => JSON.parse(readFileSync(path, "utf8")));
4610
+ const readPackageJson = options.readPackageJson ?? ((path11) => JSON.parse(readFileSync(path11, "utf8")));
2174
4611
  try {
2175
4612
  const packageJsonPath = resolvePackageJson("acpx/package.json");
2176
4613
  const pkg = readPackageJson(packageJsonPath);
@@ -2192,9 +4629,9 @@ var init_resolve_acpx_command = __esm(() => {
2192
4629
  class ConsoleAgent {
2193
4630
  router;
2194
4631
  logger;
2195
- constructor(router, logger) {
4632
+ constructor(router, logger2) {
2196
4633
  this.router = router;
2197
- this.logger = logger ?? createNoopAppLogger();
4634
+ this.logger = logger2 ?? createNoopAppLogger();
2198
4635
  }
2199
4636
  async chat(request) {
2200
4637
  if (!request.text.trim()) {
@@ -2205,7 +4642,7 @@ class ConsoleAgent {
2205
4642
  kind: request.text.trim().startsWith("/") ? "command" : "prompt",
2206
4643
  text: summarizeText(request.text)
2207
4644
  });
2208
- return await this.router.handle(request.conversationId, request.text);
4645
+ return await this.router.handle(request.conversationId, request.text, request.reply);
2209
4646
  }
2210
4647
  }
2211
4648
  function summarizeText(text) {
@@ -2332,8 +4769,8 @@ import { dirname as dirname6 } from "node:path";
2332
4769
 
2333
4770
  class StateStore {
2334
4771
  path;
2335
- constructor(path) {
2336
- this.path = path;
4772
+ constructor(path11) {
4773
+ this.path = path11;
2337
4774
  }
2338
4775
  async load() {
2339
4776
  try {
@@ -2377,6 +4814,10 @@ async function runConsole(paths, deps) {
2377
4814
  deps.daemonRuntime?.heartbeat().catch(() => {});
2378
4815
  }, deps.heartbeatIntervalMs ?? 30000);
2379
4816
  }
4817
+ if (!sdk.isLoggedIn()) {
4818
+ console.log("[weacpx] 未检测到登录凭证,正在启动扫码登录...");
4819
+ await sdk.login();
4820
+ }
2380
4821
  await sdk.start(runtime.agent);
2381
4822
  } finally {
2382
4823
  let disposeError = null;
@@ -2405,7 +4846,7 @@ function encodeBridgeRequest(request) {
2405
4846
 
2406
4847
  // src/transport/acpx-bridge/acpx-bridge-client.ts
2407
4848
  import { spawn as spawn2 } from "node:child_process";
2408
- import { fileURLToPath } from "node:url";
4849
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2409
4850
  import { createInterface } from "node:readline";
2410
4851
 
2411
4852
  class AcpxBridgeClient {
@@ -2419,7 +4860,10 @@ class AcpxBridgeClient {
2419
4860
  const id = String(this.nextId);
2420
4861
  this.nextId += 1;
2421
4862
  return awaitable((resolve, reject) => {
2422
- this.pending.set(id, { resolve, reject });
4863
+ this.pending.set(id, {
4864
+ resolve: (value) => resolve(value),
4865
+ reject
4866
+ });
2423
4867
  this.writeLine(encodeBridgeRequest({
2424
4868
  id,
2425
4869
  method,
@@ -2438,6 +4882,14 @@ class AcpxBridgeClient {
2438
4882
  pending.resolve(response.result);
2439
4883
  return;
2440
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
+ }
2441
4893
  pending.reject(new Error(response.error.message));
2442
4894
  }
2443
4895
  handleExit(error) {
@@ -2461,7 +4913,7 @@ function buildBridgeSpawnSpec(options) {
2461
4913
  };
2462
4914
  }
2463
4915
  async function spawnAcpxBridgeClient(options = {}) {
2464
- const bridgeEntryPath = options.bridgeEntryPath ?? fileURLToPath(new URL("../../bridge/bridge-main.ts", import.meta.url));
4916
+ const bridgeEntryPath = options.bridgeEntryPath ?? fileURLToPath2(new URL("../../bridge/bridge-main.ts", import.meta.url));
2465
4917
  const spawnSpec = buildBridgeSpawnSpec({
2466
4918
  execPath: process.execPath,
2467
4919
  bridgeEntryPath
@@ -2470,7 +4922,9 @@ async function spawnAcpxBridgeClient(options = {}) {
2470
4922
  cwd: options.cwd ?? process.cwd(),
2471
4923
  env: {
2472
4924
  ...process.env,
2473
- 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"
2474
4928
  },
2475
4929
  stdio: ["pipe", "pipe", "inherit"]
2476
4930
  });
@@ -2510,7 +4964,9 @@ function awaitable(executor) {
2510
4964
  executor(resolve, reject);
2511
4965
  });
2512
4966
  }
2513
- var init_acpx_bridge_client = () => {};
4967
+ var init_acpx_bridge_client = __esm(() => {
4968
+ init_prompt_output();
4969
+ });
2514
4970
 
2515
4971
  // src/transport/acpx-bridge/acpx-bridge-transport.ts
2516
4972
  class AcpxBridgeTransport {
@@ -2521,7 +4977,7 @@ class AcpxBridgeTransport {
2521
4977
  async ensureSession(session) {
2522
4978
  await this.client.request("ensureSession", this.toParams(session));
2523
4979
  }
2524
- async prompt(session, text) {
4980
+ async prompt(session, text, _reply) {
2525
4981
  return await this.client.request("prompt", {
2526
4982
  ...this.toParams(session),
2527
4983
  text
@@ -2565,121 +5021,62 @@ var init_spawn_command = __esm(() => {
2565
5021
  SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
2566
5022
  });
2567
5023
 
2568
- // src/transport/prompt-output.ts
2569
- function getPromptText(result) {
2570
- const stdoutOutput = extractPromptOutput(result.stdout);
2571
- if (result.code === 0) {
2572
- return sanitizePromptText(stdoutOutput.text);
2573
- }
2574
- const preferredError = extractPromptFailureMessage(result);
2575
- if (preferredError) {
2576
- throw new Error(preferredError);
2577
- }
2578
- const stderrOutput = extractPromptOutput(result.stderr);
2579
- const partialReply = [stdoutOutput, stderrOutput].filter((output) => output.hasAgentMessage && output.text.length > 0).map((output) => sanitizePromptText(output.text)).find((text) => text.length > 0);
2580
- if (partialReply) {
2581
- return partialReply;
2582
- }
2583
- throw new Error(`command failed with exit code ${result.code}`);
2584
- }
2585
- function normalizeCommandError(result) {
2586
- const preferredError = extractPromptFailureMessage(result);
2587
- if (preferredError) {
2588
- return preferredError;
2589
- }
2590
- return result.stdout.trim() || null;
2591
- }
2592
- function extractPromptFailureMessage(result) {
2593
- const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
2594
- const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
2595
- if (preferredMessage) {
2596
- return preferredMessage;
2597
- }
2598
- if (rpcMessages.length > 0) {
2599
- return rpcMessages[rpcMessages.length - 1] ?? null;
2600
- }
2601
- const stderrText = result.stderr.trim();
2602
- if (stderrText.length > 0) {
2603
- return stderrText;
2604
- }
2605
- return null;
2606
- }
2607
- function extractPromptOutput(output) {
2608
- const lines = output.split(`
2609
- `).map((line) => line.trim()).filter((line) => line.length > 0);
2610
- const messageSegments = [];
2611
- let currentSegment = "";
2612
- let hasAgentMessage = false;
2613
- for (const line of lines) {
2614
- try {
2615
- const event = JSON.parse(line);
2616
- const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
2617
- if (isMessageChunk) {
2618
- hasAgentMessage = true;
2619
- const chunk = event.params.update.content.text ?? "";
2620
- if (chunk.length > 0) {
2621
- currentSegment += chunk;
2622
- }
2623
- continue;
2624
- }
2625
- if (currentSegment.trim().length > 0) {
2626
- messageSegments.push(currentSegment.trim());
2627
- }
2628
- currentSegment = "";
2629
- } catch {
2630
- if (currentSegment.trim().length > 0) {
2631
- messageSegments.push(currentSegment.trim());
2632
- currentSegment = "";
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);
2633
5034
  }
5035
+ const remaining = this.buffer.trim();
5036
+ this.buffer = "";
5037
+ this.pendingLine = "";
5038
+ return remaining;
2634
5039
  }
2635
- }
2636
- if (currentSegment.trim().length > 0) {
2637
- messageSegments.push(currentSegment.trim());
2638
- }
2639
- if (messageSegments.length > 0) {
2640
- return {
2641
- text: messageSegments[messageSegments.length - 1],
2642
- hasAgentMessage
2643
- };
2644
- }
2645
- return {
2646
- text: output.trim(),
2647
- hasAgentMessage
2648
5040
  };
2649
5041
  }
2650
- function sanitizePromptText(text) {
2651
- const trimmed = text.trim();
2652
- const paragraphs = trimmed.split(/\n\s*\n/);
2653
- if (paragraphs.length < 2) {
2654
- return trimmed;
2655
- }
2656
- const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
2657
- if (!looksLikeWorkflowPreamble(firstParagraph)) {
2658
- return trimmed;
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);
2659
5050
  }
2660
- return paragraphs.slice(1).join(`
2661
-
2662
- `).trim();
2663
5051
  }
2664
- function looksLikeWorkflowPreamble(paragraph) {
2665
- if (!paragraph.startsWith("using ")) {
2666
- return false;
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;
2667
5061
  }
2668
- return paragraph.includes("using-superpowers") || paragraph.includes("repo workflow requirement") || paragraph.includes("workflow requirement") || paragraph.includes("before responding") || paragraph.includes("skill check");
2669
- }
2670
- function extractJsonRpcErrorMessages(output) {
2671
- return output.split(`
2672
- `).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
2673
- try {
2674
- const payload = JSON.parse(line);
2675
- if (typeof payload.error?.message === "string" && payload.error.message.length > 0) {
2676
- return [payload.error.message];
2677
- }
2678
- } catch {
2679
- return [];
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);
2680
5078
  }
2681
- return [];
2682
- });
5079
+ }
2683
5080
  }
2684
5081
 
2685
5082
  // src/transport/acpx-cli/node-pty-helper.ts
@@ -2769,11 +5166,15 @@ async function defaultPtyRunner(command, args, options) {
2769
5166
  class AcpxCliTransport {
2770
5167
  command;
2771
5168
  sessionInitTimeoutMs;
5169
+ permissionMode;
5170
+ nonInteractivePermissions;
2772
5171
  runCommand;
2773
5172
  runPtyCommand;
2774
5173
  constructor(options, runCommand = defaultRunner, runPtyCommand = defaultPtyRunner) {
2775
5174
  this.command = options.command ?? "acpx";
2776
5175
  this.sessionInitTimeoutMs = options.sessionInitTimeoutMs ?? 120000;
5176
+ this.permissionMode = options.permissionMode ?? "approve-all";
5177
+ this.nonInteractivePermissions = options.nonInteractivePermissions ?? "fail";
2777
5178
  this.runCommand = runCommand;
2778
5179
  this.runPtyCommand = runPtyCommand;
2779
5180
  }
@@ -2789,8 +5190,13 @@ class AcpxCliTransport {
2789
5190
  timeoutMs: this.sessionInitTimeoutMs
2790
5191
  });
2791
5192
  }
2792
- async prompt(session, text) {
2793
- const result = await this.runCommand(this.command, this.buildPromptArgs(session, text));
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);
2794
5200
  return { text: getPromptText(result) };
2795
5201
  }
2796
5202
  async cancel(session) {
@@ -2849,26 +5255,93 @@ class AcpxCliTransport {
2849
5255
  })
2850
5256
  ]);
2851
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
+ }
2852
5305
  buildArgs(session, tail) {
2853
- const prefix = ["--format", "quiet", "--cwd", session.cwd];
5306
+ const prefix = [
5307
+ "--format",
5308
+ "quiet",
5309
+ "--cwd",
5310
+ session.cwd,
5311
+ ...this.buildPermissionArgs()
5312
+ ];
2854
5313
  if (session.agentCommand) {
2855
5314
  return [...prefix, "--agent", session.agentCommand, ...tail];
2856
5315
  }
2857
5316
  return [...prefix, session.agent, ...tail];
2858
5317
  }
2859
5318
  buildPromptArgs(session, text) {
2860
- const prefix = ["--format", "json", "--json-strict", "--cwd", session.cwd];
5319
+ const prefix = [
5320
+ "--format",
5321
+ "json",
5322
+ "--json-strict",
5323
+ "--cwd",
5324
+ session.cwd,
5325
+ ...this.buildPermissionArgs()
5326
+ ];
2861
5327
  const tail = ["prompt", "-s", session.transportSession, text];
2862
5328
  if (session.agentCommand) {
2863
5329
  return [...prefix, "--agent", session.agentCommand, ...tail];
2864
5330
  }
2865
5331
  return [...prefix, session.agent, ...tail];
2866
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
+ }
2867
5337
  }
2868
5338
  function renderCommandForError(args) {
2869
5339
  const rendered = [];
2870
5340
  for (let index = 0;index < args.length; index += 1) {
2871
5341
  const arg = args[index];
5342
+ if (arg === undefined) {
5343
+ continue;
5344
+ }
2872
5345
  if (arg === "--format") {
2873
5346
  index += 1;
2874
5347
  continue;
@@ -2884,6 +5357,7 @@ function renderCommandForError(args) {
2884
5357
  var require3;
2885
5358
  var init_acpx_cli_transport = __esm(() => {
2886
5359
  init_spawn_command();
5360
+ init_prompt_output();
2887
5361
  init_node_pty_helper();
2888
5362
  require3 = createRequire3(import.meta.url);
2889
5363
  });
@@ -2897,14 +5371,14 @@ __export(exports_main, {
2897
5371
  });
2898
5372
  import { homedir as homedir2 } from "node:os";
2899
5373
  import { dirname as dirname8, join as join4 } from "node:path";
2900
- import { fileURLToPath as fileURLToPath2 } from "node:url";
5374
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2901
5375
  async function buildApp(paths, deps = {}) {
2902
5376
  await ensureConfigExists(paths.configPath);
2903
5377
  const configStore = new ConfigStore(paths.configPath);
2904
5378
  const config = await loadConfig(paths.configPath, {
2905
5379
  defaultLoggingLevel: deps.defaultLoggingLevel
2906
5380
  });
2907
- const logger = createAppLogger({
5381
+ const logger2 = createAppLogger({
2908
5382
  filePath: resolveAppLogPath(paths.configPath),
2909
5383
  level: config.logging.level,
2910
5384
  maxSizeBytes: config.logging.maxSizeBytes,
@@ -2912,24 +5386,26 @@ async function buildApp(paths, deps = {}) {
2912
5386
  retentionDays: config.logging.retentionDays,
2913
5387
  now: deps.loggerNow
2914
5388
  });
2915
- await logger.cleanup();
5389
+ await logger2.cleanup();
2916
5390
  const acpxCommand = resolveAcpxCommand({ configuredCommand: config.transport.command });
2917
5391
  const stateStore = new StateStore(paths.statePath);
2918
5392
  const state = await stateStore.load();
2919
5393
  const sessions = new SessionService(config, stateStore, state);
2920
5394
  const transport = config.transport.type === "acpx-bridge" ? await (deps.createBridgeTransport?.() ?? Promise.resolve(new AcpxBridgeTransport(await spawnAcpxBridgeClient({
2921
5395
  acpxCommand,
2922
- bridgeEntryPath: resolveBridgeEntryPath()
5396
+ bridgeEntryPath: resolveBridgeEntryPath(),
5397
+ permissionMode: config.transport.permissionMode,
5398
+ nonInteractivePermissions: config.transport.nonInteractivePermissions
2923
5399
  })))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config.transport, command: acpxCommand });
2924
- const router = new CommandRouter(sessions, transport, config, configStore, logger);
2925
- const agent = new ConsoleAgent(router, logger);
5400
+ const router = new CommandRouter(sessions, transport, config, configStore, logger2);
5401
+ const agent = new ConsoleAgent(router, logger2);
2926
5402
  return {
2927
5403
  agent,
2928
5404
  router,
2929
5405
  sessions,
2930
5406
  stateStore,
2931
5407
  configStore,
2932
- logger,
5408
+ logger: logger2,
2933
5409
  dispose: async () => {
2934
5410
  if ("dispose" in transport && typeof transport.dispose === "function") {
2935
5411
  await transport.dispose();
@@ -2967,9 +5443,9 @@ function resolveRuntimePaths() {
2967
5443
  }
2968
5444
  function resolveBridgeEntryPath() {
2969
5445
  if (import.meta.url.includes("/dist/")) {
2970
- return fileURLToPath2(new URL("./bridge/bridge-main.js", import.meta.url));
5446
+ return fileURLToPath3(new URL("./bridge/bridge-main.js", import.meta.url));
2971
5447
  }
2972
- return fileURLToPath2(new URL("./bridge/bridge-main.ts", import.meta.url));
5448
+ return fileURLToPath3(new URL("./bridge/bridge-main.ts", import.meta.url));
2973
5449
  }
2974
5450
  function resolveAppLogPath(configPath) {
2975
5451
  const rootDir = dirname8(configPath);
@@ -2988,13 +5464,14 @@ var init_main = __esm(async () => {
2988
5464
  init_state_store();
2989
5465
  init_acpx_bridge_client();
2990
5466
  init_acpx_cli_transport();
5467
+ init_weixin_sdk();
2991
5468
  if (false) {}
2992
5469
  });
2993
5470
 
2994
5471
  // src/cli.ts
2995
5472
  import { homedir as homedir3 } from "node:os";
2996
5473
  import { sep } from "node:path";
2997
- import { fileURLToPath as fileURLToPath3 } from "node:url";
5474
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
2998
5475
 
2999
5476
  // src/daemon/create-daemon-controller.ts
3000
5477
  import { mkdir as mkdir3, open } from "node:fs/promises";
@@ -3366,7 +5843,7 @@ class DaemonRuntime {
3366
5843
  }
3367
5844
 
3368
5845
  // src/cli.ts
3369
- 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"];
3370
5847
  async function runCli(args, deps = {}) {
3371
5848
  const command = args[0];
3372
5849
  const print = deps.print ?? ((line) => console.log(line));
@@ -3375,6 +5852,9 @@ async function runCli(args, deps = {}) {
3375
5852
  case "login":
3376
5853
  await (deps.login ?? defaultLogin)();
3377
5854
  return 0;
5855
+ case "logout":
5856
+ await (deps.logout ?? defaultLogout)();
5857
+ return 0;
3378
5858
  case "run":
3379
5859
  await (deps.run ?? defaultRun)();
3380
5860
  return 0;
@@ -3426,10 +5906,14 @@ async function defaultLogin() {
3426
5906
  const { main: main3 } = await init_login().then(() => exports_login);
3427
5907
  await main3();
3428
5908
  }
5909
+ async function defaultLogout() {
5910
+ const { logout: logout2 } = await Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk));
5911
+ logout2();
5912
+ }
3429
5913
  async function defaultRun() {
3430
5914
  const [{ buildApp: buildApp2, resolveRuntimePaths: resolveRuntimePaths2 }, { loadWeixinSdk: loadWeixinSdk2 }, { runConsole: runConsole2 }] = await Promise.all([
3431
5915
  init_main().then(() => exports_main),
3432
- Promise.resolve().then(() => exports_weixin_sdk),
5916
+ Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk)),
3433
5917
  Promise.resolve().then(() => exports_run_console)
3434
5918
  ]);
3435
5919
  const runtimePaths = resolveRuntimePaths2();
@@ -3463,7 +5947,7 @@ function resolveCliEntryPath() {
3463
5947
  if (process.argv[1]) {
3464
5948
  return process.argv[1];
3465
5949
  }
3466
- return fileURLToPath3(import.meta.url);
5950
+ return fileURLToPath4(import.meta.url);
3467
5951
  }
3468
5952
  if (__require.main == __require.module) {
3469
5953
  process.exitCode = await runCli(process.argv.slice(2));