liangzimixin 0.3.73 → 0.3.75

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;
@@ -20563,6 +20575,16 @@ var TokenManager = class {
20563
20575
  isAuthorized() {
20564
20576
  return this.cachedToken !== null;
20565
20577
  }
20578
+ /**
20579
+ * 失效当前内存缓存的 Token — 下次 getValidToken() 将重新获取。
20580
+ * 用于出站 HTTP 收到 401 时主动清除可能已过期的 Token。
20581
+ * 注意: 不清除文件存储,_acquireToken 会重新获取并覆盖。
20582
+ */
20583
+ invalidate() {
20584
+ this.cachedToken = null;
20585
+ this.currentTokenData = null;
20586
+ log21.info("Token invalidated (will re-acquire on next call)");
20587
+ }
20566
20588
  /** 废置并清除所有令牌 (包括文件存储和内存缓存) */
20567
20589
  async revokeAndClear() {
20568
20590
  this.clearRefreshTimer();
@@ -20966,6 +20988,8 @@ var MessagePipe = class _MessagePipe {
20966
20988
  crypto;
20967
20989
  /** 获取最新 access_token 的回调 (用于 HMAC 验签和出站 Bearer 认证) */
20968
20990
  tokenFn;
20991
+ /** 失效当前 Token 缓存的回调 — 收到 401 时调用,强制下次 tokenFn 重新获取 */
20992
+ invalidateTokenFn;
20969
20993
  /** 消息服务 API 基础地址 (用于出站发送/撤回) */
20970
20994
  messageServiceBaseUrl;
20971
20995
  /** L4 层注册的入站消息回调 */
@@ -20985,6 +21009,7 @@ var MessagePipe = class _MessagePipe {
20985
21009
  this.dedup = deps.dedup;
20986
21010
  this.crypto = deps.crypto;
20987
21011
  this.tokenFn = deps.tokenFn;
21012
+ this.invalidateTokenFn = deps.invalidateTokenFn;
20988
21013
  this.messageServiceBaseUrl = deps.messageServiceBaseUrl;
20989
21014
  this.quantumAccount = deps.quantumAccount;
20990
21015
  this.encryptionMode = deps.encryptionMode;
@@ -21072,6 +21097,48 @@ var MessagePipe = class _MessagePipe {
21072
21097
  });
21073
21098
  return { encryptedBuffer, keyId, iv };
21074
21099
  }
21100
+ /**
21101
+ * 解密文件 Buffer — 按分片解密,返回解密后的 Buffer。
21102
+ * 用于入站加密文件消息的下载后解密。
21103
+ *
21104
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
21105
+ *
21106
+ * @param buffer - 加密的文件 Buffer
21107
+ * @param keyId - 加密时的密钥标识 (来自 extraContent.sessionId)
21108
+ * @param iv - 初始化向量 (来自 extraContent.cryptoIv)
21109
+ * @returns 解密后的 Buffer
21110
+ */
21111
+ async decryptFile(buffer, keyId, iv) {
21112
+ const chunkSize = 10 * 1024 * 1024 + 16;
21113
+ const totalChunks = Math.ceil(buffer.length / chunkSize);
21114
+ let sessionKey = "";
21115
+ let fillKey = "";
21116
+ const decryptedChunks = [];
21117
+ for (let i = 0; i < totalChunks; i++) {
21118
+ const start = i * chunkSize;
21119
+ const end = Math.min(start + chunkSize, buffer.length);
21120
+ const chunk = buffer.subarray(start, end);
21121
+ const options = sessionKey ? { sessionKey, fillKey } : void 0;
21122
+ const result = await this.crypto.decryptFileChunk(chunk, keyId, iv, options);
21123
+ decryptedChunks.push(result.fileBuffer);
21124
+ sessionKey = result.sessionKey;
21125
+ fillKey = result.fillKey;
21126
+ log25.debug("\u{1F513} \u6587\u4EF6\u5206\u7247\u89E3\u5BC6", {
21127
+ chunk: `${i + 1}/${totalChunks}`,
21128
+ encryptedSize: chunk.length,
21129
+ decryptedSize: result.fileBuffer.length,
21130
+ keyId
21131
+ });
21132
+ }
21133
+ const decryptedBuffer = Buffer.concat(decryptedChunks);
21134
+ log25.info("\u{1F513} \u6587\u4EF6\u89E3\u5BC6\u5B8C\u6210", {
21135
+ encryptedSize: buffer.length,
21136
+ decryptedSize: decryptedBuffer.length,
21137
+ totalChunks,
21138
+ keyId
21139
+ });
21140
+ return decryptedBuffer;
21141
+ }
21075
21142
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
21076
21143
  static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
21077
21144
  /**
@@ -21246,6 +21313,11 @@ var MessagePipe = class _MessagePipe {
21246
21313
  url: url2,
21247
21314
  body
21248
21315
  });
21316
+ if (resp.status === 401 && this.invalidateTokenFn) {
21317
+ log25.warn(`${logTag}:token-expired, invalidating cached token for retry`);
21318
+ this.invalidateTokenFn();
21319
+ return { code: -1, msg: `HTTP ${resp.status}`, _retryable: true };
21320
+ }
21249
21321
  return { code: -1, msg: `HTTP ${resp.status}`, _retryable: resp.status >= 500 };
21250
21322
  }
21251
21323
  const result = await resp.json();
@@ -21281,7 +21353,7 @@ var MessagePipe = class _MessagePipe {
21281
21353
  const result = await this._callMessageApiOnce(url2, body, logTag);
21282
21354
  if (result && "_retryable" in result) {
21283
21355
  if (result._retryable) {
21284
- log25.warn(`${logTag}:retrying after 5xx`, { url: url2 });
21356
+ log25.warn(`${logTag}:retrying after ${result.msg}`, { url: url2 });
21285
21357
  await new Promise((r) => setTimeout(r, 1e3));
21286
21358
  try {
21287
21359
  const retryResult = await this._callMessageApiOnce(url2, body, `${logTag}:retry`);
@@ -21384,10 +21456,17 @@ var MessagePipe = class _MessagePipe {
21384
21456
  });
21385
21457
  const extraContent = callbackData.extraContent;
21386
21458
  let isEncrypted = false;
21459
+ let fileEncryptionMeta;
21387
21460
  if (typeof extraContent === "string" && extraContent.length > 0) {
21388
21461
  try {
21389
21462
  const parsed = JSON.parse(extraContent);
21390
- isEncrypted = Boolean(parsed.encryptMsg);
21463
+ if (parsed.encryptMsg) {
21464
+ isEncrypted = true;
21465
+ } else if (parsed.sessionId) {
21466
+ isEncrypted = true;
21467
+ fileEncryptionMeta = { keyId: parsed.sessionId, iv: parsed.cryptoIv ?? "" };
21468
+ log25.info("\u{1F512} \u5165\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6", { msgUid: callbackData.msgUid, keyId: parsed.sessionId });
21469
+ }
21391
21470
  } catch {
21392
21471
  log25.debug("\u2139\uFE0F extraContent \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
21393
21472
  }
@@ -21468,7 +21547,8 @@ var MessagePipe = class _MessagePipe {
21468
21547
  msgType: callbackData.type,
21469
21548
  content: JSON.stringify(contentObj),
21470
21549
  timestamp: Date.now(),
21471
- isEncrypted
21550
+ isEncrypted,
21551
+ fileEncryptionMeta
21472
21552
  };
21473
21553
  log25.debug("\u{1F4E8} \u89E3\u6790 WS \u5E27", { messageId: msg.messageId, eventType: callbackData.eventType });
21474
21554
  if (this.dedup.isDuplicate(msg.messageId)) {
@@ -22199,6 +22279,7 @@ async function startPlugin(accountConfig, internalOverrides) {
22199
22279
  dedup,
22200
22280
  crypto: cryptoEngine,
22201
22281
  tokenFn: () => tokenManager.getValidToken(),
22282
+ invalidateTokenFn: () => tokenManager.invalidate(),
22202
22283
  messageServiceBaseUrl: config2.message.messageServiceBaseUrl,
22203
22284
  quantumAccount: accountConfig.quantumAccount,
22204
22285
  encryptionMode: accountConfig.encryptionMode,
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 {
@@ -645,6 +650,12 @@ declare class TokenManager {
645
650
  hasScope(scope: string): boolean;
646
651
  /** 检查是否已授权 (是否持有有效令牌) */
647
652
  isAuthorized(): boolean;
653
+ /**
654
+ * 失效当前内存缓存的 Token — 下次 getValidToken() 将重新获取。
655
+ * 用于出站 HTTP 收到 401 时主动清除可能已过期的 Token。
656
+ * 注意: 不清除文件存储,_acquireToken 会重新获取并覆盖。
657
+ */
658
+ invalidate(): void;
648
659
  /** 废置并清除所有令牌 (包括文件存储和内存缓存) */
649
660
  revokeAndClear(): Promise<void>;
650
661
  /** 清理定时器和并发锁 — 优雅关闭时调用 */
@@ -861,6 +872,8 @@ declare class MessagePipe {
861
872
  private readonly crypto;
862
873
  /** 获取最新 access_token 的回调 (用于 HMAC 验签和出站 Bearer 认证) */
863
874
  private readonly tokenFn;
875
+ /** 失效当前 Token 缓存的回调 — 收到 401 时调用,强制下次 tokenFn 重新获取 */
876
+ private readonly invalidateTokenFn?;
864
877
  /** 消息服务 API 基础地址 (用于出站发送/撤回) */
865
878
  private readonly messageServiceBaseUrl;
866
879
  /** L4 层注册的入站消息回调 */
@@ -881,6 +894,8 @@ declare class MessagePipe {
881
894
  crypto: CryptoEngine;
882
895
  /** TokenManager.getValidToken — 用于验签和出站认证 */
883
896
  tokenFn: () => Promise<string>;
897
+ /** TokenManager.invalidate — 收到 401 时清除 Token 缓存 */
898
+ invalidateTokenFn?: () => void;
884
899
  /** 消息服务 API 基础地址 */
885
900
  messageServiceBaseUrl: string;
886
901
  /** 量子账户标识 — 用于判断是否具备解密能力 */
@@ -922,6 +937,18 @@ declare class MessagePipe {
922
937
  keyId: string;
923
938
  iv: string;
924
939
  }>;
940
+ /**
941
+ * 解密文件 Buffer — 按分片解密,返回解密后的 Buffer。
942
+ * 用于入站加密文件消息的下载后解密。
943
+ *
944
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
945
+ *
946
+ * @param buffer - 加密的文件 Buffer
947
+ * @param keyId - 加密时的密钥标识 (来自 extraContent.sessionId)
948
+ * @param iv - 初始化向量 (来自 extraContent.cryptoIv)
949
+ * @returns 解密后的 Buffer
950
+ */
951
+ decryptFile(buffer: Buffer, keyId: string, iv: string): Promise<Buffer>;
925
952
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
926
953
  private static readonly FILE_MSG_TYPES;
927
954
  /**
@@ -19461,6 +19461,16 @@ var TokenManager = class {
19461
19461
  isAuthorized() {
19462
19462
  return this.cachedToken !== null;
19463
19463
  }
19464
+ /**
19465
+ * 失效当前内存缓存的 Token — 下次 getValidToken() 将重新获取。
19466
+ * 用于出站 HTTP 收到 401 时主动清除可能已过期的 Token。
19467
+ * 注意: 不清除文件存储,_acquireToken 会重新获取并覆盖。
19468
+ */
19469
+ invalidate() {
19470
+ this.cachedToken = null;
19471
+ this.currentTokenData = null;
19472
+ log12.info("Token invalidated (will re-acquire on next call)");
19473
+ }
19464
19474
  /** 废置并清除所有令牌 (包括文件存储和内存缓存) */
19465
19475
  async revokeAndClear() {
19466
19476
  this.clearRefreshTimer();
@@ -19864,6 +19874,8 @@ var MessagePipe = class _MessagePipe {
19864
19874
  crypto;
19865
19875
  /** 获取最新 access_token 的回调 (用于 HMAC 验签和出站 Bearer 认证) */
19866
19876
  tokenFn;
19877
+ /** 失效当前 Token 缓存的回调 — 收到 401 时调用,强制下次 tokenFn 重新获取 */
19878
+ invalidateTokenFn;
19867
19879
  /** 消息服务 API 基础地址 (用于出站发送/撤回) */
19868
19880
  messageServiceBaseUrl;
19869
19881
  /** L4 层注册的入站消息回调 */
@@ -19883,6 +19895,7 @@ var MessagePipe = class _MessagePipe {
19883
19895
  this.dedup = deps.dedup;
19884
19896
  this.crypto = deps.crypto;
19885
19897
  this.tokenFn = deps.tokenFn;
19898
+ this.invalidateTokenFn = deps.invalidateTokenFn;
19886
19899
  this.messageServiceBaseUrl = deps.messageServiceBaseUrl;
19887
19900
  this.quantumAccount = deps.quantumAccount;
19888
19901
  this.encryptionMode = deps.encryptionMode;
@@ -19970,6 +19983,48 @@ var MessagePipe = class _MessagePipe {
19970
19983
  });
19971
19984
  return { encryptedBuffer, keyId, iv };
19972
19985
  }
19986
+ /**
19987
+ * 解密文件 Buffer — 按分片解密,返回解密后的 Buffer。
19988
+ * 用于入站加密文件消息的下载后解密。
19989
+ *
19990
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
19991
+ *
19992
+ * @param buffer - 加密的文件 Buffer
19993
+ * @param keyId - 加密时的密钥标识 (来自 extraContent.sessionId)
19994
+ * @param iv - 初始化向量 (来自 extraContent.cryptoIv)
19995
+ * @returns 解密后的 Buffer
19996
+ */
19997
+ async decryptFile(buffer, keyId, iv) {
19998
+ const chunkSize = 10 * 1024 * 1024 + 16;
19999
+ const totalChunks = Math.ceil(buffer.length / chunkSize);
20000
+ let sessionKey = "";
20001
+ let fillKey = "";
20002
+ const decryptedChunks = [];
20003
+ for (let i = 0; i < totalChunks; i++) {
20004
+ const start = i * chunkSize;
20005
+ const end = Math.min(start + chunkSize, buffer.length);
20006
+ const chunk = buffer.subarray(start, end);
20007
+ const options = sessionKey ? { sessionKey, fillKey } : void 0;
20008
+ const result = await this.crypto.decryptFileChunk(chunk, keyId, iv, options);
20009
+ decryptedChunks.push(result.fileBuffer);
20010
+ sessionKey = result.sessionKey;
20011
+ fillKey = result.fillKey;
20012
+ log16.debug("\u{1F513} \u6587\u4EF6\u5206\u7247\u89E3\u5BC6", {
20013
+ chunk: `${i + 1}/${totalChunks}`,
20014
+ encryptedSize: chunk.length,
20015
+ decryptedSize: result.fileBuffer.length,
20016
+ keyId
20017
+ });
20018
+ }
20019
+ const decryptedBuffer = Buffer.concat(decryptedChunks);
20020
+ log16.info("\u{1F513} \u6587\u4EF6\u89E3\u5BC6\u5B8C\u6210", {
20021
+ encryptedSize: buffer.length,
20022
+ decryptedSize: decryptedBuffer.length,
20023
+ totalChunks,
20024
+ keyId
20025
+ });
20026
+ return decryptedBuffer;
20027
+ }
19973
20028
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
19974
20029
  static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
19975
20030
  /**
@@ -20144,6 +20199,11 @@ var MessagePipe = class _MessagePipe {
20144
20199
  url: url2,
20145
20200
  body
20146
20201
  });
20202
+ if (resp.status === 401 && this.invalidateTokenFn) {
20203
+ log16.warn(`${logTag}:token-expired, invalidating cached token for retry`);
20204
+ this.invalidateTokenFn();
20205
+ return { code: -1, msg: `HTTP ${resp.status}`, _retryable: true };
20206
+ }
20147
20207
  return { code: -1, msg: `HTTP ${resp.status}`, _retryable: resp.status >= 500 };
20148
20208
  }
20149
20209
  const result = await resp.json();
@@ -20179,7 +20239,7 @@ var MessagePipe = class _MessagePipe {
20179
20239
  const result = await this._callMessageApiOnce(url2, body, logTag);
20180
20240
  if (result && "_retryable" in result) {
20181
20241
  if (result._retryable) {
20182
- log16.warn(`${logTag}:retrying after 5xx`, { url: url2 });
20242
+ log16.warn(`${logTag}:retrying after ${result.msg}`, { url: url2 });
20183
20243
  await new Promise((r) => setTimeout(r, 1e3));
20184
20244
  try {
20185
20245
  const retryResult = await this._callMessageApiOnce(url2, body, `${logTag}:retry`);
@@ -20282,10 +20342,17 @@ var MessagePipe = class _MessagePipe {
20282
20342
  });
20283
20343
  const extraContent = callbackData.extraContent;
20284
20344
  let isEncrypted = false;
20345
+ let fileEncryptionMeta;
20285
20346
  if (typeof extraContent === "string" && extraContent.length > 0) {
20286
20347
  try {
20287
20348
  const parsed = JSON.parse(extraContent);
20288
- isEncrypted = Boolean(parsed.encryptMsg);
20349
+ if (parsed.encryptMsg) {
20350
+ isEncrypted = true;
20351
+ } else if (parsed.sessionId) {
20352
+ isEncrypted = true;
20353
+ fileEncryptionMeta = { keyId: parsed.sessionId, iv: parsed.cryptoIv ?? "" };
20354
+ log16.info("\u{1F512} \u5165\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6", { msgUid: callbackData.msgUid, keyId: parsed.sessionId });
20355
+ }
20289
20356
  } catch {
20290
20357
  log16.debug("\u2139\uFE0F extraContent \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
20291
20358
  }
@@ -20366,7 +20433,8 @@ var MessagePipe = class _MessagePipe {
20366
20433
  msgType: callbackData.type,
20367
20434
  content: JSON.stringify(contentObj),
20368
20435
  timestamp: Date.now(),
20369
- isEncrypted
20436
+ isEncrypted,
20437
+ fileEncryptionMeta
20370
20438
  };
20371
20439
  log16.debug("\u{1F4E8} \u89E3\u6790 WS \u5E27", { messageId: msg.messageId, eventType: callbackData.eventType });
20372
20440
  if (this.dedup.isDuplicate(msg.messageId)) {
@@ -21174,6 +21242,7 @@ async function startPlugin(accountConfig, internalOverrides) {
21174
21242
  dedup,
21175
21243
  crypto: cryptoEngine,
21176
21244
  tokenFn: () => tokenManager.getValidToken(),
21245
+ invalidateTokenFn: () => tokenManager.invalidate(),
21177
21246
  messageServiceBaseUrl: config2.message.messageServiceBaseUrl,
21178
21247
  quantumAccount: accountConfig.quantumAccount,
21179
21248
  encryptionMode: accountConfig.encryptionMode,
@@ -21360,7 +21429,9 @@ async function resolveMedia(params) {
21360
21429
  sdkRuntime,
21361
21430
  maxBytes = 30 * 1024 * 1024,
21362
21431
  allowPrivateNetwork = false,
21363
- timeoutMs
21432
+ timeoutMs,
21433
+ messagePipe,
21434
+ fileEncryptionMeta
21364
21435
  } = params;
21365
21436
  const client = createHttpClient({ baseUrl: serverUrl, tokenManager, timeoutMs });
21366
21437
  log24.info("download:getUrl", { fileId });
@@ -21377,9 +21448,15 @@ async function resolveMedia(params) {
21377
21448
  url: downloadInfo.fileUrl,
21378
21449
  ssrfPolicy: { allowPrivateNetwork }
21379
21450
  });
21451
+ let fileBuffer = Buffer.from(fetched.buffer);
21452
+ if (fileEncryptionMeta && messagePipe) {
21453
+ log24.info("download:decrypting", { fileId, keyId: fileEncryptionMeta.keyId, encryptedSize: fileBuffer.length });
21454
+ fileBuffer = Buffer.from(await messagePipe.decryptFile(fileBuffer, fileEncryptionMeta.keyId, fileEncryptionMeta.iv));
21455
+ log24.info("download:decrypted", { fileId, encryptedSize: fetched.buffer.length, decryptedSize: fileBuffer.length });
21456
+ }
21380
21457
  const contentType = fetched.contentType || downloadInfo.mimeType;
21381
21458
  const saved = await sdkRuntime.channel.media.saveMediaBuffer(
21382
- fetched.buffer,
21459
+ fileBuffer,
21383
21460
  contentType,
21384
21461
  "inbound",
21385
21462
  maxBytes,
@@ -21423,7 +21500,9 @@ async function resolveContent(context, deps) {
21423
21500
  sdkRuntime: deps.sdkRuntime,
21424
21501
  maxBytes: deps.maxBytes,
21425
21502
  allowPrivateNetwork: deps.allowPrivateNetwork,
21426
- timeoutMs: deps.timeoutMs
21503
+ timeoutMs: deps.timeoutMs,
21504
+ messagePipe: deps.messagePipe,
21505
+ fileEncryptionMeta: deps.fileEncryptionMeta
21427
21506
  });
21428
21507
  log25.info("resolveContent:downloaded", {
21429
21508
  fileId: context.fileId,
@@ -21770,7 +21849,9 @@ var InboundPipeline = class {
21770
21849
  sdkRuntime: core,
21771
21850
  maxBytes: this.deps.pluginConfig.file.maxFileSizeMb * 1024 * 1024,
21772
21851
  allowPrivateNetwork: this.deps.pluginConfig.file.allowPrivateNetwork,
21773
- timeoutMs: this.deps.pluginConfig.file.fetchTimeoutMs
21852
+ timeoutMs: this.deps.pluginConfig.file.fetchTimeoutMs,
21853
+ messagePipe: this.deps.messagePipe,
21854
+ fileEncryptionMeta: msg.fileEncryptionMeta
21774
21855
  });
21775
21856
  const payload = buildInboundPayload(msg, resolvedContent, this.deps.pluginConfig);
21776
21857
  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.75",
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.75"
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.75"
10
10
  NPM_PACKAGE="liangzimixin"
11
11
 
12
12
  # ── 颜色 ──────────────────────────────────────────────────────