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 +926 -280
- package/dist/index.d.cts +109 -1
- package/dist/setup-entry.cjs +925 -279
- package/package.json +1 -1
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,
|
|
4432
|
-
if (!
|
|
4431
|
+
function getElementAtPath(obj, path3) {
|
|
4432
|
+
if (!path3)
|
|
4433
4433
|
return obj;
|
|
4434
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
5047
|
-
for (const seg of
|
|
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
|
|
17025
|
-
if (
|
|
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 (
|
|
17030
|
-
const key =
|
|
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
|
|
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(
|
|
17653
|
+
function buildUrl(path3) {
|
|
17643
17654
|
const base = baseUrl.replace(/\/$/, "");
|
|
17644
|
-
const p =
|
|
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(
|
|
17687
|
-
const url2 = buildUrl(
|
|
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(
|
|
17698
|
-
const url2 = buildUrl(
|
|
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(
|
|
17710
|
-
const url2 = buildUrl(
|
|
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(
|
|
17754
|
-
const url2 = buildUrl(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(${
|
|
18116
|
+
operationContext: `uploadMedia(${safeFileName})`
|
|
17856
18117
|
});
|
|
17857
18118
|
}
|
|
17858
18119
|
const client = createHttpClient({ baseUrl: serverUrl, tokenManager, timeoutMs });
|
|
17859
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(${
|
|
18155
|
+
operationContext: `uploadMedia(${safeFileName}), chunk ${partNumber}/${chunks.length}`
|
|
17895
18156
|
});
|
|
17896
18157
|
}
|
|
17897
18158
|
if (isPartExpired(part)) {
|
|
17898
|
-
|
|
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
|
-
|
|
18170
|
+
log4.info("upload:refresh ok", { uploadId, refreshedParts: refreshResult.parts.length });
|
|
17910
18171
|
}
|
|
17911
|
-
const ctx = `uploadMedia(${
|
|
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
|
-
|
|
18175
|
+
log4.debug("upload:chunk ok", { uploadId, partNumber, etag: etag.slice(0, 16) });
|
|
17915
18176
|
}
|
|
17916
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
18200
|
+
const resolved = path2.resolve(filePath);
|
|
17940
18201
|
const isAllowed = allowedRoots.some((root) => {
|
|
17941
|
-
const resolvedRoot =
|
|
17942
|
-
return resolved === resolvedRoot || resolved.startsWith(resolvedRoot +
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
18411
|
+
const ext = path2.extname(fileName).replace(".", "").toLowerCase();
|
|
18151
18412
|
let imageDimensions;
|
|
18152
18413
|
if (fileType === "image") {
|
|
18153
18414
|
imageDimensions = getImageDimensions(buffer);
|
|
18154
|
-
|
|
18415
|
+
log5.info("media:imageDimensions", { fileName, ...imageDimensions });
|
|
18155
18416
|
}
|
|
18156
|
-
|
|
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
|
-
|
|
18426
|
+
log5.info("media:encrypted", { fileName, originalSize, encryptedSize: buffer.length });
|
|
18166
18427
|
} catch (err) {
|
|
18167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
18241
|
-
return
|
|
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
|
|
18249
|
-
var messageEncryptionStatus =
|
|
18250
|
-
|
|
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
|
-
|
|
18673
|
+
log7.info("sendText called", { to, textLength: text.length });
|
|
18292
18674
|
if (!messagePipeGetter) {
|
|
18293
|
-
|
|
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
|
-
|
|
18694
|
+
log7.info("sendMedia called", { to, mediaUrl });
|
|
18313
18695
|
if (!messagePipeGetter || !sdkRuntimeGetter || !tokenManagerGetter || !pluginConfigGetter) {
|
|
18314
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
19066
|
+
log14.warn("\u{1F4E4} \u5A92\u4F53\u53D1\u9001\u964D\u7EA7", { chatId, mediaUrl, warning: result.warning });
|
|
18685
19067
|
} else {
|
|
18686
|
-
|
|
19068
|
+
log14.info("\u{1F4E4} \u5A92\u4F53\u6D88\u606F\u5DF2\u53D1\u9001", { chatId, mediaUrl });
|
|
18687
19069
|
}
|
|
18688
19070
|
} catch (err) {
|
|
18689
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19705
|
+
log16.info(`liangzimixin[${ctx.accountId}] received abort signal, shutting down`);
|
|
19309
19706
|
ctx.abortSignal?.removeEventListener("abort", onAbort);
|
|
19310
|
-
instance.shutdown().catch((err) =>
|
|
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
|
-
|
|
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
|
-
|
|
19723
|
+
log16.info(`stopping liangzimixin[${ctx.accountId}]`);
|
|
19327
19724
|
if (activeInstance) {
|
|
19328
19725
|
await activeInstance.shutdown();
|
|
19329
19726
|
activeInstance = null;
|
|
19330
19727
|
}
|
|
19331
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
19516
|
-
|
|
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
|
-
|
|
19959
|
+
log18.debug("Decrypt (passthrough)", { keyId });
|
|
19534
19960
|
return ciphertext;
|
|
19535
19961
|
}
|
|
19536
|
-
const
|
|
19537
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
19658
|
-
const url2 = `${this.baseUrl}${
|
|
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
|
-
|
|
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
|
-
|
|
19681
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20283
|
+
log20.debug("Token loaded", { key, path: filePath });
|
|
19842
20284
|
return data;
|
|
19843
20285
|
} catch (err) {
|
|
19844
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20381
|
+
log21.info("Token revoked and cleared");
|
|
19940
20382
|
}
|
|
19941
20383
|
/** 清理定时器和并发锁 — 优雅关闭时调用 */
|
|
19942
20384
|
shutdown() {
|
|
19943
20385
|
this.clearRefreshTimer();
|
|
19944
20386
|
this.refreshLock = null;
|
|
19945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20444
|
+
log21.info("Stored token has expired, acquiring new one");
|
|
20003
20445
|
}
|
|
20004
20446
|
} catch {
|
|
20005
|
-
|
|
20447
|
+
log21.warn("Failed to load token from store, acquiring new one");
|
|
20006
20448
|
}
|
|
20007
|
-
|
|
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
|
-
|
|
20480
|
+
log21.debug("Token already within refresh window, not scheduling timer");
|
|
20039
20481
|
return;
|
|
20040
20482
|
}
|
|
20041
|
-
|
|
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
|
-
|
|
20489
|
+
log21.info("Scheduled token refresh triggered");
|
|
20048
20490
|
this.cachedToken = null;
|
|
20049
20491
|
this.currentTokenData = null;
|
|
20050
20492
|
await this.getValidToken();
|
|
20051
|
-
|
|
20493
|
+
log21.info("Scheduled token refresh completed");
|
|
20052
20494
|
} catch (err) {
|
|
20053
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
20548
|
+
log22.info("ws:connecting", { url: url2 });
|
|
20107
20549
|
return new Promise((resolve3, reject) => {
|
|
20108
|
-
const ws = new wrapper_default(url2, protocols, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20818
|
+
log25.error("\u274C \u5165\u7AD9\u5904\u7406\u5F02\u5E38", { step: "handleInbound", error: err.message });
|
|
20239
20819
|
});
|
|
20240
20820
|
});
|
|
20241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20964
|
+
log25.debug("\u{1F512} \u51FA\u7AD9\u6D88\u606F\u5DF2\u52A0\u5BC6 (quantum_and_plain)", { sessionId: keyId });
|
|
20381
20965
|
}
|
|
20382
20966
|
} catch (err) {
|
|
20383
|
-
|
|
20384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
20442
|
-
const url2 = `${this.messageServiceBaseUrl}${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21215
|
+
log25.debug("\u{1F4E8} \u89E3\u6790 WS \u5E27", { messageId: msg.messageId, eventType: callbackData.eventType });
|
|
20592
21216
|
if (this.dedup.isDuplicate(msg.messageId)) {
|
|
20593
|
-
|
|
21217
|
+
log25.debug("\u{1F501} \u91CD\u590D\u6D88\u606F\u5DF2\u8DF3\u8FC7", { messageId: msg.messageId });
|
|
20594
21218
|
return;
|
|
20595
21219
|
}
|
|
20596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21243
|
+
log25.info("\u{1F4A1} \u5DF2\u53D1\u9001\u6A21\u5F0F\u63D0\u793A\u6D88\u606F", { chatId });
|
|
20620
21244
|
} catch (err) {
|
|
20621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21349
|
+
log26.warn("ws:disconnected, scheduling reconnect");
|
|
20718
21350
|
this.scheduleReconnect();
|
|
20719
21351
|
}
|
|
20720
21352
|
});
|
|
20721
21353
|
this.client.on("error", (err) => {
|
|
20722
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21368
|
+
log26.warn("heartbeat: connection not open, scheduling reconnect");
|
|
20737
21369
|
this.scheduleReconnect();
|
|
20738
21370
|
return;
|
|
20739
21371
|
}
|
|
20740
21372
|
if (!this.pongReceived) {
|
|
20741
|
-
|
|
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
|
-
|
|
21380
|
+
log26.debug("heartbeat: ping sent");
|
|
20749
21381
|
this.clearPongTimeout();
|
|
20750
21382
|
this.pongTimeoutTimer = setTimeout(() => {
|
|
20751
21383
|
if (!this.pongReceived && this.running) {
|
|
20752
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21441
|
+
log26.info("ws:reconnected", { attempt: this.reconnectAttempts, url: this.url });
|
|
20809
21442
|
return;
|
|
20810
21443
|
} catch (err) {
|
|
20811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21553
|
+
log27.warn("health-check:unhealthy", { status: resp.status });
|
|
20921
21554
|
}
|
|
20922
21555
|
return isHealthy;
|
|
20923
21556
|
} catch (err) {
|
|
20924
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21657
|
+
log28.info("PushQueue draining", { remaining: this.queue.length });
|
|
21025
21658
|
await this.drain();
|
|
21026
21659
|
}
|
|
21027
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21820
|
+
log28.debug("drain:sent", { type: item.payload.type });
|
|
21188
21821
|
} catch (err) {
|
|
21189
21822
|
this.droppedCount++;
|
|
21190
|
-
|
|
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
|
-
|
|
21830
|
+
log28.warn("drain:timeout", {
|
|
21198
21831
|
remaining: this.queue.length,
|
|
21199
21832
|
timeoutMs: this.drainTimeoutMs
|
|
21200
21833
|
});
|
|
21201
21834
|
} else {
|
|
21202
|
-
|
|
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
|
|
21849
|
+
var log29 = createLogger("plugin");
|
|
21217
21850
|
async function startPlugin(accountConfig, internalOverrides) {
|
|
21218
21851
|
const config2 = buildPluginConfig(accountConfig, internalOverrides);
|
|
21219
|
-
|
|
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
|
-
|
|
21865
|
+
log29.info("Crypto initialized \u2713");
|
|
21232
21866
|
} catch (err) {
|
|
21233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21961
|
+
log29.info("plugin registered \u2713");
|
|
21316
21962
|
}
|
|
21317
21963
|
};
|
|
21318
21964
|
var index_default = plugin;
|