omp-wechat 1.6.0 → 1.8.0

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.
Files changed (2) hide show
  1. package/dist/index.js +95 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1188,6 +1188,11 @@ var logger = {
1188
1188
  var STATE_DIR = join3(homedir2(), ".omp-wechat");
1189
1189
  var CREDENTIALS_FILE = join3(STATE_DIR, "credentials.json");
1190
1190
  var SYNC_BUF_FILE = join3(STATE_DIR, "sync_buf.txt");
1191
+ var CHANNEL_VERSION = "2.2.0";
1192
+ var BOT_AGENT = "OMP-Wechat/1.7.0";
1193
+ function baseInfo() {
1194
+ return { channel_version: CHANNEL_VERSION, bot_agent: BOT_AGENT };
1195
+ }
1191
1196
  function loadCredentials() {
1192
1197
  try {
1193
1198
  return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf8"));
@@ -1241,7 +1246,15 @@ async function apiFetch(creds, endpoint, body, timeoutMs = 15000) {
1241
1246
  const text = await res.text();
1242
1247
  if (!res.ok)
1243
1248
  throw new Error(`${endpoint} ${res.status}: ${text}`);
1244
- return JSON.parse(text);
1249
+ const data = JSON.parse(text);
1250
+ const ret = typeof data.ret === "number" ? data.ret : undefined;
1251
+ const errcode = typeof data.errcode === "number" ? data.errcode : undefined;
1252
+ if (ret !== undefined && ret !== 0 || errcode !== undefined && errcode !== 0) {
1253
+ const code = errcode ?? ret ?? 0;
1254
+ const errmsg = typeof data.errmsg === "string" ? data.errmsg : "";
1255
+ throw new Error(`${endpoint} error ret=${ret} errcode=${errcode}${errmsg ? `: ${errmsg}` : ""} (code ${code})`);
1256
+ }
1257
+ return data;
1245
1258
  } catch (err) {
1246
1259
  clearTimeout(timer);
1247
1260
  throw err;
@@ -1251,7 +1264,7 @@ async function getUpdates(creds, buf) {
1251
1264
  try {
1252
1265
  const resp = await apiFetch(creds, "ilink/bot/getupdates", {
1253
1266
  get_updates_buf: buf,
1254
- base_info: { channel_version: "0.1.0" }
1267
+ base_info: baseInfo()
1255
1268
  }, 35000);
1256
1269
  return resp;
1257
1270
  } catch (err) {
@@ -1272,7 +1285,7 @@ async function sendMessage(creds, to, text, contextToken, clientId) {
1272
1285
  item_list: [{ type: 1, text_item: { text } }],
1273
1286
  context_token: contextToken
1274
1287
  },
1275
- base_info: { channel_version: "0.1.0" }
1288
+ base_info: baseInfo()
1276
1289
  }, 15000);
1277
1290
  }
1278
1291
  var typingTicketCache = new Map;
@@ -1284,7 +1297,7 @@ async function ensureTypingTicket(creds, userId) {
1284
1297
  try {
1285
1298
  const resp = await apiFetch(creds, "ilink/bot/getconfig", {
1286
1299
  ilink_user_id: userId,
1287
- base_info: { channel_version: "2.0.1" }
1300
+ base_info: baseInfo()
1288
1301
  }, 15000);
1289
1302
  const data = resp;
1290
1303
  if (data.typing_ticket) {
@@ -1306,10 +1319,9 @@ async function sendTyping(creds, userId, command) {
1306
1319
  try {
1307
1320
  await apiFetch(creds, "ilink/bot/sendtyping", {
1308
1321
  ilink_user_id: userId,
1309
- to_user_id: userId,
1310
1322
  typing_ticket: ticket,
1311
- command,
1312
- base_info: { channel_version: "2.0.1" }
1323
+ status: command,
1324
+ base_info: baseInfo()
1313
1325
  }, 1e4);
1314
1326
  } catch (err) {
1315
1327
  logger.debug(`sendTyping failed for ${userId}: ${err}`);
@@ -1873,7 +1885,7 @@ async function uploadAndSendFile(creds, to, contextToken, filePath, maxSizeBytes
1873
1885
  filesize: ciphertext.length,
1874
1886
  no_need_thumb: true,
1875
1887
  aeskey: aesKey.toString("hex"),
1876
- base_info: { channel_version: "0.1.0" }
1888
+ base_info: baseInfo()
1877
1889
  }, 15000);
1878
1890
  const uploadFullUrl = uploadParams.upload_full_url?.trim();
1879
1891
  if (!uploadFullUrl && !uploadParams.upload_param) {
@@ -1920,7 +1932,7 @@ async function uploadAndSendFile(creds, to, contextToken, filePath, maxSizeBytes
1920
1932
  }
1921
1933
  const media = {
1922
1934
  encrypt_query_param: encryptQueryParam,
1923
- aes_key: aesKey.toString("base64"),
1935
+ aes_key: Buffer.from(aesKey.toString("hex"), "utf8").toString("base64"),
1924
1936
  encrypt_type: 1
1925
1937
  };
1926
1938
  const item = mediaType === 1 /* IMAGE */ ? {
@@ -1931,7 +1943,6 @@ async function uploadAndSendFile(creds, to, contextToken, filePath, maxSizeBytes
1931
1943
  file_item: {
1932
1944
  media,
1933
1945
  file_name: fileName,
1934
- md5: rawMd5,
1935
1946
  len: String(data.length)
1936
1947
  }
1937
1948
  };
@@ -1948,7 +1959,7 @@ async function uploadAndSendFile(creds, to, contextToken, filePath, maxSizeBytes
1948
1959
  item_list: [item],
1949
1960
  context_token: contextToken
1950
1961
  },
1951
- base_info: { channel_version: "0.1.0" }
1962
+ base_info: baseInfo()
1952
1963
  }, 15000);
1953
1964
  logger.info(`Sent ${mediaType === 1 /* IMAGE */ ? "image" : "file"} to ${to}: ${fileName} (${data.length} bytes)`);
1954
1965
  return { status: "sent", mediaType, bytes: data.length };
@@ -2006,13 +2017,20 @@ import { homedir as homedir6 } from "os";
2006
2017
  import { existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync3, rmSync } from "fs";
2007
2018
  var STATE_DIR4 = join7(homedir6(), ".omp-wechat");
2008
2019
  var SESSIONS_DIR = join7(STATE_DIR4, "sessions");
2020
+ var DEFAULT_OUTBOX_BASE = join7(STATE_DIR4, "outbox");
2009
2021
  var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1000;
2010
2022
  function sanitizeChatId(chatId) {
2011
2023
  return chatId.replace(/[^a-zA-Z0-9_@.-]/g, "_").slice(0, 200);
2012
2024
  }
2025
+ function sanitizeOutboxChatId(chatId) {
2026
+ return chatId.replace(/[^a-zA-Z0-9_-]/g, "_");
2027
+ }
2013
2028
  function sessionDirFor(chatId) {
2014
2029
  return join7(SESSIONS_DIR, sanitizeChatId(chatId));
2015
2030
  }
2031
+ function outboxDirFor(chatId, outboxBase) {
2032
+ return join7(outboxBase ?? DEFAULT_OUTBOX_BASE, sanitizeOutboxChatId(chatId));
2033
+ }
2016
2034
  function ensureSessionsDir() {
2017
2035
  mkdirSync5(SESSIONS_DIR, { recursive: true, mode: 448 });
2018
2036
  }
@@ -2028,6 +2046,54 @@ function removeSessionDir(chatId) {
2028
2046
  return false;
2029
2047
  }
2030
2048
  }
2049
+ function removeOutboxDir(chatId, outboxBase) {
2050
+ const dir = outboxDirFor(chatId, outboxBase);
2051
+ if (!existsSync2(dir))
2052
+ return false;
2053
+ try {
2054
+ rmSync(dir, { recursive: true, force: true });
2055
+ logger.info(`Removed outbox dir: ${dir}`);
2056
+ return true;
2057
+ } catch (err) {
2058
+ logger.warn(`Failed to remove outbox dir ${dir}: ${err}`);
2059
+ return false;
2060
+ }
2061
+ }
2062
+ function cleanupStaleOutboxes(outboxBase) {
2063
+ const base = outboxBase ?? DEFAULT_OUTBOX_BASE;
2064
+ if (!existsSync2(base))
2065
+ return 0;
2066
+ const now = Date.now();
2067
+ let removed = 0;
2068
+ for (const entry of readdirSync2(base, { withFileTypes: true })) {
2069
+ if (!entry.isDirectory())
2070
+ continue;
2071
+ const dir = join7(base, entry.name);
2072
+ let newestMtime = 0;
2073
+ try {
2074
+ for (const file of readdirSync2(dir)) {
2075
+ const mtime = statSync3(join7(dir, file)).mtimeMs;
2076
+ if (mtime > newestMtime)
2077
+ newestMtime = mtime;
2078
+ }
2079
+ } catch {
2080
+ continue;
2081
+ }
2082
+ if (newestMtime === 0 || now - newestMtime > STALE_THRESHOLD_MS) {
2083
+ try {
2084
+ rmSync(dir, { recursive: true, force: true });
2085
+ removed++;
2086
+ logger.info(`Cleaned up stale outbox dir: ${entry.name}`);
2087
+ } catch (err) {
2088
+ logger.warn(`Failed to cleanup outbox dir ${entry.name}: ${err}`);
2089
+ }
2090
+ }
2091
+ }
2092
+ if (removed > 0) {
2093
+ logger.info(`Outbox cleanup: removed ${removed} stale outbox(es)`);
2094
+ }
2095
+ return removed;
2096
+ }
2031
2097
  function cleanupStaleSessions() {
2032
2098
  if (!existsSync2(SESSIONS_DIR))
2033
2099
  return 0;
@@ -2081,15 +2147,7 @@ function clearAllSessions() {
2081
2147
 
2082
2148
  // src/engine/session.ts
2083
2149
  import { mkdirSync as mkdirSync6, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
2084
- import { homedir as homedir7 } from "os";
2085
2150
  import { join as join8 } from "path";
2086
- var DEFAULT_OUTBOX_BASE = join8(homedir7(), ".omp-wechat", "outbox");
2087
- function sanitizeChatId2(chatId) {
2088
- return chatId.replace(/[^a-zA-Z0-9_-]/g, "_");
2089
- }
2090
- function outboxDirFor(chatId, config) {
2091
- return join8(config.outboxDir ?? DEFAULT_OUTBOX_BASE, sanitizeChatId2(chatId));
2092
- }
2093
2151
  function outboxInstructions(outboxDir) {
2094
2152
  return `## File delivery to WeChat
2095
2153
 
@@ -2137,7 +2195,7 @@ class ChatSession {
2137
2195
  }
2138
2196
  static async create(chatId, contextToken, config, onReply, onFiles) {
2139
2197
  logger.info(`Creating session: ${chatId}`);
2140
- const outboxDir = outboxDirFor(chatId, config);
2198
+ const outboxDir = outboxDirFor(chatId, config.outboxDir);
2141
2199
  const sendFilesEnabled = config.sendFiles !== false;
2142
2200
  if (sendFilesEnabled) {
2143
2201
  try {
@@ -2270,13 +2328,14 @@ class SessionPool {
2270
2328
  get(chatId) {
2271
2329
  return this.pool.get(chatId);
2272
2330
  }
2273
- async resetSession(chatId) {
2331
+ async resetSession(chatId, config) {
2274
2332
  const entry = this.pool.get(chatId);
2275
2333
  if (entry) {
2276
2334
  await entry.dispose();
2277
2335
  this.pool.delete(chatId);
2278
2336
  }
2279
2337
  removeSessionDir(chatId);
2338
+ removeOutboxDir(chatId, config.outboxDir);
2280
2339
  }
2281
2340
  getContextToken(chatId) {
2282
2341
  return this.pool.get(chatId)?.getContextToken() ?? "";
@@ -2479,7 +2538,7 @@ class ModelCommand {
2479
2538
  // src/command/new-session-command.ts
2480
2539
  class NewSessionInvocation {
2481
2540
  async execute(ctx) {
2482
- await ctx.pool.resetSession(ctx.chatId);
2541
+ await ctx.pool.resetSession(ctx.chatId, ctx.config);
2483
2542
  return "Session reset. Your next message starts a fresh conversation.";
2484
2543
  }
2485
2544
  }
@@ -2583,7 +2642,11 @@ class WeChatBridge {
2583
2642
  this.releaseLock();
2584
2643
  });
2585
2644
  cleanupStaleSessions();
2586
- this.cleanupTimer = setInterval(() => cleanupStaleSessions(), 6 * 60 * 60 * 1000);
2645
+ cleanupStaleOutboxes(config.outboxDir);
2646
+ this.cleanupTimer = setInterval(() => {
2647
+ cleanupStaleSessions();
2648
+ cleanupStaleOutboxes(config.outboxDir);
2649
+ }, 6 * 60 * 60 * 1000);
2587
2650
  return this.state;
2588
2651
  }
2589
2652
  async stop() {
@@ -2835,7 +2898,7 @@ ${truncated}
2835
2898
  }
2836
2899
 
2837
2900
  // src/service.ts
2838
- import { platform, homedir as homedir8 } from "os";
2901
+ import { platform, homedir as homedir7 } from "os";
2839
2902
  import { join as join9, basename as basename4 } from "path";
2840
2903
  import { existsSync as existsSync3, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4, rmSync as rmSync3 } from "fs";
2841
2904
  var PLIST_LABEL = "com.omp-wechat";
@@ -2851,14 +2914,14 @@ function detectPlatform() {
2851
2914
  return "other";
2852
2915
  }
2853
2916
  function getLogDir() {
2854
- return join9(homedir8(), ".omp", "logs");
2917
+ return join9(homedir7(), ".omp", "logs");
2855
2918
  }
2856
2919
  function resolveHostBinary() {
2857
2920
  const exe = process.execPath || "omp";
2858
2921
  return basename4(exe);
2859
2922
  }
2860
2923
  function plistPath() {
2861
- return join9(homedir8(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
2924
+ return join9(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
2862
2925
  }
2863
2926
  function generatePlist() {
2864
2927
  const omp = resolveHostBinary();
@@ -2894,14 +2957,14 @@ function generatePlist() {
2894
2957
  <key>PATH</key>
2895
2958
  <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
2896
2959
  <key>HOME</key>
2897
- <string>${homedir8()}</string>
2960
+ <string>${homedir7()}</string>
2898
2961
  </dict>
2899
2962
  </dict>
2900
2963
  </plist>
2901
2964
  `;
2902
2965
  }
2903
2966
  function installLaunchd() {
2904
- const dir = join9(homedir8(), "Library", "LaunchAgents");
2967
+ const dir = join9(homedir7(), "Library", "LaunchAgents");
2905
2968
  mkdirSync7(dir, { recursive: true });
2906
2969
  mkdirSync7(getLogDir(), { recursive: true });
2907
2970
  const plist = plistPath();
@@ -2943,7 +3006,7 @@ ExecStart=/bin/sh -c 'while true; do echo "{\\"id\\":\\"ka\\",\\"type\\":\\"get_
2943
3006
  Restart=always
2944
3007
  RestartSec=10
2945
3008
 
2946
- Environment=HOME=${homedir8()}
3009
+ Environment=HOME=${homedir7()}
2947
3010
  Environment=PATH=/usr/local/bin:/usr/bin:/bin
2948
3011
 
2949
3012
  StandardOutput=null
@@ -2952,7 +3015,7 @@ StandardError=append:${logDir}/rpc.log
2952
3015
  NoNewPrivileges=true
2953
3016
  ProtectSystem=strict
2954
3017
  ProtectHome=read-only
2955
- ReadWritePaths=${logDir} ${join9(homedir8(), ".omp-wechat")} ${join9(homedir8(), ".omp")}
3018
+ ReadWritePaths=${logDir} ${join9(homedir7(), ".omp-wechat")} ${join9(homedir7(), ".omp")}
2956
3019
  PrivateTmp=true
2957
3020
 
2958
3021
  [Install]
@@ -2993,7 +3056,7 @@ function uninstallSystemd() {
2993
3056
  }
2994
3057
  var WIN_TASK_NAME = "OMP-Wechat";
2995
3058
  function winScriptPath() {
2996
- return join9(homedir8(), ".omp-wechat", "omp-wechat-rpc.ps1");
3059
+ return join9(homedir7(), ".omp-wechat", "omp-wechat-rpc.ps1");
2997
3060
  }
2998
3061
  function generateWinScript() {
2999
3062
  const omp = process.execPath || "omp";
@@ -3036,7 +3099,7 @@ while ($true) {
3036
3099
  `;
3037
3100
  }
3038
3101
  function installWinTask() {
3039
- mkdirSync7(join9(homedir8(), ".omp-wechat"), { recursive: true });
3102
+ mkdirSync7(join9(homedir7(), ".omp-wechat"), { recursive: true });
3040
3103
  mkdirSync7(getLogDir(), { recursive: true });
3041
3104
  writeFileSync4(winScriptPath(), generateWinScript());
3042
3105
  Bun.spawnSync(["schtasks", "/end", "/tn", WIN_TASK_NAME], { stderr: "ignore" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-wechat",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "OMP/Pi extension: bridge WeChat messages to OMP's AI engine via the iLink Bot API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",