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 +88 -7
- package/dist/index.d.cts +27 -0
- package/dist/setup-entry.cjs +88 -7
- package/package.json +1 -1
- package/scripts/liangzimixin_install.bat +1 -1
- package/scripts/liangzimixin_install.sh +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
/**
|
package/dist/setup-entry.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
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.
|
|
9
|
+
SCRIPT_VERSION="0.3.75"
|
|
10
10
|
NPM_PACKAGE="liangzimixin"
|
|
11
11
|
|
|
12
12
|
# ── 颜色 ──────────────────────────────────────────────────────
|