liangzimixin 0.3.46 → 0.3.47

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
@@ -2233,7 +2233,7 @@ var require_websocket = __commonJS({
2233
2233
  var http = require("http");
2234
2234
  var net = require("net");
2235
2235
  var tls = require("tls");
2236
- var { randomBytes: randomBytes3, createHash: createHash2 } = require("crypto");
2236
+ var { randomBytes: randomBytes4, createHash: createHash3 } = require("crypto");
2237
2237
  var { Duplex, Readable } = require("stream");
2238
2238
  var { URL: URL2 } = require("url");
2239
2239
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2763,7 +2763,7 @@ var require_websocket = __commonJS({
2763
2763
  }
2764
2764
  }
2765
2765
  const defaultPort = isSecure ? 443 : 80;
2766
- const key = randomBytes3(16).toString("base64");
2766
+ const key = randomBytes4(16).toString("base64");
2767
2767
  const request = isSecure ? https.request : http.request;
2768
2768
  const protocolSet = /* @__PURE__ */ new Set();
2769
2769
  let perMessageDeflate;
@@ -2893,7 +2893,7 @@ var require_websocket = __commonJS({
2893
2893
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2894
2894
  return;
2895
2895
  }
2896
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
2896
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
2897
2897
  if (res.headers["sec-websocket-accept"] !== digest) {
2898
2898
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2899
2899
  return;
@@ -3260,7 +3260,7 @@ var require_websocket_server = __commonJS({
3260
3260
  var EventEmitter2 = require("events");
3261
3261
  var http = require("http");
3262
3262
  var { Duplex } = require("stream");
3263
- var { createHash: createHash2 } = require("crypto");
3263
+ var { createHash: createHash3 } = require("crypto");
3264
3264
  var extension2 = require_extension();
3265
3265
  var PerMessageDeflate2 = require_permessage_deflate();
3266
3266
  var subprotocol2 = require_subprotocol();
@@ -3561,7 +3561,7 @@ var require_websocket_server = __commonJS({
3561
3561
  );
3562
3562
  }
3563
3563
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3564
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
3564
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
3565
3565
  const headers = [
3566
3566
  "HTTP/1.1 101 Switching Protocols",
3567
3567
  "Upgrade: websocket",
@@ -17836,8 +17836,8 @@ async function uploadMedia(params) {
17836
17836
  const { readFile: readFile3 } = await import("fs/promises");
17837
17837
  buffer = await readFile3(file2);
17838
17838
  }
17839
- const { createHash: createHash2 } = await import("crypto");
17840
- const fileHash = createHash2("md5").update(buffer).digest("hex");
17839
+ const { createHash: createHash3 } = await import("crypto");
17840
+ const fileHash = createHash3("md5").update(buffer).digest("hex");
17841
17841
  log2.debug("upload:fileHash", { fileName, fileHash });
17842
17842
  const maxBytes = maxFileSizeMb * 1024 * 1024;
17843
17843
  if (buffer.length > maxBytes) {
@@ -18017,6 +18017,65 @@ function inferMimeType(fileName) {
18017
18017
  const ext = path.extname(fileName).toLowerCase();
18018
18018
  return MIME_MAP[ext] || "application/octet-stream";
18019
18019
  }
18020
+ function getImageDimensions(buffer) {
18021
+ try {
18022
+ if (buffer.length < 24) return void 0;
18023
+ if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
18024
+ return {
18025
+ width: buffer.readUInt32BE(16),
18026
+ height: buffer.readUInt32BE(20)
18027
+ };
18028
+ }
18029
+ if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70) {
18030
+ return {
18031
+ width: buffer.readUInt16LE(6),
18032
+ height: buffer.readUInt16LE(8)
18033
+ };
18034
+ }
18035
+ if (buffer[0] === 66 && buffer[1] === 77 && buffer.length >= 26) {
18036
+ return {
18037
+ width: buffer.readInt32LE(18),
18038
+ height: Math.abs(buffer.readInt32LE(22))
18039
+ };
18040
+ }
18041
+ if (buffer[0] === 82 && buffer[1] === 73 && buffer[8] === 87 && buffer[9] === 69) {
18042
+ if (buffer[12] === 86 && buffer[13] === 80 && buffer[14] === 56 && buffer[15] === 32) {
18043
+ if (buffer.length >= 30) {
18044
+ return {
18045
+ width: buffer.readUInt16LE(26) & 16383,
18046
+ height: buffer.readUInt16LE(28) & 16383
18047
+ };
18048
+ }
18049
+ }
18050
+ if (buffer[12] === 86 && buffer[13] === 80 && buffer[14] === 56 && buffer[15] === 76) {
18051
+ if (buffer.length >= 25) {
18052
+ const bits = buffer.readUInt32LE(21);
18053
+ return {
18054
+ width: (bits & 16383) + 1,
18055
+ height: (bits >> 14 & 16383) + 1
18056
+ };
18057
+ }
18058
+ }
18059
+ }
18060
+ if (buffer[0] === 255 && buffer[1] === 216) {
18061
+ let offset = 2;
18062
+ while (offset < buffer.length - 8) {
18063
+ if (buffer[offset] !== 255) break;
18064
+ const marker = buffer[offset + 1];
18065
+ if (marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207) {
18066
+ return {
18067
+ height: buffer.readUInt16BE(offset + 5),
18068
+ width: buffer.readUInt16BE(offset + 7)
18069
+ };
18070
+ }
18071
+ const segLen = buffer.readUInt16BE(offset + 2);
18072
+ offset += 2 + segLen;
18073
+ }
18074
+ }
18075
+ } catch {
18076
+ }
18077
+ return void 0;
18078
+ }
18020
18079
  async function resolveAndUploadMedia(params) {
18021
18080
  const {
18022
18081
  mediaUrl,
@@ -18067,7 +18126,26 @@ async function resolveAndUploadMedia(params) {
18067
18126
  }
18068
18127
  const fileType = detectFileType(fileName);
18069
18128
  const mimeType = inferMimeType(fileName);
18070
- log3.info("media:fileType", { fileName, fileType, mimeType });
18129
+ const ext = path.extname(fileName).replace(".", "").toLowerCase();
18130
+ let imageDimensions;
18131
+ if (fileType === "image") {
18132
+ imageDimensions = getImageDimensions(buffer);
18133
+ log3.info("media:imageDimensions", { fileName, ...imageDimensions });
18134
+ }
18135
+ log3.info("media:fileType", { fileName, fileType, mimeType, ext });
18136
+ let encryptionMeta;
18137
+ if (!params.skipEncrypt) {
18138
+ try {
18139
+ const originalSize = buffer.length;
18140
+ const result = await messagePipe.encryptFile(buffer);
18141
+ buffer = result.encryptedBuffer;
18142
+ encryptionMeta = { keyId: result.keyId, iv: result.iv };
18143
+ log3.info("media:encrypted", { fileName, originalSize, encryptedSize: buffer.length });
18144
+ } catch (err) {
18145
+ log3.error("media:encrypt failed", { fileName, error: err.message });
18146
+ throw err;
18147
+ }
18148
+ }
18071
18149
  let uploadResult;
18072
18150
  try {
18073
18151
  uploadResult = await uploadMedia({
@@ -18092,11 +18170,17 @@ async function resolveAndUploadMedia(params) {
18092
18170
  const msgType = fileType;
18093
18171
  let contentPayload;
18094
18172
  if (fileType === "file") {
18095
- contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize, mimeType };
18173
+ contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize, ext };
18096
18174
  } else if (fileType === "image") {
18097
- contentPayload = { fileId: uploadResult.fileKey };
18175
+ contentPayload = {
18176
+ fileId: uploadResult.fileKey,
18177
+ width: imageDimensions?.width ?? 0,
18178
+ height: imageDimensions?.height ?? 0,
18179
+ altText: fileName,
18180
+ ext
18181
+ };
18098
18182
  } else {
18099
- contentPayload = { fileId: uploadResult.fileKey, mimeType };
18183
+ contentPayload = { fileId: uploadResult.fileKey, ext };
18100
18184
  }
18101
18185
  await messagePipe.sendMessage({
18102
18186
  chatId,
@@ -18104,12 +18188,15 @@ async function resolveAndUploadMedia(params) {
18104
18188
  // 私聊场景: chatId 即为用户 ID
18105
18189
  msgType,
18106
18190
  content: JSON.stringify(contentPayload),
18107
- skipEncrypt: params.skipEncrypt
18191
+ skipEncrypt: params.skipEncrypt,
18192
+ encryptionMeta
18193
+ // 文件加密的 keyId/iv → sendMessage 用于构建 extra
18108
18194
  });
18109
18195
  log3.info("media:sent", {
18110
18196
  chatId,
18111
18197
  fileId: uploadResult.fileKey,
18112
- msgType
18198
+ msgType,
18199
+ encrypted: Boolean(encryptionMeta)
18113
18200
  });
18114
18201
  return {
18115
18202
  channel: CHANNEL_ID,
@@ -20097,6 +20184,49 @@ var MessagePipe = class _MessagePipe {
20097
20184
  async injectRawFrame(rawData) {
20098
20185
  return this.handleInbound(rawData);
20099
20186
  }
20187
+ /**
20188
+ * 加密文件 Buffer — 按 10MB 分片加密,返回加密后的 Buffer + 密钥信息。
20189
+ * 用于文件上传前的加密处理。
20190
+ *
20191
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
20192
+ *
20193
+ * @param buffer - 原始文件 Buffer
20194
+ * @returns { encryptedBuffer, keyId, iv } — 加密后的 buffer + 密钥标识 + 初始化向量
20195
+ */
20196
+ async encryptFile(buffer) {
20197
+ const iv = (0, import_node_crypto3.randomBytes)(8).toString("hex");
20198
+ const chunkSize = 10 * 1024 * 1024;
20199
+ const totalChunks = Math.ceil(buffer.length / chunkSize);
20200
+ let keyId = "";
20201
+ let sessionKey = "";
20202
+ let fillKey = "";
20203
+ const encryptedChunks = [];
20204
+ for (let i = 0; i < totalChunks; i++) {
20205
+ const start = i * chunkSize;
20206
+ const end = Math.min(start + chunkSize, buffer.length);
20207
+ const chunk = buffer.subarray(start, end);
20208
+ const options = keyId ? { keyId, sessionKey, fillKey } : void 0;
20209
+ const result = await this.crypto.encryptFileChunk(chunk, iv, options);
20210
+ encryptedChunks.push(result.fileBuffer);
20211
+ keyId = result.keyId;
20212
+ sessionKey = result.sessionKey;
20213
+ fillKey = result.fillKey;
20214
+ log21.debug("\u{1F512} \u6587\u4EF6\u5206\u7247\u52A0\u5BC6", {
20215
+ chunk: `${i + 1}/${totalChunks}`,
20216
+ originalSize: chunk.length,
20217
+ encryptedSize: result.fileBuffer.length,
20218
+ keyId
20219
+ });
20220
+ }
20221
+ const encryptedBuffer = Buffer.concat(encryptedChunks);
20222
+ log21.info("\u{1F512} \u6587\u4EF6\u52A0\u5BC6\u5B8C\u6210", {
20223
+ originalSize: buffer.length,
20224
+ encryptedSize: encryptedBuffer.length,
20225
+ totalChunks,
20226
+ keyId
20227
+ });
20228
+ return { encryptedBuffer, keyId, iv };
20229
+ }
20100
20230
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
20101
20231
  static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
20102
20232
  /**
@@ -20144,11 +20274,15 @@ var MessagePipe = class _MessagePipe {
20144
20274
  const isFileMessage = _MessagePipe.FILE_MSG_TYPES.has(msg.msgType);
20145
20275
  if (!msg.skipEncrypt && this.quantumAccount) {
20146
20276
  if (this.encryptionMode === "quantum_only") {
20147
- const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
20148
- if (isFileMessage) {
20277
+ if (isFileMessage && msg.encryptionMeta) {
20278
+ extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
20279
+ log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
20280
+ } else if (isFileMessage) {
20281
+ const { keyId, iv } = await this.crypto.encrypt(msg.content);
20149
20282
  extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
20150
- log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: keyId, msgType: msg.msgType });
20283
+ log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only, fallback)", { sessionId: keyId, msgType: msg.msgType });
20151
20284
  } else {
20285
+ const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
20152
20286
  extra = JSON.stringify({
20153
20287
  cryptoIv: iv,
20154
20288
  encryptMsg: ciphertext,
@@ -20159,11 +20293,15 @@ var MessagePipe = class _MessagePipe {
20159
20293
  }
20160
20294
  } else {
20161
20295
  try {
20162
- const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
20163
- if (isFileMessage) {
20296
+ if (isFileMessage && msg.encryptionMeta) {
20297
+ extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
20298
+ log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
20299
+ } else if (isFileMessage) {
20300
+ const { keyId, iv } = await this.crypto.encrypt(msg.content);
20164
20301
  extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
20165
- log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: keyId, msgType: msg.msgType });
20302
+ log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain, fallback)", { sessionId: keyId, msgType: msg.msgType });
20166
20303
  } else {
20304
+ const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
20167
20305
  extra = JSON.stringify({
20168
20306
  cryptoIv: iv,
20169
20307
  encryptMsg: ciphertext,
package/dist/index.d.cts CHANGED
@@ -191,6 +191,11 @@ interface OutboundMessage {
191
191
  replyToMessageId?: string;
192
192
  /** 跳过出站加密 — 用于"思考中"等无需加密的系统提示消息 */
193
193
  skipEncrypt?: boolean;
194
+ /** 文件加密元数据 — 文件已在上传前加密时传入,sendMessage 直接使用此 keyId/iv 构建 extra */
195
+ encryptionMeta?: {
196
+ keyId: string;
197
+ iv: string;
198
+ };
194
199
  }
195
200
  /**
196
201
  * 加密策略枚举 — 根据消息类型选择不同的加密等级
@@ -786,6 +791,20 @@ declare class MessagePipe {
786
791
  * TODO: 验证通过后移除此方法
787
792
  */
788
793
  injectRawFrame(rawData: string | Buffer): Promise<void>;
794
+ /**
795
+ * 加密文件 Buffer — 按 10MB 分片加密,返回加密后的 Buffer + 密钥信息。
796
+ * 用于文件上传前的加密处理。
797
+ *
798
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
799
+ *
800
+ * @param buffer - 原始文件 Buffer
801
+ * @returns { encryptedBuffer, keyId, iv } — 加密后的 buffer + 密钥标识 + 初始化向量
802
+ */
803
+ encryptFile(buffer: Buffer): Promise<{
804
+ encryptedBuffer: Buffer;
805
+ keyId: string;
806
+ iv: string;
807
+ }>;
789
808
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
790
809
  private static readonly FILE_MSG_TYPES;
791
810
  /**
@@ -2233,7 +2233,7 @@ var require_websocket = __commonJS({
2233
2233
  var http = require("http");
2234
2234
  var net = require("net");
2235
2235
  var tls = require("tls");
2236
- var { randomBytes: randomBytes3, createHash: createHash2 } = require("crypto");
2236
+ var { randomBytes: randomBytes4, createHash: createHash3 } = require("crypto");
2237
2237
  var { Duplex, Readable } = require("stream");
2238
2238
  var { URL: URL2 } = require("url");
2239
2239
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2763,7 +2763,7 @@ var require_websocket = __commonJS({
2763
2763
  }
2764
2764
  }
2765
2765
  const defaultPort = isSecure ? 443 : 80;
2766
- const key = randomBytes3(16).toString("base64");
2766
+ const key = randomBytes4(16).toString("base64");
2767
2767
  const request = isSecure ? https.request : http.request;
2768
2768
  const protocolSet = /* @__PURE__ */ new Set();
2769
2769
  let perMessageDeflate;
@@ -2893,7 +2893,7 @@ var require_websocket = __commonJS({
2893
2893
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2894
2894
  return;
2895
2895
  }
2896
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
2896
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
2897
2897
  if (res.headers["sec-websocket-accept"] !== digest) {
2898
2898
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2899
2899
  return;
@@ -3260,7 +3260,7 @@ var require_websocket_server = __commonJS({
3260
3260
  var EventEmitter2 = require("events");
3261
3261
  var http = require("http");
3262
3262
  var { Duplex } = require("stream");
3263
- var { createHash: createHash2 } = require("crypto");
3263
+ var { createHash: createHash3 } = require("crypto");
3264
3264
  var extension2 = require_extension();
3265
3265
  var PerMessageDeflate2 = require_permessage_deflate();
3266
3266
  var subprotocol2 = require_subprotocol();
@@ -3561,7 +3561,7 @@ var require_websocket_server = __commonJS({
3561
3561
  );
3562
3562
  }
3563
3563
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3564
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
3564
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
3565
3565
  const headers = [
3566
3566
  "HTTP/1.1 101 Switching Protocols",
3567
3567
  "Upgrade: websocket",
@@ -3928,8 +3928,8 @@ async function uploadMedia(params) {
3928
3928
  const { readFile: readFile3 } = await import("fs/promises");
3929
3929
  buffer = await readFile3(file2);
3930
3930
  }
3931
- const { createHash: createHash2 } = await import("crypto");
3932
- const fileHash = createHash2("md5").update(buffer).digest("hex");
3931
+ const { createHash: createHash3 } = await import("crypto");
3932
+ const fileHash = createHash3("md5").update(buffer).digest("hex");
3933
3933
  log2.debug("upload:fileHash", { fileName, fileHash });
3934
3934
  const maxBytes = maxFileSizeMb * 1024 * 1024;
3935
3935
  if (buffer.length > maxBytes) {
@@ -4109,6 +4109,65 @@ function inferMimeType(fileName) {
4109
4109
  const ext = path.extname(fileName).toLowerCase();
4110
4110
  return MIME_MAP[ext] || "application/octet-stream";
4111
4111
  }
4112
+ function getImageDimensions(buffer) {
4113
+ try {
4114
+ if (buffer.length < 24) return void 0;
4115
+ if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
4116
+ return {
4117
+ width: buffer.readUInt32BE(16),
4118
+ height: buffer.readUInt32BE(20)
4119
+ };
4120
+ }
4121
+ if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70) {
4122
+ return {
4123
+ width: buffer.readUInt16LE(6),
4124
+ height: buffer.readUInt16LE(8)
4125
+ };
4126
+ }
4127
+ if (buffer[0] === 66 && buffer[1] === 77 && buffer.length >= 26) {
4128
+ return {
4129
+ width: buffer.readInt32LE(18),
4130
+ height: Math.abs(buffer.readInt32LE(22))
4131
+ };
4132
+ }
4133
+ if (buffer[0] === 82 && buffer[1] === 73 && buffer[8] === 87 && buffer[9] === 69) {
4134
+ if (buffer[12] === 86 && buffer[13] === 80 && buffer[14] === 56 && buffer[15] === 32) {
4135
+ if (buffer.length >= 30) {
4136
+ return {
4137
+ width: buffer.readUInt16LE(26) & 16383,
4138
+ height: buffer.readUInt16LE(28) & 16383
4139
+ };
4140
+ }
4141
+ }
4142
+ if (buffer[12] === 86 && buffer[13] === 80 && buffer[14] === 56 && buffer[15] === 76) {
4143
+ if (buffer.length >= 25) {
4144
+ const bits = buffer.readUInt32LE(21);
4145
+ return {
4146
+ width: (bits & 16383) + 1,
4147
+ height: (bits >> 14 & 16383) + 1
4148
+ };
4149
+ }
4150
+ }
4151
+ }
4152
+ if (buffer[0] === 255 && buffer[1] === 216) {
4153
+ let offset = 2;
4154
+ while (offset < buffer.length - 8) {
4155
+ if (buffer[offset] !== 255) break;
4156
+ const marker = buffer[offset + 1];
4157
+ if (marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207) {
4158
+ return {
4159
+ height: buffer.readUInt16BE(offset + 5),
4160
+ width: buffer.readUInt16BE(offset + 7)
4161
+ };
4162
+ }
4163
+ const segLen = buffer.readUInt16BE(offset + 2);
4164
+ offset += 2 + segLen;
4165
+ }
4166
+ }
4167
+ } catch {
4168
+ }
4169
+ return void 0;
4170
+ }
4112
4171
  async function resolveAndUploadMedia(params) {
4113
4172
  const {
4114
4173
  mediaUrl,
@@ -4159,7 +4218,26 @@ async function resolveAndUploadMedia(params) {
4159
4218
  }
4160
4219
  const fileType = detectFileType(fileName);
4161
4220
  const mimeType = inferMimeType(fileName);
4162
- log3.info("media:fileType", { fileName, fileType, mimeType });
4221
+ const ext = path.extname(fileName).replace(".", "").toLowerCase();
4222
+ let imageDimensions;
4223
+ if (fileType === "image") {
4224
+ imageDimensions = getImageDimensions(buffer);
4225
+ log3.info("media:imageDimensions", { fileName, ...imageDimensions });
4226
+ }
4227
+ log3.info("media:fileType", { fileName, fileType, mimeType, ext });
4228
+ let encryptionMeta;
4229
+ if (!params.skipEncrypt) {
4230
+ try {
4231
+ const originalSize = buffer.length;
4232
+ const result = await messagePipe.encryptFile(buffer);
4233
+ buffer = result.encryptedBuffer;
4234
+ encryptionMeta = { keyId: result.keyId, iv: result.iv };
4235
+ log3.info("media:encrypted", { fileName, originalSize, encryptedSize: buffer.length });
4236
+ } catch (err) {
4237
+ log3.error("media:encrypt failed", { fileName, error: err.message });
4238
+ throw err;
4239
+ }
4240
+ }
4163
4241
  let uploadResult;
4164
4242
  try {
4165
4243
  uploadResult = await uploadMedia({
@@ -4184,11 +4262,17 @@ async function resolveAndUploadMedia(params) {
4184
4262
  const msgType = fileType;
4185
4263
  let contentPayload;
4186
4264
  if (fileType === "file") {
4187
- contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize, mimeType };
4265
+ contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize, ext };
4188
4266
  } else if (fileType === "image") {
4189
- contentPayload = { fileId: uploadResult.fileKey };
4267
+ contentPayload = {
4268
+ fileId: uploadResult.fileKey,
4269
+ width: imageDimensions?.width ?? 0,
4270
+ height: imageDimensions?.height ?? 0,
4271
+ altText: fileName,
4272
+ ext
4273
+ };
4190
4274
  } else {
4191
- contentPayload = { fileId: uploadResult.fileKey, mimeType };
4275
+ contentPayload = { fileId: uploadResult.fileKey, ext };
4192
4276
  }
4193
4277
  await messagePipe.sendMessage({
4194
4278
  chatId,
@@ -4196,12 +4280,15 @@ async function resolveAndUploadMedia(params) {
4196
4280
  // 私聊场景: chatId 即为用户 ID
4197
4281
  msgType,
4198
4282
  content: JSON.stringify(contentPayload),
4199
- skipEncrypt: params.skipEncrypt
4283
+ skipEncrypt: params.skipEncrypt,
4284
+ encryptionMeta
4285
+ // 文件加密的 keyId/iv → sendMessage 用于构建 extra
4200
4286
  });
4201
4287
  log3.info("media:sent", {
4202
4288
  chatId,
4203
4289
  fileId: uploadResult.fileKey,
4204
- msgType
4290
+ msgType,
4291
+ encrypted: Boolean(encryptionMeta)
4205
4292
  });
4206
4293
  return {
4207
4294
  channel: CHANNEL_ID,
@@ -19141,6 +19228,49 @@ var MessagePipe = class _MessagePipe {
19141
19228
  async injectRawFrame(rawData) {
19142
19229
  return this.handleInbound(rawData);
19143
19230
  }
19231
+ /**
19232
+ * 加密文件 Buffer — 按 10MB 分片加密,返回加密后的 Buffer + 密钥信息。
19233
+ * 用于文件上传前的加密处理。
19234
+ *
19235
+ * 如果 CryptoEngine 为透传模式,直接返回原始 buffer。
19236
+ *
19237
+ * @param buffer - 原始文件 Buffer
19238
+ * @returns { encryptedBuffer, keyId, iv } — 加密后的 buffer + 密钥标识 + 初始化向量
19239
+ */
19240
+ async encryptFile(buffer) {
19241
+ const iv = (0, import_node_crypto3.randomBytes)(8).toString("hex");
19242
+ const chunkSize = 10 * 1024 * 1024;
19243
+ const totalChunks = Math.ceil(buffer.length / chunkSize);
19244
+ let keyId = "";
19245
+ let sessionKey = "";
19246
+ let fillKey = "";
19247
+ const encryptedChunks = [];
19248
+ for (let i = 0; i < totalChunks; i++) {
19249
+ const start = i * chunkSize;
19250
+ const end = Math.min(start + chunkSize, buffer.length);
19251
+ const chunk = buffer.subarray(start, end);
19252
+ const options = keyId ? { keyId, sessionKey, fillKey } : void 0;
19253
+ const result = await this.crypto.encryptFileChunk(chunk, iv, options);
19254
+ encryptedChunks.push(result.fileBuffer);
19255
+ keyId = result.keyId;
19256
+ sessionKey = result.sessionKey;
19257
+ fillKey = result.fillKey;
19258
+ log12.debug("\u{1F512} \u6587\u4EF6\u5206\u7247\u52A0\u5BC6", {
19259
+ chunk: `${i + 1}/${totalChunks}`,
19260
+ originalSize: chunk.length,
19261
+ encryptedSize: result.fileBuffer.length,
19262
+ keyId
19263
+ });
19264
+ }
19265
+ const encryptedBuffer = Buffer.concat(encryptedChunks);
19266
+ log12.info("\u{1F512} \u6587\u4EF6\u52A0\u5BC6\u5B8C\u6210", {
19267
+ originalSize: buffer.length,
19268
+ encryptedSize: encryptedBuffer.length,
19269
+ totalChunks,
19270
+ keyId
19271
+ });
19272
+ return { encryptedBuffer, keyId, iv };
19273
+ }
19144
19274
  /** 文件类消息类型集合 — 这些类型的加密消息保留原始 content(文件元数据) */
19145
19275
  static FILE_MSG_TYPES = /* @__PURE__ */ new Set(["image", "file", "voice", "video"]);
19146
19276
  /**
@@ -19188,11 +19318,15 @@ var MessagePipe = class _MessagePipe {
19188
19318
  const isFileMessage = _MessagePipe.FILE_MSG_TYPES.has(msg.msgType);
19189
19319
  if (!msg.skipEncrypt && this.quantumAccount) {
19190
19320
  if (this.encryptionMode === "quantum_only") {
19191
- const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
19192
- if (isFileMessage) {
19321
+ if (isFileMessage && msg.encryptionMeta) {
19322
+ extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
19323
+ log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
19324
+ } else if (isFileMessage) {
19325
+ const { keyId, iv } = await this.crypto.encrypt(msg.content);
19193
19326
  extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
19194
- log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: keyId, msgType: msg.msgType });
19327
+ log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only, fallback)", { sessionId: keyId, msgType: msg.msgType });
19195
19328
  } else {
19329
+ const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
19196
19330
  extra = JSON.stringify({
19197
19331
  cryptoIv: iv,
19198
19332
  encryptMsg: ciphertext,
@@ -19203,11 +19337,15 @@ var MessagePipe = class _MessagePipe {
19203
19337
  }
19204
19338
  } else {
19205
19339
  try {
19206
- const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
19207
- if (isFileMessage) {
19340
+ if (isFileMessage && msg.encryptionMeta) {
19341
+ extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
19342
+ log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
19343
+ } else if (isFileMessage) {
19344
+ const { keyId, iv } = await this.crypto.encrypt(msg.content);
19208
19345
  extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
19209
- log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: keyId, msgType: msg.msgType });
19346
+ log12.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain, fallback)", { sessionId: keyId, msgType: msg.msgType });
19210
19347
  } else {
19348
+ const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
19211
19349
  extra = JSON.stringify({
19212
19350
  cryptoIv: iv,
19213
19351
  encryptMsg: ciphertext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liangzimixin",
3
- "version": "0.3.46",
3
+ "version": "0.3.47",
4
4
  "description": "Quantum-encrypted IM channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",