liangzimixin 0.3.57 → 0.3.58

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
@@ -4428,10 +4428,10 @@ function mergeDefs(...defs) {
4428
4428
  function cloneDef(schema) {
4429
4429
  return mergeDefs(schema._zod.def);
4430
4430
  }
4431
- function getElementAtPath(obj, path2) {
4432
- if (!path2)
4431
+ function getElementAtPath(obj, path3) {
4432
+ if (!path3)
4433
4433
  return obj;
4434
- return path2.reduce((acc, key) => acc?.[key], obj);
4434
+ return path3.reduce((acc, key) => acc?.[key], obj);
4435
4435
  }
4436
4436
  function promiseAllObject(promisesObj) {
4437
4437
  const keys = Object.keys(promisesObj);
@@ -4814,11 +4814,11 @@ function aborted(x, startIndex = 0) {
4814
4814
  }
4815
4815
  return false;
4816
4816
  }
4817
- function prefixIssues(path2, issues) {
4817
+ function prefixIssues(path3, issues) {
4818
4818
  return issues.map((iss) => {
4819
4819
  var _a2;
4820
4820
  (_a2 = iss).path ?? (_a2.path = []);
4821
- iss.path.unshift(path2);
4821
+ iss.path.unshift(path3);
4822
4822
  return iss;
4823
4823
  });
4824
4824
  }
@@ -5001,7 +5001,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
5001
5001
  }
5002
5002
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
5003
5003
  const result = { errors: [] };
5004
- const processError = (error49, path2 = []) => {
5004
+ const processError = (error49, path3 = []) => {
5005
5005
  var _a2, _b;
5006
5006
  for (const issue2 of error49.issues) {
5007
5007
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -5011,7 +5011,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
5011
5011
  } else if (issue2.code === "invalid_element") {
5012
5012
  processError({ issues: issue2.issues }, issue2.path);
5013
5013
  } else {
5014
- const fullpath = [...path2, ...issue2.path];
5014
+ const fullpath = [...path3, ...issue2.path];
5015
5015
  if (fullpath.length === 0) {
5016
5016
  result.errors.push(mapper(issue2));
5017
5017
  continue;
@@ -5043,8 +5043,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
5043
5043
  }
5044
5044
  function toDotPath(_path) {
5045
5045
  const segs = [];
5046
- const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5047
- for (const seg of path2) {
5046
+ const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5047
+ for (const seg of path3) {
5048
5048
  if (typeof seg === "number")
5049
5049
  segs.push(`[${seg}]`);
5050
5050
  else if (typeof seg === "symbol")
@@ -17021,13 +17021,13 @@ function resolveRef(ref, ctx) {
17021
17021
  if (!ref.startsWith("#")) {
17022
17022
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
17023
17023
  }
17024
- const path2 = ref.slice(1).split("/").filter(Boolean);
17025
- if (path2.length === 0) {
17024
+ const path3 = ref.slice(1).split("/").filter(Boolean);
17025
+ if (path3.length === 0) {
17026
17026
  return ctx.rootSchema;
17027
17027
  }
17028
17028
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
17029
- if (path2[0] === defsKey) {
17030
- const key = path2[1];
17029
+ if (path3[0] === defsKey) {
17030
+ const key = path3[1];
17031
17031
  if (!key || !ctx.defs[key]) {
17032
17032
  throw new Error(`Reference not found: ${ref}`);
17033
17033
  }
@@ -17525,7 +17525,9 @@ var DEFAULT_INTERNAL_CONFIG = {
17525
17525
  dedup: {
17526
17526
  ttlMs: 3e5,
17527
17527
  maxEntries: 5e3
17528
- }
17528
+ },
17529
+ maxInboundConcurrency: 5,
17530
+ maxWaitingQueue: 15
17529
17531
  },
17530
17532
  auth: {
17531
17533
  serverUrl: process.env.LZMX_AUTH_URL || PRESET.apiBaseUrl,
@@ -17534,10 +17536,13 @@ var DEFAULT_INTERNAL_CONFIG = {
17534
17536
  crypto: {
17535
17537
  enabled: true,
17536
17538
  keySource: "env",
17537
- envKey: "QUANTUM_ENCRYPTION_KEY"
17539
+ envKey: "QUANTUM_ENCRYPTION_KEY",
17540
+ operationTimeoutMs: 1e4
17538
17541
  },
17539
17542
  message: {
17540
- messageServiceBaseUrl: process.env.LZMX_MSG_URL || PRESET.msgBaseUrl
17543
+ messageServiceBaseUrl: process.env.LZMX_MSG_URL || PRESET.msgBaseUrl,
17544
+ rateLimitPerSecond: 30,
17545
+ rateLimitBurst: 60
17541
17546
  },
17542
17547
  file: {
17543
17548
  fileServiceBaseUrl: process.env.LZMX_FILE_URL || PRESET.fileBaseUrl,
@@ -17546,7 +17551,12 @@ var DEFAULT_INTERNAL_CONFIG = {
17546
17551
  allowedLocalRoots: ["/tmp/openclaw"],
17547
17552
  fetchTimeoutMs: 3e4,
17548
17553
  // staging 环境文件服务部署在内网 (10.x.x.x),需要放行 SSRF 内网限制
17549
- allowPrivateNetwork: process.env.LZMX_ALLOW_PRIVATE_NETWORK ? process.env.LZMX_ALLOW_PRIVATE_NETWORK === "true" : CURRENT_ENV === "staging"
17554
+ allowPrivateNetwork: process.env.LZMX_ALLOW_PRIVATE_NETWORK ? process.env.LZMX_ALLOW_PRIVATE_NETWORK === "true" : CURRENT_ENV === "staging",
17555
+ maxUploadConcurrency: 3
17556
+ },
17557
+ metrics: {
17558
+ enabled: true,
17559
+ logIntervalMs: 6e4
17550
17560
  }
17551
17561
  };
17552
17562
  function buildPluginConfig(accountConfig, internalOverrides) {
@@ -17564,6 +17574,7 @@ function buildPluginConfig(accountConfig, internalOverrides) {
17564
17574
  serverUrl: process.env.LZMX_AUTH_URL || preset.apiBaseUrl
17565
17575
  },
17566
17576
  message: {
17577
+ ...DEFAULT_INTERNAL_CONFIG.message,
17567
17578
  messageServiceBaseUrl: process.env.LZMX_MSG_URL || preset.msgBaseUrl
17568
17579
  },
17569
17580
  file: {
@@ -17582,7 +17593,7 @@ function buildPluginConfig(accountConfig, internalOverrides) {
17582
17593
  var import_plugin_sdk3 = require("openclaw/plugin-sdk");
17583
17594
 
17584
17595
  // src/file/media.ts
17585
- var path = __toESM(require("path"), 1);
17596
+ var path2 = __toESM(require("path"), 1);
17586
17597
  var fs = __toESM(require("fs/promises"), 1);
17587
17598
 
17588
17599
  // src/types.ts
@@ -17639,9 +17650,9 @@ function createLogger(namespace, level = "info") {
17639
17650
  var log = createLogger("file/http-client");
17640
17651
  function createHttpClient(config2) {
17641
17652
  const { baseUrl, tokenManager, timeoutMs = 3e4 } = config2;
17642
- function buildUrl(path2) {
17653
+ function buildUrl(path3) {
17643
17654
  const base = baseUrl.replace(/\/$/, "");
17644
- const p = path2.startsWith("/") ? path2 : `/${path2}`;
17655
+ const p = path3.startsWith("/") ? path3 : `/${path3}`;
17645
17656
  return `${base}${p}`;
17646
17657
  }
17647
17658
  async function getHeaders(contentType) {
@@ -17683,8 +17694,8 @@ function createHttpClient(config2) {
17683
17694
  }
17684
17695
  return {
17685
17696
  /** GET 请求 — 获取资源 */
17686
- async apiGet(path2, operationContext) {
17687
- const url2 = buildUrl(path2);
17697
+ async apiGet(path3, operationContext) {
17698
+ const url2 = buildUrl(path3);
17688
17699
  log.debug("GET", { url: url2, operationContext });
17689
17700
  const response = await fetch(url2, {
17690
17701
  method: "GET",
@@ -17694,8 +17705,8 @@ function createHttpClient(config2) {
17694
17705
  return parseResponse(response, operationContext);
17695
17706
  },
17696
17707
  /** POST 请求 — 创建/提交资源 */
17697
- async apiPost(path2, body, operationContext) {
17698
- const url2 = buildUrl(path2);
17708
+ async apiPost(path3, body, operationContext) {
17709
+ const url2 = buildUrl(path3);
17699
17710
  log.debug("POST", { url: url2, operationContext });
17700
17711
  const response = await fetch(url2, {
17701
17712
  method: "POST",
@@ -17706,8 +17717,8 @@ function createHttpClient(config2) {
17706
17717
  return parseResponse(response, operationContext);
17707
17718
  },
17708
17719
  /** DELETE 请求 — 删除资源 */
17709
- async apiDelete(path2, operationContext) {
17710
- const url2 = buildUrl(path2);
17720
+ async apiDelete(path3, operationContext) {
17721
+ const url2 = buildUrl(path3);
17711
17722
  log.debug("DELETE", { url: url2, operationContext });
17712
17723
  const response = await fetch(url2, {
17713
17724
  method: "DELETE",
@@ -17750,8 +17761,8 @@ function createHttpClient(config2) {
17750
17761
  * GET 请求 (原始文本) — 用于非 JSON 响应 (如 HLS m3u8 manifest)。
17751
17762
  * 不经过 {code, msg, data} 解析,直接返回响应文本。
17752
17763
  */
17753
- async apiGetRaw(path2, operationContext) {
17754
- const url2 = buildUrl(path2);
17764
+ async apiGetRaw(path3, operationContext) {
17765
+ const url2 = buildUrl(path3);
17755
17766
  log.debug("GET (raw)", { url: url2, operationContext });
17756
17767
  const response = await fetch(url2, {
17757
17768
  method: "GET",
@@ -17806,7 +17817,227 @@ async function getDownloadUrl(client, fileId) {
17806
17817
  }
17807
17818
 
17808
17819
  // src/file/upload.ts
17809
- var log2 = createLogger("file/upload");
17820
+ var path = __toESM(require("path"), 1);
17821
+
17822
+ // src/utils/semaphore.ts
17823
+ var log2 = createLogger("transport/semaphore");
17824
+ var Semaphore = class {
17825
+ /** 最大并发数 */
17826
+ maxConcurrency;
17827
+ /** 等待队列最大长度 */
17828
+ maxWaiting;
17829
+ /** 当前持有 slot 的数量 */
17830
+ active = 0;
17831
+ /** 等待队列 (FIFO) */
17832
+ waiters = [];
17833
+ // ── 统计指标 ──
17834
+ /** 累计获取成功次数 */
17835
+ acquiredCount = 0;
17836
+ /** 累计拒绝次数 (队列溢出) */
17837
+ rejectedCount = 0;
17838
+ constructor(options) {
17839
+ this.maxConcurrency = options.maxConcurrency;
17840
+ this.maxWaiting = options.maxWaiting ?? 50;
17841
+ log2.info("Semaphore initialized", {
17842
+ maxConcurrency: this.maxConcurrency,
17843
+ maxWaiting: this.maxWaiting
17844
+ });
17845
+ }
17846
+ /**
17847
+ * 获取一个并发 slot。
17848
+ *
17849
+ * - 有空闲 slot → 立即返回 'acquired'
17850
+ * - 无空闲 slot 且等待队列未满 → 排队等待,返回 Promise<'acquired'>
17851
+ * - 无空闲 slot 且等待队列已满 → 立即返回 'rejected'
17852
+ */
17853
+ async acquire() {
17854
+ if (this.active < this.maxConcurrency) {
17855
+ this.active++;
17856
+ this.acquiredCount++;
17857
+ return "acquired";
17858
+ }
17859
+ if (this.waiters.length >= this.maxWaiting) {
17860
+ this.rejectedCount++;
17861
+ log2.warn("semaphore:rejected", {
17862
+ active: this.active,
17863
+ waiting: this.waiters.length,
17864
+ maxWaiting: this.maxWaiting
17865
+ });
17866
+ return "rejected";
17867
+ }
17868
+ return new Promise((resolve3) => {
17869
+ this.waiters.push({ resolve: resolve3 });
17870
+ });
17871
+ }
17872
+ /**
17873
+ * 非阻塞尝试获取 slot。
17874
+ * 有空闲 slot 返回 true,否则返回 false (不排队)。
17875
+ */
17876
+ tryAcquire() {
17877
+ if (this.active < this.maxConcurrency) {
17878
+ this.active++;
17879
+ this.acquiredCount++;
17880
+ return true;
17881
+ }
17882
+ return false;
17883
+ }
17884
+ /**
17885
+ * 释放一个并发 slot。
17886
+ * 如果等待队列非空,唤醒下一个 waiter。
17887
+ */
17888
+ release() {
17889
+ if (this.active <= 0) {
17890
+ log2.warn("semaphore:release called but no active slots");
17891
+ return;
17892
+ }
17893
+ if (this.waiters.length > 0) {
17894
+ const waiter = this.waiters.shift();
17895
+ this.acquiredCount++;
17896
+ waiter.resolve("acquired");
17897
+ return;
17898
+ }
17899
+ this.active--;
17900
+ }
17901
+ /** 当前活跃 slot 数 */
17902
+ get activeCount() {
17903
+ return this.active;
17904
+ }
17905
+ /** 当前等待队列长度 */
17906
+ get waitingCount() {
17907
+ return this.waiters.length;
17908
+ }
17909
+ /** 累计获取成功次数 */
17910
+ get totalAcquired() {
17911
+ return this.acquiredCount;
17912
+ }
17913
+ /** 累计拒绝次数 */
17914
+ get totalRejected() {
17915
+ return this.rejectedCount;
17916
+ }
17917
+ /** 获取指标快照 */
17918
+ getStats() {
17919
+ return {
17920
+ active: this.active,
17921
+ waiting: this.waiters.length,
17922
+ acquired: this.acquiredCount,
17923
+ rejected: this.rejectedCount
17924
+ };
17925
+ }
17926
+ };
17927
+
17928
+ // src/metrics.ts
17929
+ var log3 = createLogger("metrics");
17930
+ var HISTOGRAM_SAMPLE_SIZE = 1e3;
17931
+ var Metrics = class {
17932
+ /** 计数器: name → count */
17933
+ counters = /* @__PURE__ */ new Map();
17934
+ /** 直方图: name → HistogramData */
17935
+ histograms = /* @__PURE__ */ new Map();
17936
+ /** 定期输出定时器 */
17937
+ logTimer = null;
17938
+ /** 是否已启动 */
17939
+ started = false;
17940
+ /** 递增计数器 */
17941
+ increment(name, delta = 1) {
17942
+ const current = this.counters.get(name) ?? 0;
17943
+ this.counters.set(name, current + delta);
17944
+ }
17945
+ /** 记录延迟样本到直方图 */
17946
+ recordLatency(name, durationMs) {
17947
+ let hist = this.histograms.get(name);
17948
+ if (!hist) {
17949
+ hist = {
17950
+ count: 0,
17951
+ min: Infinity,
17952
+ max: -Infinity,
17953
+ sum: 0,
17954
+ samples: new Array(HISTOGRAM_SAMPLE_SIZE),
17955
+ writeIndex: 0,
17956
+ sampleCount: 0
17957
+ };
17958
+ this.histograms.set(name, hist);
17959
+ }
17960
+ hist.count++;
17961
+ hist.sum += durationMs;
17962
+ hist.min = Math.min(hist.min, durationMs);
17963
+ hist.max = Math.max(hist.max, durationMs);
17964
+ hist.samples[hist.writeIndex] = durationMs;
17965
+ hist.writeIndex = (hist.writeIndex + 1) % HISTOGRAM_SAMPLE_SIZE;
17966
+ if (hist.sampleCount < HISTOGRAM_SAMPLE_SIZE) hist.sampleCount++;
17967
+ }
17968
+ /** 获取指标快照 */
17969
+ snapshot() {
17970
+ const counters = {};
17971
+ for (const [name, count] of this.counters) {
17972
+ counters[name] = count;
17973
+ }
17974
+ const histograms = {};
17975
+ for (const [name, hist] of this.histograms) {
17976
+ if (hist.count === 0) continue;
17977
+ const validSamples = hist.sampleCount < HISTOGRAM_SAMPLE_SIZE ? hist.samples.slice(0, hist.sampleCount) : [...hist.samples];
17978
+ const sorted = validSamples.sort((a, b) => a - b);
17979
+ histograms[name] = {
17980
+ count: hist.count,
17981
+ min: hist.min === Infinity ? 0 : Math.round(hist.min),
17982
+ max: hist.max === -Infinity ? 0 : Math.round(hist.max),
17983
+ avg: Math.round(hist.sum / hist.count),
17984
+ p50: percentile(sorted, 0.5),
17985
+ p95: percentile(sorted, 0.95),
17986
+ p99: percentile(sorted, 0.99)
17987
+ };
17988
+ }
17989
+ return {
17990
+ counters,
17991
+ histograms,
17992
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
17993
+ };
17994
+ }
17995
+ /**
17996
+ * 启动定期日志输出。
17997
+ * @param intervalMs - 输出间隔 (ms),默认 60000 (1 分钟)
17998
+ */
17999
+ startPeriodicLog(intervalMs = 6e4) {
18000
+ if (this.started) return;
18001
+ this.started = true;
18002
+ this.logTimer = setInterval(() => {
18003
+ const snap = this.snapshot();
18004
+ if (Object.keys(snap.counters).length > 0 || Object.keys(snap.histograms).length > 0) {
18005
+ log3.info("\u{1F4CA} metrics snapshot", snap);
18006
+ }
18007
+ }, intervalMs);
18008
+ if (this.logTimer && typeof this.logTimer === "object" && "unref" in this.logTimer) {
18009
+ this.logTimer.unref();
18010
+ }
18011
+ log3.info("Metrics periodic logging started", { intervalMs });
18012
+ }
18013
+ /** 停止定期日志输出 */
18014
+ stop() {
18015
+ if (this.logTimer) {
18016
+ clearInterval(this.logTimer);
18017
+ this.logTimer = null;
18018
+ }
18019
+ this.started = false;
18020
+ log3.info("Metrics stopped");
18021
+ }
18022
+ /** 重置所有指标 (测试用) */
18023
+ reset() {
18024
+ this.counters.clear();
18025
+ this.histograms.clear();
18026
+ }
18027
+ };
18028
+ function percentile(sorted, p) {
18029
+ if (sorted.length === 0) return 0;
18030
+ const index = Math.ceil(sorted.length * p) - 1;
18031
+ return Math.round(sorted[Math.max(0, index)]);
18032
+ }
18033
+ var metrics = new Metrics();
18034
+
18035
+ // src/file/upload.ts
18036
+ var log4 = createLogger("file/upload");
18037
+ var uploadSemaphore = null;
18038
+ function initUploadSemaphore(maxConcurrency = 3) {
18039
+ uploadSemaphore = new Semaphore({ maxConcurrency, maxWaiting: 20 });
18040
+ }
17810
18041
  function splitBuffer(buffer, chunkSize) {
17811
18042
  const chunks = [];
17812
18043
  for (let offset = 0; offset < buffer.length; offset += chunkSize) {
@@ -17814,11 +18045,19 @@ function splitBuffer(buffer, chunkSize) {
17814
18045
  }
17815
18046
  return chunks;
17816
18047
  }
18048
+ function sanitizeFileName(fileName) {
18049
+ let sanitized = path.basename(fileName);
18050
+ sanitized = sanitized.replace(/[^\p{L}\p{N}_.\- ()]/gu, "");
18051
+ if (!sanitized) {
18052
+ sanitized = `unnamed_${Date.now()}`;
18053
+ }
18054
+ return sanitized;
18055
+ }
17817
18056
  async function uploadChunk(client, url2, chunk, operationContext) {
17818
18057
  const response = await client.rawPut(url2, chunk, operationContext);
17819
18058
  const etag = response.headers.get("etag") || response.headers.get("ETag") || "";
17820
18059
  if (!etag) {
17821
- log2.warn("OSS PUT \u54CD\u5E94\u7F3A\u5C11 ETag header", { operationContext });
18060
+ log4.warn("OSS PUT \u54CD\u5E94\u7F3A\u5C11 ETag header", { operationContext });
17822
18061
  }
17823
18062
  return { etag, size: chunk.length };
17824
18063
  }
@@ -17827,6 +18066,24 @@ function isPartExpired(part) {
17827
18066
  return Date.now() > expiresAt - 6e4;
17828
18067
  }
17829
18068
  async function uploadMedia(params) {
18069
+ if (uploadSemaphore) {
18070
+ const result = await uploadSemaphore.acquire();
18071
+ if (result === "rejected") {
18072
+ metrics.increment("upload.semaphore_rejected");
18073
+ throw new FileServiceError({ message: "\u6587\u4EF6\u4E0A\u4F20\u961F\u5217\u5DF2\u6EE1\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", code: 429 });
18074
+ }
18075
+ }
18076
+ try {
18077
+ return await uploadMediaInternal(params);
18078
+ } finally {
18079
+ if (uploadSemaphore) {
18080
+ uploadSemaphore.release();
18081
+ }
18082
+ metrics.increment("upload.total");
18083
+ }
18084
+ }
18085
+ async function uploadMediaInternal(params) {
18086
+ const startMs = Date.now();
17830
18087
  const {
17831
18088
  file: file2,
17832
18089
  fileName,
@@ -17837,6 +18094,10 @@ async function uploadMedia(params) {
17837
18094
  chunkSizeMb = 5,
17838
18095
  timeoutMs
17839
18096
  } = params;
18097
+ const safeFileName = sanitizeFileName(fileName);
18098
+ if (safeFileName !== fileName) {
18099
+ log4.warn("upload:fileName sanitized", { original: fileName, sanitized: safeFileName });
18100
+ }
17840
18101
  let buffer;
17841
18102
  if (Buffer.isBuffer(file2)) {
17842
18103
  buffer = file2;
@@ -17846,19 +18107,19 @@ async function uploadMedia(params) {
17846
18107
  }
17847
18108
  const { createHash: createHash3 } = await import("crypto");
17848
18109
  const fileHash = createHash3("md5").update(buffer).digest("hex");
17849
- log2.debug("upload:fileHash", { fileName, fileHash });
18110
+ log4.debug("upload:fileHash", { fileName: safeFileName, fileHash });
17850
18111
  const maxBytes = maxFileSizeMb * 1024 * 1024;
17851
18112
  if (buffer.length > maxBytes) {
17852
18113
  throw new FileServiceError({
17853
18114
  message: `\u6587\u4EF6\u5927\u5C0F ${(buffer.length / 1024 / 1024).toFixed(1)}MB \u8D85\u8FC7\u4E0A\u9650 ${maxFileSizeMb}MB`,
17854
18115
  code: -1,
17855
- operationContext: `uploadMedia(${fileName})`
18116
+ operationContext: `uploadMedia(${safeFileName})`
17856
18117
  });
17857
18118
  }
17858
18119
  const client = createHttpClient({ baseUrl: serverUrl, tokenManager, timeoutMs });
17859
- log2.info("upload:init", { fileName, fileSize: buffer.length, mimeType });
18120
+ log4.info("upload:init", { fileName: safeFileName, fileSize: buffer.length, mimeType });
17860
18121
  const initResult = await initUpload(client, {
17861
- fileName,
18122
+ fileName: safeFileName,
17862
18123
  fileSize: buffer.length,
17863
18124
  mimeType,
17864
18125
  fileHash,
@@ -17866,8 +18127,8 @@ async function uploadMedia(params) {
17866
18127
  category: 1
17867
18128
  });
17868
18129
  if (initResult.deduplicatedHit && initResult.fileKey) {
17869
- log2.info("upload:dedup hit \u26A1", {
17870
- fileName,
18130
+ log4.info("upload:dedup hit \u26A1", {
18131
+ fileName: safeFileName,
17871
18132
  fileKey: initResult.fileKey,
17872
18133
  fileSize: initResult.fileSize
17873
18134
  });
@@ -17881,7 +18142,7 @@ async function uploadMedia(params) {
17881
18142
  const { uploadId } = initResult;
17882
18143
  let { parts } = initResult;
17883
18144
  const chunkSize = initResult.chunkSize;
17884
- log2.info("upload:init ok", { uploadId, totalChunks: initResult.totalChunks, chunkSize });
18145
+ log4.info("upload:init ok", { uploadId, totalChunks: initResult.totalChunks, chunkSize });
17885
18146
  const chunks = splitBuffer(buffer, chunkSize);
17886
18147
  const completedParts = [];
17887
18148
  for (let i = 0; i < chunks.length; i++) {
@@ -17891,11 +18152,11 @@ async function uploadMedia(params) {
17891
18152
  throw new FileServiceError({
17892
18153
  message: `\u5206\u7247 ${partNumber} \u7684\u7B7E\u540D URL \u672A\u627E\u5230`,
17893
18154
  code: -1,
17894
- operationContext: `uploadMedia(${fileName}), chunk ${partNumber}/${chunks.length}`
18155
+ operationContext: `uploadMedia(${safeFileName}), chunk ${partNumber}/${chunks.length}`
17895
18156
  });
17896
18157
  }
17897
18158
  if (isPartExpired(part)) {
17898
- log2.info("upload:refresh", { uploadId, partNumber });
18159
+ log4.info("upload:refresh", { uploadId, partNumber });
17899
18160
  const remaining = Array.from(
17900
18161
  { length: chunks.length - i },
17901
18162
  (_, j) => partNumber + j
@@ -17906,19 +18167,19 @@ async function uploadMedia(params) {
17906
18167
  return refreshed || p;
17907
18168
  });
17908
18169
  part = parts.find((p) => p.partNumber === partNumber);
17909
- log2.info("upload:refresh ok", { uploadId, refreshedParts: refreshResult.parts.length });
18170
+ log4.info("upload:refresh ok", { uploadId, refreshedParts: refreshResult.parts.length });
17910
18171
  }
17911
- const ctx = `uploadMedia(${fileName}), chunk ${partNumber}/${chunks.length}`;
18172
+ const ctx = `uploadMedia(${safeFileName}), chunk ${partNumber}/${chunks.length}`;
17912
18173
  const { etag, size } = await uploadChunk(client, part.uploadUrl, chunks[i], ctx);
17913
18174
  completedParts.push({ partNumber, etag, size });
17914
- log2.debug("upload:chunk ok", { uploadId, partNumber, etag: etag.slice(0, 16) });
18175
+ log4.debug("upload:chunk ok", { uploadId, partNumber, etag: etag.slice(0, 16) });
17915
18176
  }
17916
- log2.info("upload:complete", { uploadId, totalParts: completedParts.length });
18177
+ log4.info("upload:complete", { uploadId, totalParts: completedParts.length });
17917
18178
  const result = await completeUpload(client, uploadId, {
17918
18179
  parts: completedParts,
17919
18180
  fileHash
17920
18181
  });
17921
- log2.info("upload:done", {
18182
+ log4.info("upload:done", {
17922
18183
  uploadId,
17923
18184
  fileKey: result.fileKey,
17924
18185
  fileSize: result.fileSize
@@ -17927,7 +18188,7 @@ async function uploadMedia(params) {
17927
18188
  }
17928
18189
 
17929
18190
  // src/file/media.ts
17930
- var log3 = createLogger("file/media");
18191
+ var log5 = createLogger("file/media");
17931
18192
  function validateLocalPath(filePath, allowedRoots) {
17932
18193
  if (!allowedRoots || allowedRoots.length === 0) {
17933
18194
  throw new FileServiceError({
@@ -17936,10 +18197,10 @@ function validateLocalPath(filePath, allowedRoots) {
17936
18197
  operationContext: `validateLocalPath(${filePath})`
17937
18198
  });
17938
18199
  }
17939
- const resolved = path.resolve(filePath);
18200
+ const resolved = path2.resolve(filePath);
17940
18201
  const isAllowed = allowedRoots.some((root) => {
17941
- const resolvedRoot = path.resolve(root);
17942
- return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep);
18202
+ const resolvedRoot = path2.resolve(root);
18203
+ return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path2.sep);
17943
18204
  });
17944
18205
  if (!isAllowed) {
17945
18206
  throw new FileServiceError({
@@ -17982,7 +18243,7 @@ var VIDEO_EXTS = /* @__PURE__ */ new Set([
17982
18243
  ".m4v"
17983
18244
  ]);
17984
18245
  function detectFileType(fileName) {
17985
- const ext = path.extname(fileName).toLowerCase();
18246
+ const ext = path2.extname(fileName).toLowerCase();
17986
18247
  if (IMAGE_EXTS.has(ext)) return "image";
17987
18248
  if (AUDIO_EXTS.has(ext)) return "voice";
17988
18249
  if (VIDEO_EXTS.has(ext)) return "video";
@@ -18035,7 +18296,7 @@ var MIME_MAP = {
18035
18296
  ".gz": "application/gzip"
18036
18297
  };
18037
18298
  function inferMimeType(fileName) {
18038
- const ext = path.extname(fileName).toLowerCase();
18299
+ const ext = path2.extname(fileName).toLowerCase();
18039
18300
  return MIME_MAP[ext] || "application/octet-stream";
18040
18301
  }
18041
18302
  function getImageDimensions(buffer) {
@@ -18114,13 +18375,13 @@ async function resolveAndUploadMedia(params) {
18114
18375
  } = params;
18115
18376
  try {
18116
18377
  let buffer;
18117
- let fileName = params.fileName || "unnamed_file";
18378
+ let fileName = sanitizeFileName(params.fileName || "unnamed_file");
18118
18379
  if (mediaBuffer) {
18119
18380
  buffer = mediaBuffer;
18120
- log3.info("media:source=buffer", { fileName, size: buffer.length });
18381
+ log5.info("media:source=buffer", { fileName, size: buffer.length });
18121
18382
  } else if (mediaUrl) {
18122
18383
  if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
18123
- log3.info("media:source=remote", { url: mediaUrl.slice(0, 80) });
18384
+ log5.info("media:source=remote", { url: mediaUrl.slice(0, 80) });
18124
18385
  const fetched = await sdkRuntime.channel.media.fetchRemoteMedia({
18125
18386
  url: mediaUrl,
18126
18387
  ssrfPolicy: { allowPrivateNetwork }
@@ -18131,11 +18392,11 @@ async function resolveAndUploadMedia(params) {
18131
18392
  }
18132
18393
  } else {
18133
18394
  const filePath = mediaUrl.startsWith("file://") ? mediaUrl.slice(7) : mediaUrl;
18134
- log3.info("media:source=local", { filePath });
18395
+ log5.info("media:source=local", { filePath });
18135
18396
  validateLocalPath(filePath, allowedLocalRoots);
18136
18397
  buffer = await fs.readFile(filePath);
18137
18398
  if (!params.fileName) {
18138
- fileName = path.basename(filePath);
18399
+ fileName = path2.basename(filePath);
18139
18400
  }
18140
18401
  }
18141
18402
  } else {
@@ -18147,13 +18408,13 @@ async function resolveAndUploadMedia(params) {
18147
18408
  }
18148
18409
  const fileType = detectFileType(fileName);
18149
18410
  const mimeType = inferMimeType(fileName);
18150
- const ext = path.extname(fileName).replace(".", "").toLowerCase();
18411
+ const ext = path2.extname(fileName).replace(".", "").toLowerCase();
18151
18412
  let imageDimensions;
18152
18413
  if (fileType === "image") {
18153
18414
  imageDimensions = getImageDimensions(buffer);
18154
- log3.info("media:imageDimensions", { fileName, ...imageDimensions });
18415
+ log5.info("media:imageDimensions", { fileName, ...imageDimensions });
18155
18416
  }
18156
- log3.info("media:fileType", { fileName, fileType, mimeType, ext });
18417
+ log5.info("media:fileType", { fileName, fileType, mimeType, ext });
18157
18418
  const originalFileSize = buffer.length;
18158
18419
  let encryptionMeta;
18159
18420
  if (!params.skipEncrypt) {
@@ -18162,9 +18423,9 @@ async function resolveAndUploadMedia(params) {
18162
18423
  const result = await messagePipe.encryptFile(buffer);
18163
18424
  buffer = result.encryptedBuffer;
18164
18425
  encryptionMeta = { keyId: result.keyId, iv: result.iv };
18165
- log3.info("media:encrypted", { fileName, originalSize, encryptedSize: buffer.length });
18426
+ log5.info("media:encrypted", { fileName, originalSize, encryptedSize: buffer.length });
18166
18427
  } catch (err) {
18167
- log3.error("media:encrypt failed", { fileName, error: err.message });
18428
+ log5.error("media:encrypt failed", { fileName, error: err.message });
18168
18429
  throw err;
18169
18430
  }
18170
18431
  }
@@ -18181,7 +18442,7 @@ async function resolveAndUploadMedia(params) {
18181
18442
  timeoutMs
18182
18443
  });
18183
18444
  } catch (err) {
18184
- log3.warn("media:upload failed, \u964D\u7EA7\u5904\u7406", { error: err.message });
18445
+ log5.warn("media:upload failed, \u964D\u7EA7\u5904\u7406", { error: err.message });
18185
18446
  return {
18186
18447
  channel: CHANNEL_ID,
18187
18448
  messageId: "",
@@ -18214,7 +18475,7 @@ async function resolveAndUploadMedia(params) {
18214
18475
  encryptionMeta
18215
18476
  // 文件加密的 keyId/iv → sendMessage 用于构建 extra
18216
18477
  });
18217
- log3.info("media:sent", {
18478
+ log5.info("media:sent", {
18218
18479
  chatId,
18219
18480
  fileId: uploadResult.fileKey,
18220
18481
  msgType,
@@ -18237,17 +18498,134 @@ async function resolveAndUploadMedia(params) {
18237
18498
  function inferFileNameFromUrl(url2) {
18238
18499
  try {
18239
18500
  const pathname = new URL(url2).pathname;
18240
- const basename2 = path.basename(pathname);
18241
- return basename2 || "downloaded_file";
18501
+ const basename3 = path2.basename(pathname);
18502
+ return basename3 || "downloaded_file";
18242
18503
  } catch {
18243
18504
  return "downloaded_file";
18244
18505
  }
18245
18506
  }
18246
18507
 
18508
+ // src/utils/ttl-map.ts
18509
+ var log6 = createLogger("utils/ttl-map");
18510
+ var TTLMap = class {
18511
+ store = /* @__PURE__ */ new Map();
18512
+ maxSize;
18513
+ ttlMs;
18514
+ name;
18515
+ sweepTimer = null;
18516
+ evictedCount = 0;
18517
+ expiredCount = 0;
18518
+ constructor(options) {
18519
+ this.maxSize = options.maxSize;
18520
+ this.ttlMs = options.ttlMs;
18521
+ this.name = options.name ?? "TTLMap";
18522
+ const sweepMs = options.sweepIntervalMs ?? 6e4;
18523
+ this.sweepTimer = setInterval(() => this.sweep(), sweepMs);
18524
+ if (this.sweepTimer && typeof this.sweepTimer === "object" && "unref" in this.sweepTimer) {
18525
+ this.sweepTimer.unref();
18526
+ }
18527
+ }
18528
+ /** 设置键值对 */
18529
+ set(key, value) {
18530
+ if (this.store.has(key)) {
18531
+ this.store.delete(key);
18532
+ }
18533
+ while (this.store.size >= this.maxSize) {
18534
+ const firstKey = this.store.keys().next().value;
18535
+ if (firstKey !== void 0) {
18536
+ this.store.delete(firstKey);
18537
+ this.evictedCount++;
18538
+ } else {
18539
+ break;
18540
+ }
18541
+ }
18542
+ this.store.set(key, { value, createdAt: Date.now() });
18543
+ return this;
18544
+ }
18545
+ /** 获取值 (懒清理: 过期的条目在访问时删除) */
18546
+ get(key) {
18547
+ const entry = this.store.get(key);
18548
+ if (!entry) return void 0;
18549
+ if (Date.now() - entry.createdAt > this.ttlMs) {
18550
+ this.store.delete(key);
18551
+ this.expiredCount++;
18552
+ return void 0;
18553
+ }
18554
+ return entry.value;
18555
+ }
18556
+ /** 检查是否存在 (含过期检查) */
18557
+ has(key) {
18558
+ return this.get(key) !== void 0;
18559
+ }
18560
+ /** 删除条目 */
18561
+ delete(key) {
18562
+ return this.store.delete(key);
18563
+ }
18564
+ /** 当前有效条目数 */
18565
+ get size() {
18566
+ return this.store.size;
18567
+ }
18568
+ /** 清除所有条目 */
18569
+ clear() {
18570
+ this.store.clear();
18571
+ }
18572
+ /**
18573
+ * 周期性扫描 — 从最旧的条目开始遍历,删除过期项。
18574
+ * 利用 Map 的插入顺序特性:遇到第一个未过期的就停止。
18575
+ */
18576
+ sweep() {
18577
+ const now = Date.now();
18578
+ let swept = 0;
18579
+ for (const [key, entry] of this.store) {
18580
+ if (now - entry.createdAt > this.ttlMs) {
18581
+ this.store.delete(key);
18582
+ this.expiredCount++;
18583
+ swept++;
18584
+ } else {
18585
+ break;
18586
+ }
18587
+ }
18588
+ if (swept > 0) {
18589
+ log6.debug(`${this.name}: swept ${swept} expired entries`, {
18590
+ remaining: this.store.size,
18591
+ totalEvicted: this.evictedCount,
18592
+ totalExpired: this.expiredCount
18593
+ });
18594
+ }
18595
+ }
18596
+ /** 获取指标快照 */
18597
+ getStats() {
18598
+ return {
18599
+ size: this.store.size,
18600
+ evicted: this.evictedCount,
18601
+ expired: this.expiredCount
18602
+ };
18603
+ }
18604
+ /** 停止周期性清理 (销毁时调用) */
18605
+ destroy() {
18606
+ if (this.sweepTimer) {
18607
+ clearInterval(this.sweepTimer);
18608
+ this.sweepTimer = null;
18609
+ }
18610
+ }
18611
+ };
18612
+
18247
18613
  // src/channel/outbound.ts
18248
- var log4 = createLogger("channel/outbound");
18249
- var messageEncryptionStatus = /* @__PURE__ */ new Map();
18250
- var activeChatMessage = /* @__PURE__ */ new Map();
18614
+ var log7 = createLogger("channel/outbound");
18615
+ var messageEncryptionStatus = new TTLMap({
18616
+ maxSize: 1e4,
18617
+ ttlMs: 5 * 60 * 1e3,
18618
+ // 5 分钟
18619
+ sweepIntervalMs: 6e4,
18620
+ // 每分钟清理
18621
+ name: "messageEncryptionStatus"
18622
+ });
18623
+ var activeChatMessage = new TTLMap({
18624
+ maxSize: 1e4,
18625
+ ttlMs: 5 * 60 * 1e3,
18626
+ sweepIntervalMs: 6e4,
18627
+ name: "activeChatMessage"
18628
+ });
18251
18629
  function setInboundEncryptionStatus(chatId, messageId, isEncrypted) {
18252
18630
  messageEncryptionStatus.set(messageId, isEncrypted);
18253
18631
  activeChatMessage.set(chatId, messageId);
@@ -18258,6 +18636,10 @@ function clearInboundEncryptionStatus(chatId, messageId) {
18258
18636
  activeChatMessage.delete(chatId);
18259
18637
  }
18260
18638
  }
18639
+ function destroyEncryptionMaps() {
18640
+ messageEncryptionStatus.destroy();
18641
+ activeChatMessage.destroy();
18642
+ }
18261
18643
  function getOutboundEncryptionStatus(chatId) {
18262
18644
  const messageId = activeChatMessage.get(chatId);
18263
18645
  if (!messageId) return void 0;
@@ -18288,9 +18670,9 @@ var quantumImOutbound = {
18288
18670
  textChunkLimit: 1e4,
18289
18671
  /** 发送文本消息 — 通过 messagePipe 发送 */
18290
18672
  sendText: async ({ cfg, to, text, accountId }) => {
18291
- log4.info("sendText called", { to, textLength: text.length });
18673
+ log7.info("sendText called", { to, textLength: text.length });
18292
18674
  if (!messagePipeGetter) {
18293
- log4.error("outbound:sendText failed, messagePipe not initialized");
18675
+ log7.error("outbound:sendText failed, messagePipe not initialized");
18294
18676
  return { channel: CHANNEL_ID, messageId: "", chatId: to };
18295
18677
  }
18296
18678
  const messagePipe = messagePipeGetter();
@@ -18309,9 +18691,9 @@ var quantumImOutbound = {
18309
18691
  },
18310
18692
  /** 发送媒体消息 — SDK fetchRemoteMedia + 三步上传 + 发送 */
18311
18693
  sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId }) => {
18312
- log4.info("sendMedia called", { to, mediaUrl });
18694
+ log7.info("sendMedia called", { to, mediaUrl });
18313
18695
  if (!messagePipeGetter || !sdkRuntimeGetter || !tokenManagerGetter || !pluginConfigGetter) {
18314
- log4.error("outbound:sendMedia failed, dependencies not initialized");
18696
+ log7.error("outbound:sendMedia failed, dependencies not initialized");
18315
18697
  return { channel: CHANNEL_ID, messageId: "", chatId: to };
18316
18698
  }
18317
18699
  const messagePipe = messagePipeGetter();
@@ -18336,14 +18718,14 @@ var quantumImOutbound = {
18336
18718
  skipEncrypt
18337
18719
  });
18338
18720
  if (result.warning) {
18339
- log4.warn("sendMedia:degraded", { chatId, warning: result.warning });
18721
+ log7.warn("sendMedia:degraded", { chatId, warning: result.warning });
18340
18722
  }
18341
18723
  return { channel: CHANNEL_ID, messageId: result.messageId, chatId };
18342
18724
  }
18343
18725
  };
18344
18726
 
18345
18727
  // src/message-handler/parser.ts
18346
- var log5 = createLogger("message-handler/parser");
18728
+ var log8 = createLogger("message-handler/parser");
18347
18729
  function safeParse3(raw) {
18348
18730
  try {
18349
18731
  const parsed = JSON.parse(raw);
@@ -18424,34 +18806,34 @@ ${body}` : body || raw.content;
18424
18806
  }
18425
18807
 
18426
18808
  // src/channel/gate.ts
18427
- var log6 = createLogger("channel/gate");
18809
+ var log9 = createLogger("channel/gate");
18428
18810
  var DEFAULT_MESSAGE_EXPIRY_MS = 30 * 60 * 1e3;
18429
18811
  function checkMessageGate(msg, config2) {
18430
18812
  if (msg.senderId === config2.botUserId) {
18431
- log6.info("Gate: anti-loop, discarding self message", { messageId: msg.messageId });
18813
+ log9.info("Gate: anti-loop, discarding self message", { messageId: msg.messageId });
18432
18814
  return { pass: false, reason: "anti-loop: message from self" };
18433
18815
  }
18434
18816
  const expiryMs = config2.messageExpiryMs ?? DEFAULT_MESSAGE_EXPIRY_MS;
18435
18817
  const age = Date.now() - msg.timestamp;
18436
18818
  if (age > expiryMs) {
18437
- log6.info("Gate: message expired", { messageId: msg.messageId, ageMs: age });
18819
+ log9.info("Gate: message expired", { messageId: msg.messageId, ageMs: age });
18438
18820
  return { pass: false, reason: `message expired: age=${age}ms > ${expiryMs}ms` };
18439
18821
  }
18440
18822
  if (config2.allowFrom && config2.allowFrom.length > 0) {
18441
18823
  if (!config2.allowFrom.includes(msg.senderId)) {
18442
- log6.info("Gate: sender not in allowlist", { messageId: msg.messageId, senderId: msg.senderId });
18824
+ log9.info("Gate: sender not in allowlist", { messageId: msg.messageId, senderId: msg.senderId });
18443
18825
  return { pass: false, reason: "sender not in allowlist" };
18444
18826
  }
18445
18827
  }
18446
18828
  if (msg.msgType === "system") {
18447
- log6.info("Gate: skipping system message", { messageId: msg.messageId });
18829
+ log9.info("Gate: skipping system message", { messageId: msg.messageId });
18448
18830
  return { pass: false, reason: "system message skipped" };
18449
18831
  }
18450
18832
  return { pass: true };
18451
18833
  }
18452
18834
 
18453
18835
  // src/message-handler/abort-detect.ts
18454
- var log7 = createLogger("message-handler/abort-detect");
18836
+ var log10 = createLogger("message-handler/abort-detect");
18455
18837
  var DEFAULT_ABORT_TRIGGERS = [
18456
18838
  "\u505C\u6B62",
18457
18839
  "\u53D6\u6D88",
@@ -18470,7 +18852,7 @@ function detectAbort(content, triggers) {
18470
18852
  }
18471
18853
 
18472
18854
  // src/file/download.ts
18473
- var log8 = createLogger("file/download");
18855
+ var log11 = createLogger("file/download");
18474
18856
  function inferResourceType(mimeType) {
18475
18857
  if (mimeType.startsWith("image/")) return "image";
18476
18858
  if (mimeType.startsWith("audio/")) return "voice";
@@ -18488,7 +18870,7 @@ async function resolveMedia(params) {
18488
18870
  timeoutMs
18489
18871
  } = params;
18490
18872
  const client = createHttpClient({ baseUrl: serverUrl, tokenManager, timeoutMs });
18491
- log8.info("download:getUrl", { fileId });
18873
+ log11.info("download:getUrl", { fileId });
18492
18874
  const downloadInfo = await getDownloadUrl(client, fileId);
18493
18875
  if (downloadInfo.fileSize > maxBytes) {
18494
18876
  throw new FileServiceError({
@@ -18497,7 +18879,7 @@ async function resolveMedia(params) {
18497
18879
  operationContext: `resolveMedia(${fileId})`
18498
18880
  });
18499
18881
  }
18500
- log8.info("download:fetch", { fileId, fileUrl: downloadInfo.fileUrl.slice(0, 80) });
18882
+ log11.info("download:fetch", { fileId, fileUrl: downloadInfo.fileUrl.slice(0, 80) });
18501
18883
  const fetched = await sdkRuntime.channel.media.fetchRemoteMedia({
18502
18884
  url: downloadInfo.fileUrl,
18503
18885
  ssrfPolicy: { allowPrivateNetwork }
@@ -18511,7 +18893,7 @@ async function resolveMedia(params) {
18511
18893
  downloadInfo.fileName
18512
18894
  );
18513
18895
  const resourceType = inferResourceType(saved.contentType || contentType);
18514
- log8.info("download:done", {
18896
+ log11.info("download:done", {
18515
18897
  fileId,
18516
18898
  localPath: saved.path,
18517
18899
  contentType: saved.contentType,
@@ -18528,7 +18910,7 @@ async function resolveMedia(params) {
18528
18910
  }
18529
18911
 
18530
18912
  // src/message-handler/content-resolver.ts
18531
- var log9 = createLogger("message-handler/content-resolver");
18913
+ var log12 = createLogger("message-handler/content-resolver");
18532
18914
  async function resolveContent(context, deps) {
18533
18915
  if (!context.fileId) {
18534
18916
  return {
@@ -18537,7 +18919,7 @@ async function resolveContent(context, deps) {
18537
18919
  };
18538
18920
  }
18539
18921
  try {
18540
- log9.info("resolveContent:downloading", {
18922
+ log12.info("resolveContent:downloading", {
18541
18923
  fileId: context.fileId,
18542
18924
  fileName: context.fileName
18543
18925
  });
@@ -18550,7 +18932,7 @@ async function resolveContent(context, deps) {
18550
18932
  allowPrivateNetwork: deps.allowPrivateNetwork,
18551
18933
  timeoutMs: deps.timeoutMs
18552
18934
  });
18553
- log9.info("resolveContent:downloaded", {
18935
+ log12.info("resolveContent:downloaded", {
18554
18936
  fileId: context.fileId,
18555
18937
  localPath: resolved.localPath,
18556
18938
  resourceType: resolved.resourceType
@@ -18562,7 +18944,7 @@ async function resolveContent(context, deps) {
18562
18944
  mediaResourceType: resolved.resourceType
18563
18945
  };
18564
18946
  } catch (err) {
18565
- log9.warn("resolveContent:download failed", {
18947
+ log12.warn("resolveContent:download failed", {
18566
18948
  fileId: context.fileId,
18567
18949
  error: err.message
18568
18950
  });
@@ -18574,7 +18956,7 @@ async function resolveContent(context, deps) {
18574
18956
  }
18575
18957
 
18576
18958
  // src/message-handler/envelope-builder.ts
18577
- var log10 = createLogger("message-handler/envelope-builder");
18959
+ var log13 = createLogger("message-handler/envelope-builder");
18578
18960
  function buildInboundPayload(msg, resolvedContent, config2) {
18579
18961
  const text = resolvedContent.text;
18580
18962
  const payload = {
@@ -18610,7 +18992,7 @@ function buildInboundPayload(msg, resolvedContent, config2) {
18610
18992
  if (resolvedContent.mediaLocalPath) {
18611
18993
  payload.MediaPath = resolvedContent.mediaLocalPath;
18612
18994
  }
18613
- log10.debug("envelope:built", {
18995
+ log13.debug("envelope:built", {
18614
18996
  messageId: msg.messageId,
18615
18997
  chatId: msg.chatId,
18616
18998
  sessionKey: msg.chatId
@@ -18619,7 +19001,7 @@ function buildInboundPayload(msg, resolvedContent, config2) {
18619
19001
  }
18620
19002
 
18621
19003
  // src/reply-dispatcher/dispatcher.ts
18622
- var log11 = createLogger("reply-dispatcher/dispatcher");
19004
+ var log14 = createLogger("reply-dispatcher/dispatcher");
18623
19005
  function createQuantumImDeliverFn(deps) {
18624
19006
  const {
18625
19007
  messagePipe,
@@ -18657,7 +19039,7 @@ function createQuantumImDeliverFn(deps) {
18657
19039
  replyToMessageId,
18658
19040
  skipEncrypt: shouldSkipEncrypt
18659
19041
  });
18660
- log11.info("\u{1F4E4} AI \u6587\u672C\u56DE\u590D\u5DF2\u53D1\u9001", {
19042
+ log14.info("\u{1F4E4} AI \u6587\u672C\u56DE\u590D\u5DF2\u53D1\u9001", {
18661
19043
  chatId,
18662
19044
  \u957F\u5EA6: payload.text.length,
18663
19045
  \u56DE\u590D\u9884\u89C8: payload.text.slice(0, 200)
@@ -18681,12 +19063,12 @@ function createQuantumImDeliverFn(deps) {
18681
19063
  skipEncrypt: shouldSkipEncrypt
18682
19064
  });
18683
19065
  if (result.warning) {
18684
- log11.warn("\u{1F4E4} \u5A92\u4F53\u53D1\u9001\u964D\u7EA7", { chatId, mediaUrl, warning: result.warning });
19066
+ log14.warn("\u{1F4E4} \u5A92\u4F53\u53D1\u9001\u964D\u7EA7", { chatId, mediaUrl, warning: result.warning });
18685
19067
  } else {
18686
- log11.info("\u{1F4E4} \u5A92\u4F53\u6D88\u606F\u5DF2\u53D1\u9001", { chatId, mediaUrl });
19068
+ log14.info("\u{1F4E4} \u5A92\u4F53\u6D88\u606F\u5DF2\u53D1\u9001", { chatId, mediaUrl });
18687
19069
  }
18688
19070
  } catch (err) {
18689
- log11.error("\u{1F4E4} \u5A92\u4F53\u53D1\u9001\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u6587\u672C\u94FE\u63A5", {
19071
+ log14.error("\u{1F4E4} \u5A92\u4F53\u53D1\u9001\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u6587\u672C\u94FE\u63A5", {
18690
19072
  chatId,
18691
19073
  mediaUrl,
18692
19074
  error: err.message
@@ -18719,21 +19101,32 @@ function getPluginRuntime() {
18719
19101
  }
18720
19102
 
18721
19103
  // src/message-handler/handler.ts
18722
- var log12 = createLogger("message-handler/handler");
19104
+ var log15 = createLogger("message-handler/handler");
18723
19105
  var InboundPipeline = class {
18724
19106
  deps;
18725
19107
  constructor(deps) {
18726
19108
  this.deps = deps;
18727
- log12.info("InboundPipeline initialized");
19109
+ log15.info("InboundPipeline initialized");
18728
19110
  }
18729
- /** 处理单条入站消息 — 执行完整的处理流水线 */
19111
+ /** 处理单条入站消息 — 获取 Semaphore slot 后执行完整的处理流水线 */
18730
19112
  async handle(msg) {
18731
19113
  const startMs = Date.now();
19114
+ const semaphore = this.deps.messagePipe.semaphore;
19115
+ const acquireResult = await semaphore.acquire();
19116
+ if (acquireResult === "rejected") {
19117
+ metrics.increment("inbound.semaphore_rejected");
19118
+ log15.warn("\u{1F6AB} \u6D88\u606F\u88AB\u62D2\u7EDD (Semaphore \u6EA2\u51FA)", {
19119
+ messageId: msg.messageId,
19120
+ active: semaphore.activeCount,
19121
+ waiting: semaphore.waitingCount
19122
+ });
19123
+ return;
19124
+ }
18732
19125
  try {
18733
19126
  const context = parseMessage(msg);
18734
19127
  const gateResult = checkMessageGate(msg, this.deps.gateConfig);
18735
19128
  if (!gateResult.pass) {
18736
- log12.info("\u{1F6AB} \u6D88\u606F\u88AB\u62E6\u622A (gate)", {
19129
+ log15.info("\u{1F6AB} \u6D88\u606F\u88AB\u62E6\u622A (gate)", {
18737
19130
  messageId: msg.messageId,
18738
19131
  reason: gateResult.reason
18739
19132
  });
@@ -18741,7 +19134,7 @@ var InboundPipeline = class {
18741
19134
  }
18742
19135
  setInboundEncryptionStatus(msg.chatId, msg.messageId, msg.isEncrypted ?? false);
18743
19136
  if (detectAbort(context.text)) {
18744
- log12.info("\u26D4 \u68C0\u6D4B\u5230\u4E2D\u6B62\u6307\u4EE4", {
19137
+ log15.info("\u26D4 \u68C0\u6D4B\u5230\u4E2D\u6B62\u6307\u4EE4", {
18745
19138
  messageId: msg.messageId,
18746
19139
  chatId: msg.chatId
18747
19140
  });
@@ -18750,7 +19143,7 @@ var InboundPipeline = class {
18750
19143
  const core = getPluginRuntime();
18751
19144
  const isCommand = core.channel.commands?.isControlCommandMessage?.(context.text, this.deps.sdkConfig);
18752
19145
  if (isCommand) {
18753
- log12.info("\u{1F527} \u68C0\u6D4B\u5230\u7CFB\u7EDF\u547D\u4EE4", {
19146
+ log15.info("\u{1F527} \u68C0\u6D4B\u5230\u7CFB\u7EDF\u547D\u4EE4", {
18754
19147
  messageId: msg.messageId,
18755
19148
  command: context.text.trim()
18756
19149
  });
@@ -18762,7 +19155,7 @@ var InboundPipeline = class {
18762
19155
  cfg: this.deps.sdkConfig,
18763
19156
  dispatcherOptions: {
18764
19157
  deliver: async (payload2) => {
18765
- log12.debug("\u{1F527} \u547D\u4EE4 deliver \u56DE\u8C03\u6536\u5230", {
19158
+ log15.debug("\u{1F527} \u547D\u4EE4 deliver \u56DE\u8C03\u6536\u5230", {
18766
19159
  messageId: msg.messageId,
18767
19160
  hasText: Boolean(payload2.text),
18768
19161
  textLength: payload2.text?.length ?? 0,
@@ -18780,14 +19173,14 @@ var InboundPipeline = class {
18780
19173
  });
18781
19174
  },
18782
19175
  onError: (err) => {
18783
- log12.error("\u274C \u547D\u4EE4\u56DE\u590D\u53D1\u9001\u5931\u8D25", { error: String(err) });
19176
+ log15.error("\u274C \u547D\u4EE4\u56DE\u590D\u53D1\u9001\u5931\u8D25", { error: String(err) });
18784
19177
  }
18785
19178
  },
18786
19179
  replyOptions: {}
18787
19180
  });
18788
19181
  clearInboundEncryptionStatus(msg.chatId, msg.messageId);
18789
19182
  const durationMs2 = Date.now() - startMs;
18790
- log12.info("\u2705 \u7CFB\u7EDF\u547D\u4EE4\u5904\u7406\u5B8C\u6210", {
19183
+ log15.info("\u2705 \u7CFB\u7EDF\u547D\u4EE4\u5904\u7406\u5B8C\u6210", {
18791
19184
  messageId: msg.messageId,
18792
19185
  command: context.text.trim(),
18793
19186
  \u8017\u65F6ms: durationMs2
@@ -18802,7 +19195,7 @@ var InboundPipeline = class {
18802
19195
  content: JSON.stringify({ content: "\u4EFB\u52A1\u5DF2\u6536\u5230\u{1F44C}\uFF0C\u6B63\u5728\u5904\u7406\u4E2D..." }),
18803
19196
  skipEncrypt: feedbackSkipEncrypt
18804
19197
  }).catch((err) => {
18805
- log12.warn("\u26A0\uFE0F \u5373\u65F6\u53CD\u9988\u53D1\u9001\u5931\u8D25", { error: err.message });
19198
+ log15.warn("\u26A0\uFE0F \u5373\u65F6\u53CD\u9988\u53D1\u9001\u5931\u8D25", { error: err.message });
18806
19199
  });
18807
19200
  const resolvedContent = await resolveContent(context, {
18808
19201
  tokenManager: this.deps.tokenManager,
@@ -18834,7 +19227,7 @@ var InboundPipeline = class {
18834
19227
  deliver
18835
19228
  // TODO: 可选 typingCallbacks (打字指示器)
18836
19229
  });
18837
- log12.info("\u{1F4E5} \u6536\u5230\u6D88\u606F \u2192 \u53D1\u7ED9 AI", {
19230
+ log15.info("\u{1F4E5} \u6536\u5230\u6D88\u606F \u2192 \u53D1\u7ED9 AI", {
18838
19231
  messageId: msg.messageId,
18839
19232
  chatId: msg.chatId,
18840
19233
  \u5185\u5BB9\u9884\u89C8: String(payload.Body ?? "").slice(0, 200)
@@ -18849,7 +19242,7 @@ var InboundPipeline = class {
18849
19242
  await dispatcher.waitForIdle();
18850
19243
  }
18851
19244
  const durationMs = Date.now() - startMs;
18852
- log12.info("\u2705 \u6D88\u606F\u5904\u7406\u5B8C\u6210", {
19245
+ log15.info("\u2705 \u6D88\u606F\u5904\u7406\u5B8C\u6210", {
18853
19246
  messageId: msg.messageId,
18854
19247
  chatId: msg.chatId,
18855
19248
  \u56DE\u590D\u6570: counts?.final ?? 0,
@@ -18858,7 +19251,7 @@ var InboundPipeline = class {
18858
19251
  clearInboundEncryptionStatus(msg.chatId, msg.messageId);
18859
19252
  } catch (err) {
18860
19253
  const durationMs = Date.now() - startMs;
18861
- log12.error("\u274C \u6D88\u606F\u5904\u7406\u5931\u8D25", {
19254
+ log15.error("\u274C \u6D88\u606F\u5904\u7406\u5931\u8D25", {
18862
19255
  messageId: msg.messageId,
18863
19256
  chatId: msg.chatId,
18864
19257
  step: "handle",
@@ -18867,6 +19260,10 @@ var InboundPipeline = class {
18867
19260
  durationMs
18868
19261
  });
18869
19262
  clearInboundEncryptionStatus(msg.chatId, msg.messageId);
19263
+ } finally {
19264
+ semaphore.release();
19265
+ metrics.increment("inbound.total");
19266
+ metrics.recordLatency("inbound.latency", Date.now() - startMs);
18870
19267
  }
18871
19268
  }
18872
19269
  };
@@ -19100,7 +19497,7 @@ var quantumImOnboarding = {
19100
19497
  };
19101
19498
 
19102
19499
  // src/channel/plugin.ts
19103
- var log13 = createLogger("channel/plugin");
19500
+ var log16 = createLogger("channel/plugin");
19104
19501
  var meta3 = {
19105
19502
  id: CHANNEL_ID,
19106
19503
  label: "QuantumIM",
@@ -19268,7 +19665,7 @@ var quantumImPlugin = {
19268
19665
  gateway: {
19269
19666
  /** 启动账户 — 初始化所有模块并开始接收消息 */
19270
19667
  startAccount: async (ctx) => {
19271
- log13.info(`starting liangzimixin[${ctx.accountId}]`);
19668
+ log16.info(`starting liangzimixin[${ctx.accountId}]`);
19272
19669
  try {
19273
19670
  const resolved = resolveAccount(ctx.cfg, ctx.accountId);
19274
19671
  if (!resolved.credentials) {
@@ -19298,16 +19695,16 @@ var quantumImPlugin = {
19298
19695
  setSdkRuntimeGetter(() => getPluginRuntime());
19299
19696
  setTokenManagerGetter(() => instance.tokenManager);
19300
19697
  setPluginConfigGetter(() => instance.config);
19301
- log13.info(`liangzimixin[${ctx.accountId}] started \u2713`);
19698
+ log16.info(`liangzimixin[${ctx.accountId}] started \u2713`);
19302
19699
  await new Promise((resolve3) => {
19303
19700
  if (ctx.abortSignal?.aborted) {
19304
19701
  resolve3();
19305
19702
  return;
19306
19703
  }
19307
19704
  const onAbort = () => {
19308
- log13.info(`liangzimixin[${ctx.accountId}] received abort signal, shutting down`);
19705
+ log16.info(`liangzimixin[${ctx.accountId}] received abort signal, shutting down`);
19309
19706
  ctx.abortSignal?.removeEventListener("abort", onAbort);
19310
- instance.shutdown().catch((err) => log13.error("Shutdown error", { error: err.message })).finally(() => {
19707
+ instance.shutdown().catch((err) => log16.error("Shutdown error", { error: err.message })).finally(() => {
19311
19708
  activeInstance = null;
19312
19709
  resolve3();
19313
19710
  });
@@ -19315,7 +19712,7 @@ var quantumImPlugin = {
19315
19712
  ctx.abortSignal?.addEventListener("abort", onAbort);
19316
19713
  });
19317
19714
  } catch (err) {
19318
- log13.error(`liangzimixin[${ctx.accountId}] start failed`, {
19715
+ log16.error(`liangzimixin[${ctx.accountId}] start failed`, {
19319
19716
  error: err.message
19320
19717
  });
19321
19718
  throw err;
@@ -19323,12 +19720,12 @@ var quantumImPlugin = {
19323
19720
  },
19324
19721
  /** 停止账户 — 优雅关闭所有模块 */
19325
19722
  stopAccount: async (ctx) => {
19326
- log13.info(`stopping liangzimixin[${ctx.accountId}]`);
19723
+ log16.info(`stopping liangzimixin[${ctx.accountId}]`);
19327
19724
  if (activeInstance) {
19328
19725
  await activeInstance.shutdown();
19329
19726
  activeInstance = null;
19330
19727
  }
19331
- log13.info(`stopped liangzimixin[${ctx.accountId}]`);
19728
+ log16.info(`stopped liangzimixin[${ctx.accountId}]`);
19332
19729
  }
19333
19730
  }
19334
19731
  };
@@ -19336,7 +19733,7 @@ var quantumImPlugin = {
19336
19733
  // src/crypto/quantum-plug.ts
19337
19734
  var import_node_fs = require("fs");
19338
19735
  var import_node_path = require("path");
19339
- var log14 = createLogger("crypto/quantum-plug");
19736
+ var log17 = createLogger("crypto/quantum-plug");
19340
19737
  var KMS_URLS = {
19341
19738
  test: "http://223.244.14.238:8552",
19342
19739
  staging: "http://223.244.14.238:8552",
@@ -19350,7 +19747,7 @@ function writeQuantumConfig(config2) {
19350
19747
  const configPath = (0, import_node_path.join)(sdkDir, "quantum.json");
19351
19748
  (0, import_node_fs.mkdirSync)(sdkDir, { recursive: true });
19352
19749
  (0, import_node_fs.writeFileSync)(configPath, JSON.stringify(config2, null, 2), "utf-8");
19353
- log14.info("quantum.json written", { path: configPath });
19750
+ log17.info("quantum.json written", { path: configPath });
19354
19751
  }
19355
19752
  function createQuantumPlug() {
19356
19753
  let sdk = null;
@@ -19359,11 +19756,11 @@ function createQuantumPlug() {
19359
19756
  try {
19360
19757
  const sdkPath = (0, import_node_path.resolve)(__dirname, "quantum-sdk", "index.cjs");
19361
19758
  sdk = require(sdkPath);
19362
- log14.info("Quantum SDK loaded \u2713");
19759
+ log17.info("Quantum SDK loaded \u2713");
19363
19760
  return sdk;
19364
19761
  } catch (err) {
19365
19762
  const msg = err instanceof Error ? err.message : String(err);
19366
- log14.error("Failed to load quantum SDK", { error: msg });
19763
+ log17.error("Failed to load quantum SDK", { error: msg });
19367
19764
  throw new Error(`Quantum SDK load failed: ${msg}`);
19368
19765
  }
19369
19766
  }
@@ -19371,7 +19768,7 @@ function createQuantumPlug() {
19371
19768
  async init() {
19372
19769
  const s = loadSdk();
19373
19770
  await s.init();
19374
- log14.info("Quantum SDK initialized \u2713");
19771
+ log17.info("Quantum SDK initialized \u2713");
19375
19772
  },
19376
19773
  async encrypt(plaintext, iv, mode) {
19377
19774
  const s = loadSdk();
@@ -19406,12 +19803,31 @@ function getKmsUrl(env) {
19406
19803
 
19407
19804
  // src/crypto/crypto-engine.ts
19408
19805
  var import_node_crypto = require("crypto");
19409
- var log15 = createLogger("crypto/crypto-engine");
19806
+ var log18 = createLogger("crypto/crypto-engine");
19410
19807
  var SM4_MODE = 4;
19411
19808
  var PASSTHROUGH_KEY_ID = "passthrough";
19412
19809
  var IV_LENGTH = 8;
19413
19810
  var FILE_CHUNK_SIZE = 10 * 1024 * 1024;
19414
19811
  var FILE_DECRYPT_CHUNK_SIZE = FILE_CHUNK_SIZE + 16;
19812
+ var DEFAULT_OPERATION_TIMEOUT_MS = 1e4;
19813
+ var CryptoTimeoutError = class extends Error {
19814
+ constructor(operation, timeoutMs) {
19815
+ super(`Crypto ${operation} timed out after ${timeoutMs}ms`);
19816
+ this.name = "CryptoTimeoutError";
19817
+ }
19818
+ };
19819
+ function withTimeout(promise2, timeoutMs, operation) {
19820
+ let timer;
19821
+ const timeoutPromise = new Promise((_, reject) => {
19822
+ timer = setTimeout(() => {
19823
+ reject(new CryptoTimeoutError(operation, timeoutMs));
19824
+ }, timeoutMs);
19825
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
19826
+ });
19827
+ return Promise.race([promise2, timeoutPromise]).finally(() => {
19828
+ clearTimeout(timer);
19829
+ });
19830
+ }
19415
19831
  function generateIv() {
19416
19832
  try {
19417
19833
  return (0, import_node_crypto.randomBytes)(IV_LENGTH).toString("hex");
@@ -19433,18 +19849,21 @@ var CryptoEngine = class _CryptoEngine {
19433
19849
  credentials;
19434
19850
  /** 部署环境 */
19435
19851
  env;
19852
+ /** 加密/解密操作超时 (ms) */
19853
+ operationTimeoutMs;
19436
19854
  /**
19437
19855
  * 构造加密引擎 (加密模式)
19438
19856
  *
19439
19857
  * @param credentials - 量子 SDK 所需的凭据
19440
19858
  * @param env - 部署环境 ('test' | 'staging' | 'production')
19441
19859
  */
19442
- constructor(credentials, env = "production") {
19860
+ constructor(credentials, env = "production", operationTimeoutMs) {
19443
19861
  this.plug = quantum_plug_default;
19444
19862
  this.passthrough = false;
19445
19863
  this.credentials = credentials;
19446
19864
  this.env = env;
19447
- log15.info("CryptoEngine created (quantum SDK mode)");
19865
+ this.operationTimeoutMs = operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
19866
+ log18.info("CryptoEngine created (quantum SDK mode)", { operationTimeoutMs: this.operationTimeoutMs });
19448
19867
  }
19449
19868
  /**
19450
19869
  * 创建透传模式的加密引擎 (静态工厂方法)
@@ -19464,7 +19883,8 @@ var CryptoEngine = class _CryptoEngine {
19464
19883
  instance.initialized = true;
19465
19884
  instance.credentials = null;
19466
19885
  instance.env = "production";
19467
- log15.info("CryptoEngine created (passthrough mode \u2014 crypto disabled)");
19886
+ instance.operationTimeoutMs = DEFAULT_OPERATION_TIMEOUT_MS;
19887
+ log18.info("CryptoEngine created (passthrough mode \u2014 crypto disabled)");
19468
19888
  return instance;
19469
19889
  }
19470
19890
  /**
@@ -19494,7 +19914,7 @@ var CryptoEngine = class _CryptoEngine {
19494
19914
  }
19495
19915
  await this.plug.init();
19496
19916
  this.initialized = true;
19497
- log15.info("CryptoEngine initialized \u2713");
19917
+ log18.info("CryptoEngine initialized \u2713");
19498
19918
  }
19499
19919
  /**
19500
19920
  * 加密明文
@@ -19508,12 +19928,18 @@ var CryptoEngine = class _CryptoEngine {
19508
19928
  async encrypt(plaintext) {
19509
19929
  this.ensureInitialized();
19510
19930
  if (this.passthrough) {
19511
- log15.debug("Encrypt (passthrough)", { length: plaintext.length });
19931
+ log18.debug("Encrypt (passthrough)", { length: plaintext.length });
19512
19932
  return { ciphertext: plaintext, keyId: PASSTHROUGH_KEY_ID, iv: "" };
19513
19933
  }
19514
19934
  const iv = generateIv();
19515
- const result = await this.plug.encrypt(plaintext, iv, SM4_MODE);
19516
- log15.debug("Encrypted", { length: plaintext.length, keyId: result.keyId });
19935
+ const startMs = Date.now();
19936
+ const result = await withTimeout(
19937
+ this.plug.encrypt(plaintext, iv, SM4_MODE),
19938
+ this.operationTimeoutMs,
19939
+ "encrypt"
19940
+ );
19941
+ metrics.recordLatency("crypto.encrypt", Date.now() - startMs);
19942
+ log18.debug("Encrypted", { length: plaintext.length, keyId: result.keyId });
19517
19943
  return { ...result, iv };
19518
19944
  }
19519
19945
  /**
@@ -19530,11 +19956,17 @@ var CryptoEngine = class _CryptoEngine {
19530
19956
  async decrypt(ciphertext, keyId, iv = "") {
19531
19957
  this.ensureInitialized();
19532
19958
  if (this.passthrough) {
19533
- log15.debug("Decrypt (passthrough)", { keyId });
19959
+ log18.debug("Decrypt (passthrough)", { keyId });
19534
19960
  return ciphertext;
19535
19961
  }
19536
- const result = await this.plug.decrypt(ciphertext, keyId, iv, SM4_MODE);
19537
- log15.debug("Decrypted", { keyId });
19962
+ const startMs = Date.now();
19963
+ const result = await withTimeout(
19964
+ this.plug.decrypt(ciphertext, keyId, iv, SM4_MODE),
19965
+ this.operationTimeoutMs,
19966
+ "decrypt"
19967
+ );
19968
+ metrics.recordLatency("crypto.decrypt", Date.now() - startMs);
19969
+ log18.debug("Decrypted", { keyId });
19538
19970
  return result.plaintext;
19539
19971
  }
19540
19972
  /**
@@ -19614,7 +20046,7 @@ var CryptoEngine = class _CryptoEngine {
19614
20046
  };
19615
20047
 
19616
20048
  // src/auth/oauth-client.ts
19617
- var log16 = createLogger("auth/oauth-client");
20049
+ var log19 = createLogger("auth/oauth-client");
19618
20050
  var ApiError = class extends Error {
19619
20051
  constructor(status, body, endpoint) {
19620
20052
  super(`API error: ${status} on ${endpoint}`);
@@ -19648,14 +20080,14 @@ var OAuthClient = class {
19648
20080
  this.baseUrl = config2.baseUrl.replace(/\/+$/, "");
19649
20081
  this.appId = config2.appId;
19650
20082
  this.appSecret = config2.appSecret;
19651
- log16.info("OAuthClient initialized", { baseUrl: this.baseUrl, appId: sanitize(this.appId) });
20083
+ log19.info("OAuthClient initialized", { baseUrl: this.baseUrl, appId: sanitize(this.appId) });
19652
20084
  }
19653
20085
  // ── 通用 HTTP 请求 ──
19654
20086
  /**
19655
20087
  * 统一 HTTP 请求封装 — 含超时、日志脱敏和错误处理
19656
20088
  */
19657
- async request(method, path2, options = {}) {
19658
- const url2 = `${this.baseUrl}${path2}`;
20089
+ async request(method, path3, options = {}) {
20090
+ const url2 = `${this.baseUrl}${path3}`;
19659
20091
  const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
19660
20092
  const headers = { ...options.headers };
19661
20093
  let bodyStr;
@@ -19668,7 +20100,7 @@ var OAuthClient = class {
19668
20100
  bodyStr = JSON.stringify(options.body);
19669
20101
  }
19670
20102
  }
19671
- log16.debug("HTTP request", { method, url: url2 });
20103
+ log19.debug("HTTP request", { method, url: url2 });
19672
20104
  const response = await fetch(url2, {
19673
20105
  method,
19674
20106
  headers,
@@ -19677,20 +20109,14 @@ var OAuthClient = class {
19677
20109
  });
19678
20110
  const responseBody = await response.text();
19679
20111
  if (!response.ok) {
19680
- let parsedBody;
19681
- try {
19682
- parsedBody = JSON.parse(responseBody);
19683
- } catch {
19684
- parsedBody = responseBody;
19685
- }
19686
- log16.error("HTTP error", { method, url: url2, status: response.status });
19687
- throw new ApiError(response.status, parsedBody, path2);
20112
+ log19.error("HTTP error", { method, url: url2, status: response.status });
20113
+ throw new ApiError(response.status, void 0, path3);
19688
20114
  }
19689
- log16.debug("HTTP response", { method, url: url2, status: response.status });
20115
+ log19.debug("HTTP response", { method, url: url2, status: response.status });
19690
20116
  try {
19691
20117
  return JSON.parse(responseBody);
19692
20118
  } catch {
19693
- throw new ApiError(response.status, responseBody, path2);
20119
+ throw new ApiError(response.status, responseBody, path3);
19694
20120
  }
19695
20121
  }
19696
20122
  // ── 公共方法 ──
@@ -19712,7 +20138,7 @@ var OAuthClient = class {
19712
20138
  params.set("client_id", this.appId);
19713
20139
  params.set("client_secret", this.appSecret);
19714
20140
  params.set("scope", SCOPE);
19715
- log16.info("Requesting access token", { appId: sanitize(this.appId) });
20141
+ log19.info("Requesting access token", { appId: sanitize(this.appId) });
19716
20142
  const response = await this.request(
19717
20143
  "POST",
19718
20144
  "/auth/token",
@@ -19729,7 +20155,7 @@ var OAuthClient = class {
19729
20155
  grantedAt: now,
19730
20156
  expiresAt: now + expiresIn * 1e3
19731
20157
  };
19732
- log16.info("Access token acquired", {
20158
+ log19.info("Access token acquired", {
19733
20159
  accessToken: sanitize(tokenData.accessToken),
19734
20160
  expiresIn: tokenData.expiresIn,
19735
20161
  scope: tokenData.scope
@@ -19747,7 +20173,7 @@ var import_promises = require("fs/promises");
19747
20173
  var import_node_fs2 = require("fs");
19748
20174
  var import_node_path2 = require("path");
19749
20175
  var import_node_crypto2 = require("crypto");
19750
- var log17 = createLogger("auth/token-store");
20176
+ var log20 = createLogger("auth/token-store");
19751
20177
  var TokenStore = class {
19752
20178
  /** 存储目录路径 */
19753
20179
  storageDir;
@@ -19762,7 +20188,7 @@ var TokenStore = class {
19762
20188
  constructor(storageDir, crypto) {
19763
20189
  this.storageDir = storageDir;
19764
20190
  this.crypto = crypto;
19765
- log17.info("TokenStore initialized", { storageDir });
20191
+ log20.info("TokenStore initialized", { storageDir });
19766
20192
  }
19767
20193
  /**
19768
20194
  * 确保存储目录存在并设置正确的权限。
@@ -19772,7 +20198,7 @@ var TokenStore = class {
19772
20198
  if (this.dirEnsured) return;
19773
20199
  if (!(0, import_node_fs2.existsSync)(this.storageDir)) {
19774
20200
  await (0, import_promises.mkdir)(this.storageDir, { recursive: true, mode: 448 });
19775
- log17.debug("Storage directory created", { dir: this.storageDir });
20201
+ log20.debug("Storage directory created", { dir: this.storageDir });
19776
20202
  }
19777
20203
  this.dirEnsured = true;
19778
20204
  }
@@ -19799,7 +20225,8 @@ var TokenStore = class {
19799
20225
  await this.ensureDir();
19800
20226
  const json2 = JSON.stringify(data);
19801
20227
  const { ciphertext, keyId, iv } = await this.crypto.encrypt(json2);
19802
- const storage = { ciphertext, keyId, iv };
20228
+ const hmac = (0, import_node_crypto2.createHmac)("sha256", keyId).update(ciphertext).digest("hex");
20229
+ const storage = { ciphertext, keyId, iv, hmac };
19803
20230
  const storageJson = JSON.stringify(storage);
19804
20231
  const targetPath = this.filePath(key);
19805
20232
  const tmpSuffix = (0, import_node_crypto2.randomBytes)(4).toString("hex");
@@ -19807,7 +20234,7 @@ var TokenStore = class {
19807
20234
  try {
19808
20235
  await (0, import_promises.writeFile)(tmpPath, storageJson, { mode: 384 });
19809
20236
  await (0, import_promises.rename)(tmpPath, targetPath);
19810
- log17.debug("Token saved", { key, path: targetPath });
20237
+ log20.debug("Token saved", { key, path: targetPath });
19811
20238
  } catch (err) {
19812
20239
  try {
19813
20240
  await (0, import_promises.unlink)(tmpPath);
@@ -19830,18 +20257,33 @@ var TokenStore = class {
19830
20257
  async load(key) {
19831
20258
  const filePath = this.filePath(key);
19832
20259
  if (!(0, import_node_fs2.existsSync)(filePath)) {
19833
- log17.debug("Token file not found", { key, path: filePath });
20260
+ log20.debug("Token file not found", { key, path: filePath });
19834
20261
  return null;
19835
20262
  }
19836
20263
  try {
19837
20264
  const raw = await (0, import_promises.readFile)(filePath, "utf-8");
19838
20265
  const storage = JSON.parse(raw);
20266
+ if (storage.hmac) {
20267
+ const expectedHmac = (0, import_node_crypto2.createHmac)("sha256", storage.keyId).update(storage.ciphertext).digest("hex");
20268
+ if (expectedHmac !== storage.hmac) {
20269
+ log20.warn("SECURITY_AUDIT: token file integrity check failed", {
20270
+ key,
20271
+ path: filePath
20272
+ });
20273
+ return null;
20274
+ }
20275
+ } else {
20276
+ log20.debug("Token file missing HMAC (legacy format), skipping integrity check", {
20277
+ key,
20278
+ path: filePath
20279
+ });
20280
+ }
19839
20281
  const json2 = await this.crypto.decrypt(storage.ciphertext, storage.keyId, storage.iv ?? "");
19840
20282
  const data = JSON.parse(json2);
19841
- log17.debug("Token loaded", { key, path: filePath });
20283
+ log20.debug("Token loaded", { key, path: filePath });
19842
20284
  return data;
19843
20285
  } catch (err) {
19844
- log17.warn("Failed to load token (corrupted or key mismatch), treating as empty", {
20286
+ log20.warn("Failed to load token (corrupted or key mismatch), treating as empty", {
19845
20287
  key,
19846
20288
  error: err instanceof Error ? err.message : String(err)
19847
20289
  });
@@ -19857,7 +20299,7 @@ var TokenStore = class {
19857
20299
  const filePath = this.filePath(key);
19858
20300
  try {
19859
20301
  await (0, import_promises.unlink)(filePath);
19860
- log17.debug("Token file cleared", { key, path: filePath });
20302
+ log20.debug("Token file cleared", { key, path: filePath });
19861
20303
  } catch (err) {
19862
20304
  if (err.code !== "ENOENT") {
19863
20305
  throw err;
@@ -19867,7 +20309,7 @@ var TokenStore = class {
19867
20309
  };
19868
20310
 
19869
20311
  // src/auth/token-manager.ts
19870
- var log18 = createLogger("auth/token-manager");
20312
+ var log21 = createLogger("auth/token-manager");
19871
20313
  var DEFAULT_REFRESH_AHEAD_MS = 5 * 60 * 1e3;
19872
20314
  var MAX_RETRIES = 3;
19873
20315
  var RETRY_BASE_MS = 1e3;
@@ -19892,7 +20334,7 @@ var TokenManager = class {
19892
20334
  this.oauthClient = deps.oauthClient;
19893
20335
  this.tokenStore = deps.tokenStore;
19894
20336
  this.refreshAheadMs = deps.refreshAheadMs ?? DEFAULT_REFRESH_AHEAD_MS;
19895
- log18.info("TokenManager initialized", { refreshAheadMs: this.refreshAheadMs });
20337
+ log21.info("TokenManager initialized", { refreshAheadMs: this.refreshAheadMs });
19896
20338
  }
19897
20339
  // ── 核心方法 ──
19898
20340
  /**
@@ -19910,7 +20352,7 @@ var TokenManager = class {
19910
20352
  return this.cachedToken;
19911
20353
  }
19912
20354
  if (this.refreshLock) {
19913
- log18.debug("Waiting for concurrent token acquisition");
20355
+ log21.debug("Waiting for concurrent token acquisition");
19914
20356
  return this.refreshLock;
19915
20357
  }
19916
20358
  this.refreshLock = this._acquireTokenWithRetry();
@@ -19936,13 +20378,13 @@ var TokenManager = class {
19936
20378
  this.cachedToken = null;
19937
20379
  this.cachedScope = null;
19938
20380
  this.currentTokenData = null;
19939
- log18.info("Token revoked and cleared");
20381
+ log21.info("Token revoked and cleared");
19940
20382
  }
19941
20383
  /** 清理定时器和并发锁 — 优雅关闭时调用 */
19942
20384
  shutdown() {
19943
20385
  this.clearRefreshTimer();
19944
20386
  this.refreshLock = null;
19945
- log18.info("TokenManager shutdown");
20387
+ log21.info("TokenManager shutdown");
19946
20388
  }
19947
20389
  // ── 内部方法 ──
19948
20390
  /**
@@ -19958,18 +20400,18 @@ var TokenManager = class {
19958
20400
  return await this._acquireToken();
19959
20401
  } catch (err) {
19960
20402
  if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
19961
- log18.error("Token acquire failed with auth error, not retrying", { status: err.status });
20403
+ log21.error("Token acquire failed with auth error, not retrying", { status: err.status });
19962
20404
  throw err;
19963
20405
  }
19964
20406
  if (attempt === MAX_RETRIES) {
19965
- log18.error("Token acquire failed after all retries", { attempts: attempt + 1 });
20407
+ log21.error("Token acquire failed after all retries", { attempts: attempt + 1 });
19966
20408
  throw new TokenAcquireError(
19967
20409
  `Token acquire failed after ${attempt + 1} attempts`,
19968
20410
  { cause: err instanceof Error ? err : void 0 }
19969
20411
  );
19970
20412
  }
19971
20413
  const delay = RETRY_BASE_MS * Math.pow(2, attempt);
19972
- log18.warn("Token acquire failed, retrying", {
20414
+ log21.warn("Token acquire failed, retrying", {
19973
20415
  attempt: attempt + 1,
19974
20416
  maxRetries: MAX_RETRIES,
19975
20417
  retryInMs: delay,
@@ -19993,18 +20435,18 @@ var TokenManager = class {
19993
20435
  try {
19994
20436
  const stored = await this.tokenStore.load("token");
19995
20437
  if (stored && !this.isTokenHardExpired(stored)) {
19996
- log18.info("Token loaded from store (still valid)");
20438
+ log21.info("Token loaded from store (still valid)");
19997
20439
  this.updateCache(stored);
19998
20440
  this.scheduleRefresh(stored);
19999
20441
  return stored.accessToken;
20000
20442
  }
20001
20443
  if (stored) {
20002
- log18.info("Stored token has expired, acquiring new one");
20444
+ log21.info("Stored token has expired, acquiring new one");
20003
20445
  }
20004
20446
  } catch {
20005
- log18.warn("Failed to load token from store, acquiring new one");
20447
+ log21.warn("Failed to load token from store, acquiring new one");
20006
20448
  }
20007
- log18.info("Acquiring new access token via POST /auth/token");
20449
+ log21.info("Acquiring new access token via POST /auth/token");
20008
20450
  const tokenData = await this.oauthClient.getToken();
20009
20451
  this.updateCache(tokenData);
20010
20452
  await this.tokenStore.save("token", tokenData);
@@ -20035,22 +20477,22 @@ var TokenManager = class {
20035
20477
  this.clearRefreshTimer();
20036
20478
  const refreshInMs = tokenData.expiresAt - this.refreshAheadMs - Date.now();
20037
20479
  if (refreshInMs <= 0) {
20038
- log18.debug("Token already within refresh window, not scheduling timer");
20480
+ log21.debug("Token already within refresh window, not scheduling timer");
20039
20481
  return;
20040
20482
  }
20041
- log18.debug("Scheduling token refresh", {
20483
+ log21.debug("Scheduling token refresh", {
20042
20484
  refreshInMs,
20043
20485
  refreshAt: new Date(Date.now() + refreshInMs).toISOString()
20044
20486
  });
20045
20487
  this.refreshTimer = setTimeout(async () => {
20046
20488
  try {
20047
- log18.info("Scheduled token refresh triggered");
20489
+ log21.info("Scheduled token refresh triggered");
20048
20490
  this.cachedToken = null;
20049
20491
  this.currentTokenData = null;
20050
20492
  await this.getValidToken();
20051
- log18.info("Scheduled token refresh completed");
20493
+ log21.info("Scheduled token refresh completed");
20052
20494
  } catch (err) {
20053
- log18.error("Scheduled token refresh failed", {
20495
+ log21.error("Scheduled token refresh failed", {
20054
20496
  error: err instanceof Error ? err.message : String(err)
20055
20497
  });
20056
20498
  }
@@ -20081,13 +20523,13 @@ var wrapper_default = import_websocket.default;
20081
20523
 
20082
20524
  // src/transport/ws-client.ts
20083
20525
  var import_node_events = require("events");
20084
- var log19 = createLogger("transport/ws-client");
20526
+ var log22 = createLogger("transport/ws-client");
20085
20527
  var WSClient = class extends import_node_events.EventEmitter {
20086
20528
  /** 底层 WebSocket 实例 */
20087
20529
  ws = null;
20088
20530
  constructor() {
20089
20531
  super();
20090
- log19.info("WSClient initialized");
20532
+ log22.info("WSClient initialized");
20091
20533
  }
20092
20534
  /**
20093
20535
  * 连接到 WebSocket 服务器并绑定事件。
@@ -20103,13 +20545,17 @@ var WSClient = class extends import_node_events.EventEmitter {
20103
20545
  this.ws = null;
20104
20546
  }
20105
20547
  const { url: url2, protocols, headers } = options;
20106
- log19.info("ws:connecting", { url: url2 });
20548
+ log22.info("ws:connecting", { url: url2 });
20107
20549
  return new Promise((resolve3, reject) => {
20108
- const ws = new wrapper_default(url2, protocols, { headers });
20550
+ const ws = new wrapper_default(url2, protocols, {
20551
+ headers,
20552
+ maxPayload: 5 * 1024 * 1024
20553
+ // 5MB — 防止恶意超大帧导致内存耗尽
20554
+ });
20109
20555
  let connected = false;
20110
20556
  ws.on("open", () => {
20111
20557
  connected = true;
20112
- log19.info("ws:connected", { url: url2 });
20558
+ log22.info("ws:connected", { url: url2 });
20113
20559
  this.emit("open");
20114
20560
  resolve3();
20115
20561
  });
@@ -20120,11 +20566,11 @@ var WSClient = class extends import_node_events.EventEmitter {
20120
20566
  this.emit("pong");
20121
20567
  });
20122
20568
  ws.on("close", (code, reason) => {
20123
- log19.info("ws:closed", { code, reason: reason.toString() });
20569
+ log22.info("ws:closed", { code, reason: reason.toString() });
20124
20570
  this.emit("close", code, reason.toString());
20125
20571
  });
20126
20572
  ws.on("error", (err) => {
20127
- log19.error("ws:error", { error: err.message });
20573
+ log22.error("ws:error", { error: err.message });
20128
20574
  this.emit("error", err);
20129
20575
  if (!connected) {
20130
20576
  reject(err);
@@ -20136,7 +20582,7 @@ var WSClient = class extends import_node_events.EventEmitter {
20136
20582
  /** 发送数据到服务器 — 前置检查连接状态 */
20137
20583
  send(data) {
20138
20584
  if (!this.ws || this.ws.readyState !== wrapper_default.OPEN) {
20139
- log19.warn("ws:send skipped, not connected", { readyState: this.ws?.readyState });
20585
+ log22.warn("ws:send skipped, not connected", { readyState: this.ws?.readyState });
20140
20586
  return;
20141
20587
  }
20142
20588
  this.ws.send(data);
@@ -20153,7 +20599,7 @@ var WSClient = class extends import_node_events.EventEmitter {
20153
20599
  this.ws.removeAllListeners();
20154
20600
  this.ws.close(code, reason);
20155
20601
  this.ws = null;
20156
- log19.info("ws:close requested", { code, reason });
20602
+ log22.info("ws:close requested", { code, reason });
20157
20603
  }
20158
20604
  }
20159
20605
  /** 当前连接状态 (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED) */
@@ -20163,7 +20609,7 @@ var WSClient = class extends import_node_events.EventEmitter {
20163
20609
  };
20164
20610
 
20165
20611
  // src/transport/dedup.ts
20166
- var log20 = createLogger("transport/dedup");
20612
+ var log23 = createLogger("transport/dedup");
20167
20613
  var MessageDedup = class {
20168
20614
  /** 去重缓存生存时间 (ms) */
20169
20615
  ttlMs;
@@ -20174,7 +20620,7 @@ var MessageDedup = class {
20174
20620
  constructor(config2) {
20175
20621
  this.ttlMs = config2?.ttlMs ?? 3e5;
20176
20622
  this.maxEntries = config2?.maxEntries ?? 5e3;
20177
- log20.info("MessageDedup initialized", { ttlMs: this.ttlMs, maxEntries: this.maxEntries });
20623
+ log23.info("MessageDedup initialized", { ttlMs: this.ttlMs, maxEntries: this.maxEntries });
20178
20624
  }
20179
20625
  /**
20180
20626
  * 检查消息是否重复。
@@ -20210,7 +20656,121 @@ var MessageDedup = class {
20210
20656
 
20211
20657
  // src/transport/message-pipe.ts
20212
20658
  var import_node_crypto3 = require("crypto");
20213
- var log21 = createLogger("transport/message-pipe");
20659
+
20660
+ // src/transport/rate-limiter.ts
20661
+ var log24 = createLogger("transport/rate-limiter");
20662
+ var RateLimiter = class {
20663
+ /** 每秒补充的 token 数 */
20664
+ ratePerSecond;
20665
+ /** token 上限 (突发容量) */
20666
+ burst;
20667
+ /** 最大等待时间 (ms) */
20668
+ maxWaitMs;
20669
+ /** 当前可用 token 数 (浮点数,支持部分补充) */
20670
+ tokens;
20671
+ /** 上次补充 token 的时间戳 (ms) */
20672
+ lastRefillTime;
20673
+ // ── 统计指标 ──
20674
+ /** 累计允许的请求数 */
20675
+ allowedCount = 0;
20676
+ /** 累计被限流的请求数 */
20677
+ throttledCount = 0;
20678
+ constructor(options) {
20679
+ this.ratePerSecond = options.ratePerSecond;
20680
+ this.burst = options.burst ?? options.ratePerSecond * 2;
20681
+ this.maxWaitMs = options.maxWaitMs ?? 5e3;
20682
+ this.tokens = this.burst;
20683
+ this.lastRefillTime = Date.now();
20684
+ log24.info("RateLimiter initialized", {
20685
+ ratePerSecond: this.ratePerSecond,
20686
+ burst: this.burst,
20687
+ maxWaitMs: this.maxWaitMs
20688
+ });
20689
+ }
20690
+ /**
20691
+ * 获取一个 token。
20692
+ *
20693
+ * - 有可用 token → 立即返回 'allowed'
20694
+ * - 无 token 且等待时间 ≤ maxWaitMs → 等待补充后返回 'allowed'
20695
+ * - 无 token 且等待时间 > maxWaitMs → 立即返回 'throttled'
20696
+ */
20697
+ async acquire() {
20698
+ this.refill();
20699
+ if (this.tokens >= 1) {
20700
+ this.tokens -= 1;
20701
+ this.allowedCount++;
20702
+ return "allowed";
20703
+ }
20704
+ const waitMs = (1 - this.tokens) / this.ratePerSecond * 1e3;
20705
+ if (waitMs > this.maxWaitMs) {
20706
+ this.throttledCount++;
20707
+ log24.warn("rate-limit:throttled", {
20708
+ waitMs: Math.round(waitMs),
20709
+ maxWaitMs: this.maxWaitMs,
20710
+ availableTokens: this.tokens.toFixed(2)
20711
+ });
20712
+ return "throttled";
20713
+ }
20714
+ this.tokens -= 1;
20715
+ await new Promise((resolve3) => setTimeout(resolve3, Math.ceil(waitMs)));
20716
+ this.refill();
20717
+ this.allowedCount++;
20718
+ return "allowed";
20719
+ }
20720
+ /**
20721
+ * 非阻塞尝试获取 token。
20722
+ * 有可用 token 返回 true,否则返回 false (不等待)。
20723
+ */
20724
+ tryAcquire() {
20725
+ this.refill();
20726
+ if (this.tokens >= 1) {
20727
+ this.tokens -= 1;
20728
+ this.allowedCount++;
20729
+ return true;
20730
+ }
20731
+ return false;
20732
+ }
20733
+ /**
20734
+ * 补充 token — 根据时间经过量按比例补充。
20735
+ * 利用时间差计算,不需要定时器。
20736
+ */
20737
+ refill() {
20738
+ const now = Date.now();
20739
+ const elapsed = now - this.lastRefillTime;
20740
+ if (elapsed <= 0) return;
20741
+ const newTokens = elapsed / 1e3 * this.ratePerSecond;
20742
+ this.tokens = Math.min(this.tokens + newTokens, this.burst);
20743
+ this.lastRefillTime = now;
20744
+ }
20745
+ /** 当前可用 token 数 */
20746
+ get availableTokens() {
20747
+ this.refill();
20748
+ return Math.floor(this.tokens);
20749
+ }
20750
+ /** 获取指标快照 */
20751
+ getStats() {
20752
+ return {
20753
+ available: Math.floor(this.tokens),
20754
+ allowed: this.allowedCount,
20755
+ throttled: this.throttledCount
20756
+ };
20757
+ }
20758
+ };
20759
+
20760
+ // src/transport/message-pipe.ts
20761
+ var log25 = createLogger("transport/message-pipe");
20762
+ var CallbackDataSchema = external_exports.object({
20763
+ appId: external_exports.number(),
20764
+ loginId: external_exports.string().optional(),
20765
+ eventType: external_exports.string(),
20766
+ userId: external_exports.string(),
20767
+ groupId: external_exports.string(),
20768
+ toUserId: external_exports.string(),
20769
+ msgUid: external_exports.string(),
20770
+ type: external_exports.string(),
20771
+ content: external_exports.record(external_exports.string(), external_exports.unknown()),
20772
+ extra: external_exports.string().optional()
20773
+ });
20214
20774
  var MessagePipe = class _MessagePipe {
20215
20775
  wsClient;
20216
20776
  dedup;
@@ -20225,6 +20785,10 @@ var MessagePipe = class _MessagePipe {
20225
20785
  quantumAccount;
20226
20786
  /** 消息加密模式 */
20227
20787
  encryptionMode;
20788
+ /** 入站并发控制信号量 */
20789
+ inboundSemaphore;
20790
+ /** 出站速率限制器 */
20791
+ rateLimiter;
20228
20792
  constructor(deps) {
20229
20793
  this.wsClient = deps.wsClient;
20230
20794
  this.dedup = deps.dedup;
@@ -20233,17 +20797,37 @@ var MessagePipe = class _MessagePipe {
20233
20797
  this.messageServiceBaseUrl = deps.messageServiceBaseUrl;
20234
20798
  this.quantumAccount = deps.quantumAccount;
20235
20799
  this.encryptionMode = deps.encryptionMode;
20800
+ this.inboundSemaphore = new Semaphore({
20801
+ maxConcurrency: deps.maxInboundConcurrency ?? 5,
20802
+ maxWaiting: deps.maxWaitingQueue ?? 15
20803
+ });
20804
+ this.rateLimiter = new RateLimiter({
20805
+ ratePerSecond: deps.rateLimitPerSecond ?? 30,
20806
+ burst: deps.rateLimitBurst ?? 60
20807
+ });
20236
20808
  this.wsClient.on("message", (rawData) => {
20809
+ if (this.inboundSemaphore.activeCount >= (deps.maxInboundConcurrency ?? 5) && this.inboundSemaphore.waitingCount >= (deps.maxWaitingQueue ?? 15)) {
20810
+ metrics.increment("inbound.backpressure_rejected");
20811
+ log25.warn("backpressure:rejected", {
20812
+ active: this.inboundSemaphore.activeCount,
20813
+ waiting: this.inboundSemaphore.waitingCount
20814
+ });
20815
+ return;
20816
+ }
20237
20817
  this.handleInbound(rawData).catch((err) => {
20238
- log21.error("\u274C \u5165\u7AD9\u5904\u7406\u5F02\u5E38", { step: "handleInbound", error: err.message });
20818
+ log25.error("\u274C \u5165\u7AD9\u5904\u7406\u5F02\u5E38", { step: "handleInbound", error: err.message });
20239
20819
  });
20240
20820
  });
20241
- log21.info("MessagePipe initialized");
20821
+ log25.info("MessagePipe initialized");
20822
+ }
20823
+ /** 获取入站并发信号量 — 由 InboundPipeline 用于 acquire/release */
20824
+ get semaphore() {
20825
+ return this.inboundSemaphore;
20242
20826
  }
20243
20827
  /** 注册入站消息回调 — 解密后的消息会通过此回调传给 L4 层 */
20244
20828
  onMessage(callback) {
20245
20829
  this.messageCallback = callback;
20246
- log21.info("Inbound message callback registered");
20830
+ log25.info("Inbound message callback registered");
20247
20831
  }
20248
20832
  /**
20249
20833
  * 🧪 DEBUG — 注入模拟 WS 原始帧,走完完整的 7 步入站流水线。
@@ -20280,7 +20864,7 @@ var MessagePipe = class _MessagePipe {
20280
20864
  keyId = result.keyId;
20281
20865
  sessionKey = result.sessionKey;
20282
20866
  fillKey = result.fillKey;
20283
- log21.debug("\u{1F512} \u6587\u4EF6\u5206\u7247\u52A0\u5BC6", {
20867
+ log25.debug("\u{1F512} \u6587\u4EF6\u5206\u7247\u52A0\u5BC6", {
20284
20868
  chunk: `${i + 1}/${totalChunks}`,
20285
20869
  originalSize: chunk.length,
20286
20870
  encryptedSize: result.fileBuffer.length,
@@ -20288,7 +20872,7 @@ var MessagePipe = class _MessagePipe {
20288
20872
  });
20289
20873
  }
20290
20874
  const encryptedBuffer = Buffer.concat(encryptedChunks);
20291
- log21.info("\u{1F512} \u6587\u4EF6\u52A0\u5BC6\u5B8C\u6210", {
20875
+ log25.info("\u{1F512} \u6587\u4EF6\u52A0\u5BC6\u5B8C\u6210", {
20292
20876
  originalSize: buffer.length,
20293
20877
  encryptedSize: encryptedBuffer.length,
20294
20878
  totalChunks,
@@ -20345,11 +20929,11 @@ var MessagePipe = class _MessagePipe {
20345
20929
  if (this.encryptionMode === "quantum_only") {
20346
20930
  if (isFileMessage && msg.encryptionMeta) {
20347
20931
  extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
20348
- log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
20932
+ log25.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
20349
20933
  } else if (isFileMessage) {
20350
20934
  const { keyId, iv } = await this.crypto.encrypt(msg.content);
20351
20935
  extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
20352
- log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only, fallback)", { sessionId: keyId, msgType: msg.msgType });
20936
+ log25.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_only, fallback)", { sessionId: keyId, msgType: msg.msgType });
20353
20937
  } else {
20354
20938
  const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
20355
20939
  extra = JSON.stringify({
@@ -20358,17 +20942,17 @@ var MessagePipe = class _MessagePipe {
20358
20942
  sessionId: keyId
20359
20943
  });
20360
20944
  content = JSON.stringify({ content: "" });
20361
- log21.debug("\u{1F512} \u51FA\u7AD9\u6D88\u606F\u5DF2\u52A0\u5BC6 (quantum_only)", { sessionId: keyId });
20945
+ log25.debug("\u{1F512} \u51FA\u7AD9\u6D88\u606F\u5DF2\u52A0\u5BC6 (quantum_only)", { sessionId: keyId });
20362
20946
  }
20363
20947
  } else {
20364
20948
  try {
20365
20949
  if (isFileMessage && msg.encryptionMeta) {
20366
20950
  extra = _MessagePipe.buildFileEncryptExtra(msg.encryptionMeta.iv, msg.encryptionMeta.keyId);
20367
- 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 });
20951
+ log25.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain)", { sessionId: msg.encryptionMeta.keyId, msgType: msg.msgType });
20368
20952
  } else if (isFileMessage) {
20369
20953
  const { keyId, iv } = await this.crypto.encrypt(msg.content);
20370
20954
  extra = _MessagePipe.buildFileEncryptExtra(iv, keyId);
20371
- log21.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain, fallback)", { sessionId: keyId, msgType: msg.msgType });
20955
+ log25.debug("\u{1F512} \u51FA\u7AD9\u6587\u4EF6\u6D88\u606F\u5DF2\u6807\u8BB0\u52A0\u5BC6 (quantum_and_plain, fallback)", { sessionId: keyId, msgType: msg.msgType });
20372
20956
  } else {
20373
20957
  const { ciphertext, keyId, iv } = await this.crypto.encrypt(msg.content);
20374
20958
  extra = JSON.stringify({
@@ -20377,12 +20961,20 @@ var MessagePipe = class _MessagePipe {
20377
20961
  sessionId: keyId
20378
20962
  });
20379
20963
  content = JSON.stringify({ content: "" });
20380
- log21.debug("\u{1F512} \u51FA\u7AD9\u6D88\u606F\u5DF2\u52A0\u5BC6 (quantum_and_plain)", { sessionId: keyId });
20964
+ log25.debug("\u{1F512} \u51FA\u7AD9\u6D88\u606F\u5DF2\u52A0\u5BC6 (quantum_and_plain)", { sessionId: keyId });
20381
20965
  }
20382
20966
  } catch (err) {
20383
- log21.error("\u274C \u51FA\u7AD9\u6D88\u606F\u52A0\u5BC6\u5931\u8D25\uFF0C\u5C06\u53D1\u9001\u660E\u6587", {
20384
- error: err.message
20967
+ const event = err instanceof CryptoTimeoutError ? "encryption_timeout_downgrade" : "encryption_downgrade";
20968
+ log25.warn(`SECURITY_AUDIT:${event}`, {
20969
+ event,
20970
+ chatId: msg.chatId,
20971
+ msgType: msg.msgType,
20972
+ reason: err.message,
20973
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20385
20974
  });
20975
+ if (err instanceof CryptoTimeoutError) {
20976
+ metrics.increment("crypto.timeout");
20977
+ }
20386
20978
  }
20387
20979
  }
20388
20980
  }
@@ -20400,7 +20992,7 @@ var MessagePipe = class _MessagePipe {
20400
20992
  }
20401
20993
  const result = await this.callMessageApi("/messages/v1/send", body, "outbound");
20402
20994
  if (!result) return;
20403
- log21.info("\u{1F4E4} outbound:sent \u2192 IM \u670D\u52A1\u5668", {
20995
+ log25.info("\u{1F4E4} outbound:sent \u2192 IM \u670D\u52A1\u5668", {
20404
20996
  chatId: msg.chatId,
20405
20997
  encrypted: Boolean(extra),
20406
20998
  requestId: result.request_id,
@@ -20424,7 +21016,7 @@ var MessagePipe = class _MessagePipe {
20424
21016
  };
20425
21017
  const result = await this.callMessageApi("/messages/v1/recall", body, "recall");
20426
21018
  if (!result) return;
20427
- log21.info("recall:sent", {
21019
+ log25.info("recall:sent", {
20428
21020
  messageId: params.messageId,
20429
21021
  requestId: result.request_id
20430
21022
  });
@@ -20438,9 +21030,19 @@ var MessagePipe = class _MessagePipe {
20438
21030
  * @param body - 请求体
20439
21031
  * @param logTag - 日志标签前缀 (如 'outbound' / 'recall')
20440
21032
  */
20441
- async callMessageApi(path2, body, logTag) {
20442
- const url2 = `${this.messageServiceBaseUrl}${path2}`;
21033
+ async callMessageApi(path3, body, logTag) {
21034
+ const url2 = `${this.messageServiceBaseUrl}${path3}`;
21035
+ const rateLimitResult = await this.rateLimiter.acquire();
21036
+ if (rateLimitResult === "throttled") {
21037
+ metrics.increment("outbound.dropped");
21038
+ log25.warn(`${logTag}:rate-limit-dropped`, {
21039
+ url: url2,
21040
+ body: { receive_id: body.receive_id, msg_type: body.msg_type }
21041
+ });
21042
+ return null;
21043
+ }
20443
21044
  try {
21045
+ const apiStartMs = Date.now();
20444
21046
  const token = await this.tokenFn();
20445
21047
  const resp = await fetch(url2, {
20446
21048
  method: "POST",
@@ -20451,25 +21053,29 @@ var MessagePipe = class _MessagePipe {
20451
21053
  body: JSON.stringify(body)
20452
21054
  });
20453
21055
  if (!resp.ok) {
20454
- log21.error(`${logTag}:http-error`, {
21056
+ log25.error(`${logTag}:http-error`, {
20455
21057
  status: resp.status,
20456
21058
  statusText: resp.statusText,
20457
21059
  url: url2
20458
21060
  });
21061
+ metrics.increment("outbound.failed");
20459
21062
  return null;
20460
21063
  }
20461
21064
  const result = await resp.json();
20462
21065
  if (result.code !== 0 && result.code !== 200) {
20463
- log21.error(`${logTag}:api-error`, {
21066
+ log25.error(`${logTag}:api-error`, {
20464
21067
  code: result.code,
20465
21068
  msg: result.msg,
20466
21069
  requestId: result.request_id
20467
21070
  });
20468
21071
  return null;
20469
21072
  }
21073
+ metrics.increment("outbound.success");
21074
+ metrics.recordLatency("outbound.latency", Date.now() - apiStartMs);
20470
21075
  return result;
20471
21076
  } catch (err) {
20472
- log21.error(`${logTag}:network-error`, {
21077
+ metrics.increment("outbound.failed");
21078
+ log25.error(`${logTag}:network-error`, {
20473
21079
  url: url2,
20474
21080
  error: err.message
20475
21081
  });
@@ -20491,7 +21097,7 @@ var MessagePipe = class _MessagePipe {
20491
21097
  try {
20492
21098
  frame = JSON.parse(String(rawData));
20493
21099
  } catch (err) {
20494
- log21.warn("\u26A0\uFE0F WS \u5E27 JSON \u89E3\u6790\u5931\u8D25", { error: err.message });
21100
+ log25.warn("\u26A0\uFE0F WS \u5E27 JSON \u89E3\u6790\u5931\u8D25", { error: err.message });
20495
21101
  return;
20496
21102
  }
20497
21103
  const timestamp = frame["X-CTQ-Timestamp"];
@@ -20502,18 +21108,36 @@ var MessagePipe = class _MessagePipe {
20502
21108
  const signatureInput = timestamp + nonce + frame.data;
20503
21109
  const expected = (0, import_node_crypto3.createHmac)("sha256", token).update(signatureInput).digest("hex");
20504
21110
  if (expected !== signature) {
20505
- log21.warn("\u26A0\uFE0F HMAC \u9A8C\u7B7E\u5931\u8D25\uFF0C\u4E22\u5F03\u5E27", { timestamp, nonce });
21111
+ log25.warn("\u26A0\uFE0F HMAC \u9A8C\u7B7E\u5931\u8D25\uFF0C\u4E22\u5F03\u5E27", { timestamp, nonce });
20506
21112
  return;
20507
21113
  }
20508
21114
  }
20509
21115
  let callbackData;
20510
21116
  try {
20511
- callbackData = JSON.parse(frame.data);
21117
+ const parsed = JSON.parse(frame.data);
21118
+ const result = CallbackDataSchema.safeParse(parsed);
21119
+ if (!result.success) {
21120
+ log25.warn("\u26A0\uFE0F CallbackData validation failed", {
21121
+ errors: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
21122
+ });
21123
+ return;
21124
+ }
21125
+ callbackData = result.data;
20512
21126
  } catch (err) {
20513
- log21.warn("\u26A0\uFE0F CallbackData \u89E3\u6790\u5931\u8D25", { error: err.message });
21127
+ log25.warn("\u26A0\uFE0F CallbackData \u89E3\u6790\u5931\u8D25", { error: err.message });
21128
+ return;
21129
+ }
21130
+ const contentSize = JSON.stringify(callbackData.content).length;
21131
+ const MAX_CONTENT_SIZE = 1 * 1024 * 1024;
21132
+ if (contentSize > MAX_CONTENT_SIZE) {
21133
+ log25.warn("\u26A0\uFE0F message content exceeds size limit", {
21134
+ msgUid: callbackData.msgUid,
21135
+ contentSize,
21136
+ maxSize: MAX_CONTENT_SIZE
21137
+ });
20514
21138
  return;
20515
21139
  }
20516
- log21.info("\u{1F4E9} \u5165\u7AD9\u539F\u59CB\u6570\u636E", {
21140
+ log25.info("\u{1F4E9} \u5165\u7AD9\u539F\u59CB\u6570\u636E", {
20517
21141
  appId: callbackData.appId,
20518
21142
  msgUid: callbackData.msgUid,
20519
21143
  eventType: callbackData.eventType,
@@ -20531,25 +21155,25 @@ var MessagePipe = class _MessagePipe {
20531
21155
  const parsed = JSON.parse(extra);
20532
21156
  isEncrypted = Boolean(parsed.encryptMsg);
20533
21157
  } catch {
20534
- log21.debug("\u2139\uFE0F extra \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
21158
+ log25.debug("\u2139\uFE0F extra \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u89C6\u4E3A\u660E\u6587", { msgUid: callbackData.msgUid });
20535
21159
  }
20536
21160
  }
20537
21161
  let contentObj;
20538
21162
  if (this.encryptionMode === "quantum_only") {
20539
21163
  if (!this.quantumAccount) {
20540
- log21.info("\u26A0\uFE0F quantum_only: \u672A\u914D\u7F6E quantumAccount\uFF0C\u53D1\u9001\u5F15\u5BFC\u63D0\u793A", { chatId: callbackData.userId });
21164
+ log25.info("\u26A0\uFE0F quantum_only: \u672A\u914D\u7F6E quantumAccount\uFF0C\u53D1\u9001\u5F15\u5BFC\u63D0\u793A", { chatId: callbackData.userId });
20541
21165
  await this.sendHintMessage(callbackData, HINT_NO_QUANTUM_ACCOUNT);
20542
21166
  return;
20543
21167
  }
20544
21168
  if (!isEncrypted) {
20545
- log21.info("\u26A0\uFE0F quantum_only: \u6536\u5230\u660E\u6587\u6D88\u606F\uFF0C\u62D2\u7EDD\u5904\u7406", { msgUid: callbackData.msgUid });
21169
+ log25.info("\u26A0\uFE0F quantum_only: \u6536\u5230\u660E\u6587\u6D88\u606F\uFF0C\u62D2\u7EDD\u5904\u7406", { msgUid: callbackData.msgUid });
20546
21170
  await this.sendHintMessage(callbackData, HINT_PLAINTEXT_NOT_SUPPORTED);
20547
21171
  return;
20548
21172
  }
20549
21173
  try {
20550
21174
  contentObj = await this.decryptExtra(callbackData);
20551
21175
  } catch (err) {
20552
- log21.error("\u274C quantum_only: \u6D88\u606F\u89E3\u5BC6\u5931\u8D25", {
21176
+ log25.error("\u274C quantum_only: \u6D88\u606F\u89E3\u5BC6\u5931\u8D25", {
20553
21177
  msgUid: callbackData.msgUid,
20554
21178
  error: err.message
20555
21179
  });
@@ -20558,14 +21182,14 @@ var MessagePipe = class _MessagePipe {
20558
21182
  } else {
20559
21183
  if (isEncrypted) {
20560
21184
  if (!this.quantumAccount) {
20561
- log21.info("\u26A0\uFE0F quantum_and_plain: \u6536\u5230\u52A0\u5BC6\u6D88\u606F\u4F46\u672A\u914D\u7F6E quantumAccount", { msgUid: callbackData.msgUid });
21185
+ log25.info("\u26A0\uFE0F quantum_and_plain: \u6536\u5230\u52A0\u5BC6\u6D88\u606F\u4F46\u672A\u914D\u7F6E quantumAccount", { msgUid: callbackData.msgUid });
20562
21186
  await this.sendHintMessage(callbackData, HINT_NO_QUANTUM_ACCOUNT);
20563
21187
  return;
20564
21188
  }
20565
21189
  try {
20566
21190
  contentObj = await this.decryptExtra(callbackData);
20567
21191
  } catch (err) {
20568
- log21.error("\u274C quantum_and_plain: \u6D88\u606F\u89E3\u5BC6\u5931\u8D25", {
21192
+ log25.error("\u274C quantum_and_plain: \u6D88\u606F\u89E3\u5BC6\u5931\u8D25", {
20569
21193
  msgUid: callbackData.msgUid,
20570
21194
  error: err.message
20571
21195
  });
@@ -20576,7 +21200,7 @@ var MessagePipe = class _MessagePipe {
20576
21200
  }
20577
21201
  }
20578
21202
  if (callbackData.eventType !== "callback:direct") {
20579
- log21.debug("\u2139\uFE0F \u5FFD\u7565\u975E\u76EE\u6807\u4E8B\u4EF6", { eventType: callbackData.eventType });
21203
+ log25.debug("\u2139\uFE0F \u5FFD\u7565\u975E\u76EE\u6807\u4E8B\u4EF6", { eventType: callbackData.eventType });
20580
21204
  return;
20581
21205
  }
20582
21206
  const msg = {
@@ -20588,16 +21212,16 @@ var MessagePipe = class _MessagePipe {
20588
21212
  timestamp: Date.now(),
20589
21213
  isEncrypted
20590
21214
  };
20591
- log21.debug("\u{1F4E8} \u89E3\u6790 WS \u5E27", { messageId: msg.messageId, eventType: callbackData.eventType });
21215
+ log25.debug("\u{1F4E8} \u89E3\u6790 WS \u5E27", { messageId: msg.messageId, eventType: callbackData.eventType });
20592
21216
  if (this.dedup.isDuplicate(msg.messageId)) {
20593
- log21.debug("\u{1F501} \u91CD\u590D\u6D88\u606F\u5DF2\u8DF3\u8FC7", { messageId: msg.messageId });
21217
+ log25.debug("\u{1F501} \u91CD\u590D\u6D88\u606F\u5DF2\u8DF3\u8FC7", { messageId: msg.messageId });
20594
21218
  return;
20595
21219
  }
20596
- log21.info("\u2705 \u5165\u7AD9\u6D88\u606F\u9A8C\u8BC1\u901A\u8FC7", { messageId: msg.messageId, chatId: msg.chatId });
21220
+ log25.info("\u2705 \u5165\u7AD9\u6D88\u606F\u9A8C\u8BC1\u901A\u8FC7", { messageId: msg.messageId, chatId: msg.chatId });
20597
21221
  if (this.messageCallback) {
20598
21222
  this.messageCallback(msg);
20599
21223
  } else {
20600
- log21.warn("\u26A0\uFE0F \u6D88\u606F\u56DE\u8C03\u672A\u6CE8\u518C\uFF0C\u65E0\u6CD5\u5904\u7406", { messageId: msg.messageId });
21224
+ log25.warn("\u26A0\uFE0F \u6D88\u606F\u56DE\u8C03\u672A\u6CE8\u518C\uFF0C\u65E0\u6CD5\u5904\u7406", { messageId: msg.messageId });
20601
21225
  }
20602
21226
  }
20603
21227
  // ── 私有辅助方法 ──
@@ -20616,9 +21240,9 @@ var MessagePipe = class _MessagePipe {
20616
21240
  content: JSON.stringify({ content: hintText }),
20617
21241
  skipEncrypt: true
20618
21242
  });
20619
- log21.info("\u{1F4A1} \u5DF2\u53D1\u9001\u6A21\u5F0F\u63D0\u793A\u6D88\u606F", { chatId });
21243
+ log25.info("\u{1F4A1} \u5DF2\u53D1\u9001\u6A21\u5F0F\u63D0\u793A\u6D88\u606F", { chatId });
20620
21244
  } catch (err) {
20621
- log21.error("\u274C \u63D0\u793A\u6D88\u606F\u53D1\u9001\u5931\u8D25", { error: err.message });
21245
+ log25.error("\u274C \u63D0\u793A\u6D88\u606F\u53D1\u9001\u5931\u8D25", { error: err.message });
20622
21246
  }
20623
21247
  }
20624
21248
  /**
@@ -20630,12 +21254,12 @@ var MessagePipe = class _MessagePipe {
20630
21254
  const encryptMsg = extraData.encryptMsg ?? "";
20631
21255
  const sessionId = extraData.sessionId ?? "";
20632
21256
  if (!encryptMsg) {
20633
- log21.warn("\u26A0\uFE0F extra \u4E2D encryptMsg \u4E3A\u7A7A\uFF0C\u4F7F\u7528\u539F\u59CB content", { msgUid: callbackData.msgUid });
21257
+ log25.warn("\u26A0\uFE0F extra \u4E2D encryptMsg \u4E3A\u7A7A\uFF0C\u4F7F\u7528\u539F\u59CB content", { msgUid: callbackData.msgUid });
20634
21258
  return callbackData.content;
20635
21259
  }
20636
21260
  const cryptoIv = extraData.cryptoIv ?? "";
20637
21261
  const decrypted = await this.crypto.decrypt(encryptMsg, sessionId, cryptoIv);
20638
- log21.debug("\u{1F513} \u6D88\u606F\u89E3\u5BC6\u6210\u529F", { msgUid: callbackData.msgUid, sessionId, hasIv: Boolean(cryptoIv) });
21262
+ log25.debug("\u{1F513} \u6D88\u606F\u89E3\u5BC6\u6210\u529F", { msgUid: callbackData.msgUid, sessionId, hasIv: Boolean(cryptoIv) });
20639
21263
  try {
20640
21264
  return JSON.parse(decrypted);
20641
21265
  } catch {
@@ -20645,7 +21269,7 @@ var MessagePipe = class _MessagePipe {
20645
21269
  };
20646
21270
 
20647
21271
  // src/transport/connection-manager.ts
20648
- var log22 = createLogger("transport/connection-manager");
21272
+ var log26 = createLogger("transport/connection-manager");
20649
21273
  function sleep2(ms) {
20650
21274
  return new Promise((resolve3) => setTimeout(resolve3, ms));
20651
21275
  }
@@ -20679,7 +21303,7 @@ var ConnectionManager = class {
20679
21303
  reconnectMaxMs: options?.reconnectMaxMs ?? 6e4,
20680
21304
  maxReconnectAttempts: options?.maxReconnectAttempts ?? 10
20681
21305
  };
20682
- log22.info("ConnectionManager initialized", this.options);
21306
+ log26.info("ConnectionManager initialized", this.options);
20683
21307
  }
20684
21308
  /**
20685
21309
  * 启动连接 — 连接 WS + 开始心跳 + 注册重连逻辑
@@ -20693,8 +21317,16 @@ var ConnectionManager = class {
20693
21317
  this.appId = appId ?? "";
20694
21318
  this.running = true;
20695
21319
  this.reconnectAttempts = 0;
20696
- const token = await tokenFn();
20697
21320
  const wsUrl = url2.replace(/\/+$/, "") + "/events/stream";
21321
+ if (!wsUrl.startsWith("wss://")) {
21322
+ if (process.env.LZMX_ALLOW_INSECURE_WS === "true") {
21323
+ log26.warn("SECURITY_AUDIT: insecure WS allowed via LZMX_ALLOW_INSECURE_WS", { url: wsUrl });
21324
+ } else {
21325
+ throw new Error("Insecure WebSocket (ws://) is not allowed. Set LZMX_ALLOW_INSECURE_WS=true to override.");
21326
+ }
21327
+ }
21328
+ const token = await tokenFn();
21329
+ log26.debug("ws:auth", { authorization: sanitize(token) });
20698
21330
  await this.client.connect({
20699
21331
  url: wsUrl,
20700
21332
  token,
@@ -20705,7 +21337,7 @@ var ConnectionManager = class {
20705
21337
  });
20706
21338
  this.registerEvents();
20707
21339
  this.startHeartbeat();
20708
- log22.info("ConnectionManager started \u2713", { url: url2 });
21340
+ log26.info("ConnectionManager started \u2713", { url: url2 });
20709
21341
  }
20710
21342
  /** 注册 close / error / pong 事件 */
20711
21343
  registerEvents() {
@@ -20714,17 +21346,17 @@ var ConnectionManager = class {
20714
21346
  this.client.removeAllListeners("pong");
20715
21347
  this.client.on("close", () => {
20716
21348
  if (this.running) {
20717
- log22.warn("ws:disconnected, scheduling reconnect");
21349
+ log26.warn("ws:disconnected, scheduling reconnect");
20718
21350
  this.scheduleReconnect();
20719
21351
  }
20720
21352
  });
20721
21353
  this.client.on("error", (err) => {
20722
- log22.error("ws:connection-error", { error: err.message });
21354
+ log26.error("ws:connection-error", { error: err.message });
20723
21355
  });
20724
21356
  this.client.on("pong", () => {
20725
21357
  this.pongReceived = true;
20726
21358
  this.clearPongTimeout();
20727
- log22.debug("heartbeat: pong received");
21359
+ log26.debug("heartbeat: pong received");
20728
21360
  });
20729
21361
  }
20730
21362
  /** 启动心跳保活 — 定时发送 WebSocket Ping 帧 */
@@ -20733,23 +21365,23 @@ var ConnectionManager = class {
20733
21365
  this.pongReceived = true;
20734
21366
  this.heartbeatTimer = setInterval(() => {
20735
21367
  if (this.client.readyState !== wrapper_default.OPEN) {
20736
- log22.warn("heartbeat: connection not open, scheduling reconnect");
21368
+ log26.warn("heartbeat: connection not open, scheduling reconnect");
20737
21369
  this.scheduleReconnect();
20738
21370
  return;
20739
21371
  }
20740
21372
  if (!this.pongReceived) {
20741
- log22.warn("heartbeat: pong timeout, connection dead");
21373
+ log26.warn("heartbeat: pong timeout, connection dead");
20742
21374
  this.client.close(4e3, "pong timeout");
20743
21375
  this.scheduleReconnect();
20744
21376
  return;
20745
21377
  }
20746
21378
  this.pongReceived = false;
20747
21379
  this.client.ping();
20748
- log22.debug("heartbeat: ping sent");
21380
+ log26.debug("heartbeat: ping sent");
20749
21381
  this.clearPongTimeout();
20750
21382
  this.pongTimeoutTimer = setTimeout(() => {
20751
21383
  if (!this.pongReceived && this.running) {
20752
- log22.warn("heartbeat: pong timeout (independent timer), closing connection");
21384
+ log26.warn("heartbeat: pong timeout (independent timer), closing connection");
20753
21385
  this.client.close(4e3, "pong timeout");
20754
21386
  this.scheduleReconnect();
20755
21387
  }
@@ -20787,12 +21419,13 @@ var ConnectionManager = class {
20787
21419
  this.options.reconnectBaseMs * 2 ** this.reconnectAttempts,
20788
21420
  this.options.reconnectMaxMs
20789
21421
  );
20790
- log22.info("ws:reconnecting", { attempt: this.reconnectAttempts + 1, delayMs: delay });
21422
+ log26.info("ws:reconnecting", { attempt: this.reconnectAttempts + 1, delayMs: delay });
20791
21423
  await sleep2(delay);
20792
21424
  if (!this.running) return;
20793
21425
  this.reconnectAttempts++;
20794
21426
  try {
20795
21427
  const token = await this.tokenFn();
21428
+ log26.debug("ws:reconnect-auth", { authorization: sanitize(token) });
20796
21429
  const wsUrl = this.url.replace(/\/+$/, "") + "/events/stream";
20797
21430
  await this.client.connect({
20798
21431
  url: wsUrl,
@@ -20805,17 +21438,17 @@ var ConnectionManager = class {
20805
21438
  this.reconnectAttempts = 0;
20806
21439
  this.registerEvents();
20807
21440
  this.startHeartbeat();
20808
- log22.info("ws:reconnected", { attempt: this.reconnectAttempts, url: this.url });
21441
+ log26.info("ws:reconnected", { attempt: this.reconnectAttempts, url: this.url });
20809
21442
  return;
20810
21443
  } catch (err) {
20811
- log22.warn("ws:reconnect-failed", {
21444
+ log26.warn("ws:reconnect-failed", {
20812
21445
  attempt: this.reconnectAttempts,
20813
21446
  error: err.message
20814
21447
  });
20815
21448
  }
20816
21449
  }
20817
21450
  if (this.running) {
20818
- log22.error("ws:max-reconnect-reached", {
21451
+ log26.error("ws:max-reconnect-reached", {
20819
21452
  maxAttempts: this.options.maxReconnectAttempts
20820
21453
  });
20821
21454
  }
@@ -20825,7 +21458,7 @@ var ConnectionManager = class {
20825
21458
  this.running = false;
20826
21459
  this.stopHeartbeat();
20827
21460
  this.client.close(1e3, "shutdown");
20828
- log22.info("ConnectionManager stopped");
21461
+ log26.info("ConnectionManager stopped");
20829
21462
  }
20830
21463
  /** 当前是否已连接 (WebSocket readyState === OPEN) */
20831
21464
  get isConnected() {
@@ -20834,7 +21467,7 @@ var ConnectionManager = class {
20834
21467
  };
20835
21468
 
20836
21469
  // src/push/cockatoo-client.ts
20837
- var log23 = createLogger("push/cockatoo-client");
21470
+ var log27 = createLogger("push/cockatoo-client");
20838
21471
  var DEFAULT_TIMEOUT_MS2 = 3e4;
20839
21472
  var CockatooPushError = class extends Error {
20840
21473
  constructor(status, body, endpoint) {
@@ -20868,7 +21501,7 @@ var CockatooClient = class {
20868
21501
  const base = config2.endpoint.replace(/\/+$/, "");
20869
21502
  this.pushUrl = `${base}/api/v1/push`;
20870
21503
  this.healthUrl = `${base}/health`;
20871
- log23.info("CockatooClient initialized", { endpoint: config2.endpoint });
21504
+ log27.info("CockatooClient initialized", { endpoint: config2.endpoint });
20872
21505
  }
20873
21506
  /**
20874
21507
  * 推送消息到 Cockatoo 服务。
@@ -20899,7 +21532,7 @@ var CockatooClient = class {
20899
21532
  }
20900
21533
  throw new CockatooPushError(resp.status, body, this.pushUrl);
20901
21534
  }
20902
- log23.debug("push:sent", { type: payload.type });
21535
+ log27.debug("push:sent", { type: payload.type });
20903
21536
  }
20904
21537
  /**
20905
21538
  * 执行一次健康检查。
@@ -20917,11 +21550,11 @@ var CockatooClient = class {
20917
21550
  });
20918
21551
  const isHealthy = resp.ok;
20919
21552
  if (!isHealthy) {
20920
- log23.warn("health-check:unhealthy", { status: resp.status });
21553
+ log27.warn("health-check:unhealthy", { status: resp.status });
20921
21554
  }
20922
21555
  return isHealthy;
20923
21556
  } catch (err) {
20924
- log23.warn("health-check:error", { error: err.message });
21557
+ log27.warn("health-check:error", { error: err.message });
20925
21558
  return false;
20926
21559
  }
20927
21560
  }
@@ -20933,7 +21566,7 @@ var CockatooClient = class {
20933
21566
  this.healthy = await this.healthCheck();
20934
21567
  } catch {
20935
21568
  this.healthy = false;
20936
- log23.warn("Cockatoo health check failed");
21569
+ log27.warn("Cockatoo health check failed");
20937
21570
  }
20938
21571
  }, this.config.healthCheckIntervalMs ?? 6e4);
20939
21572
  if (typeof this.healthCheckTimer === "object" && "unref" in this.healthCheckTimer) {
@@ -20954,7 +21587,7 @@ var CockatooClient = class {
20954
21587
  };
20955
21588
 
20956
21589
  // src/push/push-queue.ts
20957
- var log24 = createLogger("push/push-queue");
21590
+ var log28 = createLogger("push/push-queue");
20958
21591
  var RETRY_BASE_MS2 = 1e3;
20959
21592
  var PushQueue = class {
20960
21593
  client;
@@ -20996,7 +21629,7 @@ var PushQueue = class {
20996
21629
  this.unhealthyRetryMs = config2?.unhealthyRetryMs ?? 5e3;
20997
21630
  this.drainOnStop = config2?.drainOnStop ?? false;
20998
21631
  this.drainTimeoutMs = config2?.drainTimeoutMs ?? 5e3;
20999
- log24.info("PushQueue initialized", {
21632
+ log28.info("PushQueue initialized", {
21000
21633
  maxSize: this.maxSize,
21001
21634
  retryAttempts: this.retryAttempts,
21002
21635
  processIntervalMs: this.processIntervalMs
@@ -21007,7 +21640,7 @@ var PushQueue = class {
21007
21640
  start() {
21008
21641
  if (this.running) return;
21009
21642
  this.running = true;
21010
- log24.info("PushQueue started");
21643
+ log28.info("PushQueue started");
21011
21644
  this.scheduleNext(0);
21012
21645
  }
21013
21646
  /**
@@ -21021,10 +21654,10 @@ var PushQueue = class {
21021
21654
  this.running = false;
21022
21655
  this.clearTimer();
21023
21656
  if (this.drainOnStop && this.queue.length > 0) {
21024
- log24.info("PushQueue draining", { remaining: this.queue.length });
21657
+ log28.info("PushQueue draining", { remaining: this.queue.length });
21025
21658
  await this.drain();
21026
21659
  }
21027
- log24.info("PushQueue stopped", {
21660
+ log28.info("PushQueue stopped", {
21028
21661
  remaining: this.queue.length,
21029
21662
  sent: this.sentCount,
21030
21663
  dropped: this.droppedCount,
@@ -21039,13 +21672,13 @@ var PushQueue = class {
21039
21672
  if (this.queue.length >= this.maxSize) {
21040
21673
  const dropped = this.queue.shift();
21041
21674
  this.droppedCount++;
21042
- log24.warn("queue:full, dropping oldest", {
21675
+ log28.warn("queue:full, dropping oldest", {
21043
21676
  droppedType: dropped?.payload.type,
21044
21677
  queueSize: this.queue.length
21045
21678
  });
21046
21679
  }
21047
21680
  this.queue.push({ payload, retries: 0, eligibleAt: 0 });
21048
- log24.debug("queue:enqueued", { type: payload.type, queueSize: this.queue.length });
21681
+ log28.debug("queue:enqueued", { type: payload.type, queueSize: this.queue.length });
21049
21682
  }
21050
21683
  /** 当前队列长度 */
21051
21684
  get size() {
@@ -21070,7 +21703,7 @@ var PushQueue = class {
21070
21703
  this.clearTimer();
21071
21704
  this.processTimer = setTimeout(() => {
21072
21705
  this.tick().catch((err) => {
21073
- log24.error("tick:unexpected-error", { error: err.message });
21706
+ log28.error("tick:unexpected-error", { error: err.message });
21074
21707
  if (this.running) {
21075
21708
  this.scheduleNext(this.idleIntervalMs);
21076
21709
  }
@@ -21092,7 +21725,7 @@ var PushQueue = class {
21092
21725
  async tick() {
21093
21726
  if (!this.running) return;
21094
21727
  if (!this.client.isHealthy) {
21095
- log24.debug("tick:service-unhealthy, pausing");
21728
+ log28.debug("tick:service-unhealthy, pausing");
21096
21729
  this.scheduleNext(this.unhealthyRetryMs);
21097
21730
  return;
21098
21731
  }
@@ -21112,7 +21745,7 @@ var PushQueue = class {
21112
21745
  try {
21113
21746
  await this.processItem(item);
21114
21747
  } catch (err) {
21115
- log24.error("tick:process-unexpected", { error: err.message });
21748
+ log28.error("tick:process-unexpected", { error: err.message });
21116
21749
  } finally {
21117
21750
  this.processing = false;
21118
21751
  }
@@ -21131,14 +21764,14 @@ var PushQueue = class {
21131
21764
  try {
21132
21765
  await this.client.push(item.payload);
21133
21766
  this.sentCount++;
21134
- log24.debug("push:success", {
21767
+ log28.debug("push:success", {
21135
21768
  type: item.payload.type,
21136
21769
  retries: item.retries
21137
21770
  });
21138
21771
  } catch (err) {
21139
21772
  if (err instanceof CockatooPushError && err.isClientError) {
21140
21773
  this.droppedCount++;
21141
- log24.warn("push:4xx-dropped", {
21774
+ log28.warn("push:4xx-dropped", {
21142
21775
  type: item.payload.type,
21143
21776
  status: err.status,
21144
21777
  retries: item.retries
@@ -21147,7 +21780,7 @@ var PushQueue = class {
21147
21780
  }
21148
21781
  if (item.retries >= this.retryAttempts) {
21149
21782
  this.droppedCount++;
21150
- log24.error("push:max-retries-dropped", {
21783
+ log28.error("push:max-retries-dropped", {
21151
21784
  type: item.payload.type,
21152
21785
  retries: item.retries,
21153
21786
  error: err.message
@@ -21160,7 +21793,7 @@ var PushQueue = class {
21160
21793
  item.eligibleAt = Date.now() + backoffMs;
21161
21794
  this.queue.push(item);
21162
21795
  const errorDetail = err instanceof CockatooPushError ? `HTTP ${err.status}` : err.message;
21163
- log24.warn("push:retry-enqueued", {
21796
+ log28.warn("push:retry-enqueued", {
21164
21797
  type: item.payload.type,
21165
21798
  retries: item.retries,
21166
21799
  backoffMs,
@@ -21184,22 +21817,22 @@ var PushQueue = class {
21184
21817
  try {
21185
21818
  await this.client.push(item.payload);
21186
21819
  this.sentCount++;
21187
- log24.debug("drain:sent", { type: item.payload.type });
21820
+ log28.debug("drain:sent", { type: item.payload.type });
21188
21821
  } catch (err) {
21189
21822
  this.droppedCount++;
21190
- log24.warn("drain:dropped", {
21823
+ log28.warn("drain:dropped", {
21191
21824
  type: item.payload.type,
21192
21825
  error: err.message
21193
21826
  });
21194
21827
  }
21195
21828
  }
21196
21829
  if (this.queue.length > 0) {
21197
- log24.warn("drain:timeout", {
21830
+ log28.warn("drain:timeout", {
21198
21831
  remaining: this.queue.length,
21199
21832
  timeoutMs: this.drainTimeoutMs
21200
21833
  });
21201
21834
  } else {
21202
- log24.info("drain:complete");
21835
+ log28.info("drain:complete");
21203
21836
  }
21204
21837
  }
21205
21838
  // ── 内部工具 ──
@@ -21213,10 +21846,10 @@ var PushQueue = class {
21213
21846
  };
21214
21847
 
21215
21848
  // src/index.ts
21216
- var log25 = createLogger("plugin");
21849
+ var log29 = createLogger("plugin");
21217
21850
  async function startPlugin(accountConfig, internalOverrides) {
21218
21851
  const config2 = buildPluginConfig(accountConfig, internalOverrides);
21219
- log25.info("Config built \u2713", { pluginId: config2.pluginId });
21852
+ log29.info("Config built \u2713", { pluginId: config2.pluginId });
21220
21853
  let cryptoEngine;
21221
21854
  if (config2.crypto.enabled && accountConfig.quantumAccount) {
21222
21855
  try {
@@ -21225,12 +21858,13 @@ async function startPlugin(accountConfig, internalOverrides) {
21225
21858
  appId: accountConfig.appId,
21226
21859
  quantumAccount: accountConfig.quantumAccount
21227
21860
  },
21228
- config2.env
21861
+ config2.env,
21862
+ config2.crypto.operationTimeoutMs
21229
21863
  );
21230
21864
  await cryptoEngine.init();
21231
- log25.info("Crypto initialized \u2713");
21865
+ log29.info("Crypto initialized \u2713");
21232
21866
  } catch (err) {
21233
- log25.warn("Crypto init failed, falling back to passthrough mode", {
21867
+ log29.warn("Crypto init failed, falling back to passthrough mode", {
21234
21868
  error: err instanceof Error ? err.message : String(err)
21235
21869
  });
21236
21870
  cryptoEngine = CryptoEngine.createPassthrough();
@@ -21238,7 +21872,7 @@ async function startPlugin(accountConfig, internalOverrides) {
21238
21872
  } else {
21239
21873
  cryptoEngine = CryptoEngine.createPassthrough();
21240
21874
  const reason = !config2.crypto.enabled ? "disabled by config" : "quantumAccount not provided";
21241
- log25.info(`Crypto passthrough mode \u2713 (${reason})`);
21875
+ log29.info(`Crypto passthrough mode \u2713 (${reason})`);
21242
21876
  }
21243
21877
  const oauthClient = new OAuthClient({
21244
21878
  baseUrl: config2.auth.serverUrl,
@@ -21252,7 +21886,7 @@ async function startPlugin(accountConfig, internalOverrides) {
21252
21886
  tokenStore,
21253
21887
  refreshAheadMs: config2.auth.refreshAheadMs
21254
21888
  });
21255
- log25.info("Auth initialized \u2713");
21889
+ log29.info("Auth initialized \u2713");
21256
21890
  let pushQueue = null;
21257
21891
  let cockatooClient = null;
21258
21892
  if (config2.push?.enabled) {
@@ -21267,7 +21901,7 @@ async function startPlugin(accountConfig, internalOverrides) {
21267
21901
  });
21268
21902
  cockatooClient.startHealthCheck();
21269
21903
  pushQueue.start();
21270
- log25.info("Push initialized \u2713");
21904
+ log29.info("Push initialized \u2713");
21271
21905
  }
21272
21906
  const wsClient = new WSClient();
21273
21907
  const dedup = new MessageDedup(config2.transport.dedup);
@@ -21278,7 +21912,11 @@ async function startPlugin(accountConfig, internalOverrides) {
21278
21912
  tokenFn: () => tokenManager.getValidToken(),
21279
21913
  messageServiceBaseUrl: config2.message.messageServiceBaseUrl,
21280
21914
  quantumAccount: accountConfig.quantumAccount,
21281
- encryptionMode: accountConfig.encryptionMode
21915
+ encryptionMode: accountConfig.encryptionMode,
21916
+ maxInboundConcurrency: config2.transport.maxInboundConcurrency,
21917
+ maxWaitingQueue: config2.transport.maxWaitingQueue,
21918
+ rateLimitPerSecond: config2.message.rateLimitPerSecond,
21919
+ rateLimitBurst: config2.message.rateLimitBurst
21282
21920
  });
21283
21921
  const connectionManager = new ConnectionManager(wsClient, {
21284
21922
  heartbeatIntervalMs: config2.transport.heartbeatIntervalMs,
@@ -21287,8 +21925,14 @@ async function startPlugin(accountConfig, internalOverrides) {
21287
21925
  reconnectMaxMs: config2.transport.reconnectMaxMs,
21288
21926
  maxReconnectAttempts: config2.transport.maxReconnectAttempts
21289
21927
  });
21290
- log25.info("Transport initialized \u2713");
21291
- log25.info("Plugin started \u2713");
21928
+ log29.info("Transport initialized \u2713");
21929
+ initUploadSemaphore(config2.file.maxUploadConcurrency);
21930
+ log29.info("Upload semaphore initialized \u2713", { maxConcurrency: config2.file.maxUploadConcurrency });
21931
+ if (config2.metrics.enabled) {
21932
+ metrics.startPeriodicLog(config2.metrics.logIntervalMs);
21933
+ log29.info("Metrics initialized \u2713", { intervalMs: config2.metrics.logIntervalMs });
21934
+ }
21935
+ log29.info("Plugin started \u2713");
21292
21936
  return {
21293
21937
  config: config2,
21294
21938
  messagePipe,
@@ -21296,12 +21940,14 @@ async function startPlugin(accountConfig, internalOverrides) {
21296
21940
  tokenManager,
21297
21941
  pushQueue,
21298
21942
  shutdown: async () => {
21299
- log25.info("Shutting down...");
21943
+ log29.info("Shutting down...");
21300
21944
  await connectionManager.stop();
21301
21945
  if (pushQueue) await pushQueue.stop();
21302
21946
  if (cockatooClient) cockatooClient.stopHealthCheck();
21303
21947
  tokenManager.shutdown();
21304
- log25.info("Shutdown complete \u2713");
21948
+ metrics.stop();
21949
+ destroyEncryptionMaps();
21950
+ log29.info("Shutdown complete \u2713");
21305
21951
  }
21306
21952
  };
21307
21953
  }
@@ -21312,7 +21958,7 @@ var plugin = {
21312
21958
  register(api) {
21313
21959
  setPluginRuntime(api.runtime);
21314
21960
  api.registerChannel({ plugin: quantumImPlugin });
21315
- log25.info("plugin registered \u2713");
21961
+ log29.info("plugin registered \u2713");
21316
21962
  }
21317
21963
  };
21318
21964
  var index_default = plugin;