liangzimixin 0.3.73 → 0.3.74

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/index.cjs CHANGED
@@ -18972,7 +18972,9 @@ async function resolveMedia(params) {
18972
18972
  sdkRuntime,
18973
18973
  maxBytes = 30 * 1024 * 1024,
18974
18974
  allowPrivateNetwork = false,
18975
- timeoutMs
18975
+ timeoutMs,
18976
+ messagePipe,
18977
+ fileEncryptionMeta
18976
18978
  } = params;
18977
18979
  const client = createHttpClient({ baseUrl: serverUrl, tokenManager, timeoutMs });
18978
18980
  log11.info("download:getUrl", { fileId });
@@ -18989,9 +18991,15 @@ async function resolveMedia(params) {
18989
18991
  url: downloadInfo.fileUrl,
18990
18992
  ssrfPolicy: { allowPrivateNetwork }
18991
18993
  });
18994
+ let fileBuffer = Buffer.from(fetched.buffer);
18995
+ if (fileEncryptionMeta && messagePipe) {
18996
+ log11.info("download:decrypting", { fileId, keyId: fileEncryptionMeta.keyId, encryptedSize: fileBuffer.length });
18997
+ fileBuffer = Buffer.from(await messagePipe.decryptFile(fileBuffer, fileEncryptionMeta.keyId, fileEncryptionMeta.iv));
18998
+ log11.info("download:decrypted", { fileId, encryptedSize: fetched.buffer.length, decryptedSize: fileBuffer.length });
18999
+ }
18992
19000
  const contentType = fetched.contentType || downloadInfo.mimeType;
18993
19001
  const saved = await sdkRuntime.channel.media.saveMediaBuffer(
18994
- fetched.buffer,
19002
+ fileBuffer,
18995
19003
  contentType,
18996
19004
  "inbound",
18997
19005
  maxBytes,
@@ -19035,7 +19043,9 @@ async function resolveContent(context, deps) {
19035
19043
  sdkRuntime: deps.sdkRuntime,
19036
19044
  maxBytes: deps.maxBytes,
19037
19045
  allowPrivateNetwork: deps.allowPrivateNetwork,
19038
- timeoutMs: deps.timeoutMs
19046
+ timeoutMs: deps.timeoutMs,
19047
+ messagePipe: deps.messagePipe,
19048
+ fileEncryptionMeta: deps.fileEncryptionMeta
19039
19049
  });
19040
19050
  log12.info("resolveContent:downloaded", {
19041
19051
  fileId: context.fileId,
@@ -19394,7 +19404,9 @@ var InboundPipeline = class {
19394
19404
  sdkRuntime: core,
19395
19405
  maxBytes: this.deps.pluginConfig.file.maxFileSizeMb * 1024 * 1024,
19396
19406
  allowPrivateNetwork: this.deps.pluginConfig.file.allowPrivateNetwork,
19397
- timeoutMs: this.deps.pluginConfig.file.fetchTimeoutMs
19407
+ timeoutMs: this.deps.pluginConfig.file.fetchTimeoutMs,
19408
+ messagePipe: this.deps.messagePipe,
19409
+ fileEncryptionMeta: msg.fileEncryptionMeta
19398
19410
  });
19399
19411
  const payload = buildInboundPayload(msg, resolvedContent, this.deps.pluginConfig);
19400
19412
  const { sdkConfig } = this.deps;
@@ -21072,6 +21084,48 @@ var MessagePipe = class _MessagePipe {
21072
21084
  });
21073
21085
  return { encryptedBuffer, keyId, iv };
21074
21086
  }
21087
+ /**
21088
+ * 解密文件 Buffer — 按分片解密,返回解密后的 Buffer。
21089
+ * 用于入站加密文件消息的下载后解密。
21090
+ *
21091
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
21092
+ *
21093
+ * @param buffer - 加密的文件 Buffer
21094
+ * @param keyId - 加密时的密钥标识 (来自 extraContent.sessionId)
21095
+ * @param iv - 初始化向量 (来自 extraContent.cryptoIv)
21096
+ * @returns 解密后的 Buffer
21097
+ */
21098
+ async decryptFile(buffer, keyId, iv) {
21099
+ const chunkSize = 10 * 1024 * 1024 + 16;
21100
+ const totalChunks = Math.ceil(buffer.length / chunkSize);
21101
+ let sessionKey = "";
21102
+ let fillKey = "";
21103
+ const decryptedChunks = [];
21104
+ for (let i = 0; i < totalChunks; i++) {
21105
+ const start = i * chunkSize;
21106
+ const end = Math.min(start + chunkSize, buffer.length);
21107
+ const chunk = buffer.subarray(start, end);
21108
+ const options = sessionKey ? { sessionKey, fillKey } : void 0;
21109
+ const result = await this.crypto.decryptFileChunk(chunk, keyId, iv, options);
21110
+ decryptedChunks.push(result.fileBuffer);
21111
+ sessionKey = result.sessionKey;
21112
+ fillKey = result.fillKey;
21113
+ log25.debug("\u{1F513} \u6587\u4EF6\u5206\u7247\u89E3\u5BC6", {
21114
+ chunk: `${i + 1}/${totalChunks}`,
21115
+ encryptedSize: chunk.length,
21116
+ decryptedSize: result.fileBuffer.length,
21117
+ keyId
21118
+ });
21119
+ }
21120
+ const decryptedBuffer = Buffer.concat(decryptedChunks);
21121
+ log25.info("\u{1F513} \u6587\u4EF6\u89E3\u5BC6\u5B8C\u6210", {
21122
+ encryptedSize: buffer.length,
21123
+ decryptedSize: decryptedBuffer.length,
21124
+ totalChunks,
21125
+ keyId
21126
+ });
21127
+ return decryptedBuffer;
21128
+ }
21075
21129
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
21076
21130
  static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
21077
21131
  /**
@@ -21384,10 +21438,17 @@ var MessagePipe = class _MessagePipe {
21384
21438
  });
21385
21439
  const extraContent = callbackData.extraContent;
21386
21440
  let isEncrypted = false;
21441
+ let fileEncryptionMeta;
21387
21442
  if (typeof extraContent === "string" && extraContent.length > 0) {
21388
21443
  try {
21389
21444
  const parsed = JSON.parse(extraContent);
21390
- isEncrypted = Boolean(parsed.encryptMsg);
21445
+ if (parsed.encryptMsg) {
21446
+ isEncrypted = true;
21447
+ } else if (parsed.sessionId) {
21448
+ isEncrypted = true;
21449
+ fileEncryptionMeta = { keyId: parsed.sessionId, iv: parsed.cryptoIv ?? "" };
21450
+ log25.info("\u{1F512} \u5165\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6", { msgUid: callbackData.msgUid, keyId: parsed.sessionId });
21451
+ }
21391
21452
  } catch {
21392
21453
  log25.debug("\u2139\uFE0F extraContent \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
21393
21454
  }
@@ -21468,7 +21529,8 @@ var MessagePipe = class _MessagePipe {
21468
21529
  msgType: callbackData.type,
21469
21530
  content: JSON.stringify(contentObj),
21470
21531
  timestamp: Date.now(),
21471
- isEncrypted
21532
+ isEncrypted,
21533
+ fileEncryptionMeta
21472
21534
  };
21473
21535
  log25.debug("\u{1F4E8} \u89E3\u6790 WS \u5E27", { messageId: msg.messageId, eventType: callbackData.eventType });
21474
21536
  if (this.dedup.isDuplicate(msg.messageId)) {
package/dist/index.d.cts CHANGED
@@ -195,6 +195,11 @@ interface InboundMessage {
195
195
  replyToMessageId?: string;
196
196
  /** 入站消息是否经过加密 — 用于决定回复是否也需要加密 */
197
197
  isEncrypted?: boolean;
198
+ /** 文件加密元数据 — 入站文件消息解密所需的 keyId/iv (来自 extraContent.sessionId + cryptoIv) */
199
+ fileEncryptionMeta?: {
200
+ keyId: string;
201
+ iv: string;
202
+ };
198
203
  }
199
204
  /** 出站消息 — 插件向 IM 服务器发送的消息 */
200
205
  interface OutboundMessage {
@@ -922,6 +927,18 @@ declare class MessagePipe {
922
927
  keyId: string;
923
928
  iv: string;
924
929
  }>;
930
+ /**
931
+ * 解密文件 Buffer — 按分片解密,返回解密后的 Buffer。
932
+ * 用于入站加密文件消息的下载后解密。
933
+ *
934
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
935
+ *
936
+ * @param buffer - 加密的文件 Buffer
937
+ * @param keyId - 加密时的密钥标识 (来自 extraContent.sessionId)
938
+ * @param iv - 初始化向量 (来自 extraContent.cryptoIv)
939
+ * @returns 解密后的 Buffer
940
+ */
941
+ decryptFile(buffer: Buffer, keyId: string, iv: string): Promise<Buffer>;
925
942
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
926
943
  private static readonly FILE_MSG_TYPES;
927
944
  /**
@@ -19970,6 +19970,48 @@ var MessagePipe = class _MessagePipe {
19970
19970
  });
19971
19971
  return { encryptedBuffer, keyId, iv };
19972
19972
  }
19973
+ /**
19974
+ * 解密文件 Buffer — 按分片解密,返回解密后的 Buffer。
19975
+ * 用于入站加密文件消息的下载后解密。
19976
+ *
19977
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
19978
+ *
19979
+ * @param buffer - 加密的文件 Buffer
19980
+ * @param keyId - 加密时的密钥标识 (来自 extraContent.sessionId)
19981
+ * @param iv - 初始化向量 (来自 extraContent.cryptoIv)
19982
+ * @returns 解密后的 Buffer
19983
+ */
19984
+ async decryptFile(buffer, keyId, iv) {
19985
+ const chunkSize = 10 * 1024 * 1024 + 16;
19986
+ const totalChunks = Math.ceil(buffer.length / chunkSize);
19987
+ let sessionKey = "";
19988
+ let fillKey = "";
19989
+ const decryptedChunks = [];
19990
+ for (let i = 0; i < totalChunks; i++) {
19991
+ const start = i * chunkSize;
19992
+ const end = Math.min(start + chunkSize, buffer.length);
19993
+ const chunk = buffer.subarray(start, end);
19994
+ const options = sessionKey ? { sessionKey, fillKey } : void 0;
19995
+ const result = await this.crypto.decryptFileChunk(chunk, keyId, iv, options);
19996
+ decryptedChunks.push(result.fileBuffer);
19997
+ sessionKey = result.sessionKey;
19998
+ fillKey = result.fillKey;
19999
+ log16.debug("\u{1F513} \u6587\u4EF6\u5206\u7247\u89E3\u5BC6", {
20000
+ chunk: `${i + 1}/${totalChunks}`,
20001
+ encryptedSize: chunk.length,
20002
+ decryptedSize: result.fileBuffer.length,
20003
+ keyId
20004
+ });
20005
+ }
20006
+ const decryptedBuffer = Buffer.concat(decryptedChunks);
20007
+ log16.info("\u{1F513} \u6587\u4EF6\u89E3\u5BC6\u5B8C\u6210", {
20008
+ encryptedSize: buffer.length,
20009
+ decryptedSize: decryptedBuffer.length,
20010
+ totalChunks,
20011
+ keyId
20012
+ });
20013
+ return decryptedBuffer;
20014
+ }
19973
20015
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
19974
20016
  static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
19975
20017
  /**
@@ -20282,10 +20324,17 @@ var MessagePipe = class _MessagePipe {
20282
20324
  });
20283
20325
  const extraContent = callbackData.extraContent;
20284
20326
  let isEncrypted = false;
20327
+ let fileEncryptionMeta;
20285
20328
  if (typeof extraContent === "string" && extraContent.length > 0) {
20286
20329
  try {
20287
20330
  const parsed = JSON.parse(extraContent);
20288
- isEncrypted = Boolean(parsed.encryptMsg);
20331
+ if (parsed.encryptMsg) {
20332
+ isEncrypted = true;
20333
+ } else if (parsed.sessionId) {
20334
+ isEncrypted = true;
20335
+ fileEncryptionMeta = { keyId: parsed.sessionId, iv: parsed.cryptoIv ?? "" };
20336
+ log16.info("\u{1F512} \u5165\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6", { msgUid: callbackData.msgUid, keyId: parsed.sessionId });
20337
+ }
20289
20338
  } catch {
20290
20339
  log16.debug("\u2139\uFE0F extraContent \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
20291
20340
  }
@@ -20366,7 +20415,8 @@ var MessagePipe = class _MessagePipe {
20366
20415
  msgType: callbackData.type,
20367
20416
  content: JSON.stringify(contentObj),
20368
20417
  timestamp: Date.now(),
20369
- isEncrypted
20418
+ isEncrypted,
20419
+ fileEncryptionMeta
20370
20420
  };
20371
20421
  log16.debug("\u{1F4E8} \u89E3\u6790 WS \u5E27", { messageId: msg.messageId, eventType: callbackData.eventType });
20372
20422
  if (this.dedup.isDuplicate(msg.messageId)) {
@@ -21360,7 +21410,9 @@ async function resolveMedia(params) {
21360
21410
  sdkRuntime,
21361
21411
  maxBytes = 30 * 1024 * 1024,
21362
21412
  allowPrivateNetwork = false,
21363
- timeoutMs
21413
+ timeoutMs,
21414
+ messagePipe,
21415
+ fileEncryptionMeta
21364
21416
  } = params;
21365
21417
  const client = createHttpClient({ baseUrl: serverUrl, tokenManager, timeoutMs });
21366
21418
  log24.info("download:getUrl", { fileId });
@@ -21377,9 +21429,15 @@ async function resolveMedia(params) {
21377
21429
  url: downloadInfo.fileUrl,
21378
21430
  ssrfPolicy: { allowPrivateNetwork }
21379
21431
  });
21432
+ let fileBuffer = Buffer.from(fetched.buffer);
21433
+ if (fileEncryptionMeta && messagePipe) {
21434
+ log24.info("download:decrypting", { fileId, keyId: fileEncryptionMeta.keyId, encryptedSize: fileBuffer.length });
21435
+ fileBuffer = Buffer.from(await messagePipe.decryptFile(fileBuffer, fileEncryptionMeta.keyId, fileEncryptionMeta.iv));
21436
+ log24.info("download:decrypted", { fileId, encryptedSize: fetched.buffer.length, decryptedSize: fileBuffer.length });
21437
+ }
21380
21438
  const contentType = fetched.contentType || downloadInfo.mimeType;
21381
21439
  const saved = await sdkRuntime.channel.media.saveMediaBuffer(
21382
- fetched.buffer,
21440
+ fileBuffer,
21383
21441
  contentType,
21384
21442
  "inbound",
21385
21443
  maxBytes,
@@ -21423,7 +21481,9 @@ async function resolveContent(context, deps) {
21423
21481
  sdkRuntime: deps.sdkRuntime,
21424
21482
  maxBytes: deps.maxBytes,
21425
21483
  allowPrivateNetwork: deps.allowPrivateNetwork,
21426
- timeoutMs: deps.timeoutMs
21484
+ timeoutMs: deps.timeoutMs,
21485
+ messagePipe: deps.messagePipe,
21486
+ fileEncryptionMeta: deps.fileEncryptionMeta
21427
21487
  });
21428
21488
  log25.info("resolveContent:downloaded", {
21429
21489
  fileId: context.fileId,
@@ -21770,7 +21830,9 @@ var InboundPipeline = class {
21770
21830
  sdkRuntime: core,
21771
21831
  maxBytes: this.deps.pluginConfig.file.maxFileSizeMb * 1024 * 1024,
21772
21832
  allowPrivateNetwork: this.deps.pluginConfig.file.allowPrivateNetwork,
21773
- timeoutMs: this.deps.pluginConfig.file.fetchTimeoutMs
21833
+ timeoutMs: this.deps.pluginConfig.file.fetchTimeoutMs,
21834
+ messagePipe: this.deps.messagePipe,
21835
+ fileEncryptionMeta: msg.fileEncryptionMeta
21774
21836
  });
21775
21837
  const payload = buildInboundPayload(msg, resolvedContent, this.deps.pluginConfig);
21776
21838
  const { sdkConfig } = this.deps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liangzimixin",
3
- "version": "0.3.73",
3
+ "version": "0.3.74",
4
4
  "description": "Quantum-encrypted IM channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -7,7 +7,7 @@ REM liangzimixin install script (Windows)
7
7
  REM Usage: liangzimixin_install.bat <appId> <appSecret> [quantumAccount]
8
8
  REM ============================================================
9
9
 
10
- set "SCRIPT_VERSION=0.3.73"
10
+ set "SCRIPT_VERSION=0.3.74"
11
11
  set "NPM_PACKAGE=liangzimixin"
12
12
 
13
13
  set "SKIP_SELF_UPDATE=0"
@@ -6,7 +6,7 @@ set -euo pipefail
6
6
  # 用法: ./liangzimixin_install.sh <appId> <appSecret> [quantumAccount]
7
7
  # ============================================================
8
8
 
9
- SCRIPT_VERSION="0.3.73"
9
+ SCRIPT_VERSION="0.3.74"
10
10
  NPM_PACKAGE="liangzimixin"
11
11
 
12
12
  # ── 颜色 ──────────────────────────────────────────────────────