liangzimixin 0.3.35 → 0.3.37
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 +103 -6
- package/dist/index.d.cts +7 -1
- package/dist/setup-entry.cjs +103 -6
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -17956,6 +17956,56 @@ function detectFileType(fileName) {
|
|
|
17956
17956
|
if (VIDEO_EXTS.has(ext)) return "video";
|
|
17957
17957
|
return "file";
|
|
17958
17958
|
}
|
|
17959
|
+
var MIME_MAP = {
|
|
17960
|
+
// 图片
|
|
17961
|
+
".jpg": "image/jpeg",
|
|
17962
|
+
".jpeg": "image/jpeg",
|
|
17963
|
+
".png": "image/png",
|
|
17964
|
+
".gif": "image/gif",
|
|
17965
|
+
".bmp": "image/bmp",
|
|
17966
|
+
".webp": "image/webp",
|
|
17967
|
+
".svg": "image/svg+xml",
|
|
17968
|
+
".ico": "image/x-icon",
|
|
17969
|
+
".tiff": "image/tiff",
|
|
17970
|
+
".tif": "image/tiff",
|
|
17971
|
+
// 音频
|
|
17972
|
+
".mp3": "audio/mpeg",
|
|
17973
|
+
".wav": "audio/wav",
|
|
17974
|
+
".ogg": "audio/ogg",
|
|
17975
|
+
".opus": "audio/opus",
|
|
17976
|
+
".flac": "audio/flac",
|
|
17977
|
+
".aac": "audio/aac",
|
|
17978
|
+
".m4a": "audio/mp4",
|
|
17979
|
+
".wma": "audio/x-ms-wma",
|
|
17980
|
+
// 视频
|
|
17981
|
+
".mp4": "video/mp4",
|
|
17982
|
+
".mov": "video/quicktime",
|
|
17983
|
+
".avi": "video/x-msvideo",
|
|
17984
|
+
".mkv": "video/x-matroska",
|
|
17985
|
+
".wmv": "video/x-ms-wmv",
|
|
17986
|
+
".flv": "video/x-flv",
|
|
17987
|
+
".webm": "video/webm",
|
|
17988
|
+
".m4v": "video/mp4",
|
|
17989
|
+
// 文档
|
|
17990
|
+
".pdf": "application/pdf",
|
|
17991
|
+
".doc": "application/msword",
|
|
17992
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
17993
|
+
".xls": "application/vnd.ms-excel",
|
|
17994
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
17995
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
17996
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
17997
|
+
".txt": "text/plain",
|
|
17998
|
+
".csv": "text/csv",
|
|
17999
|
+
".json": "application/json",
|
|
18000
|
+
".zip": "application/zip",
|
|
18001
|
+
".rar": "application/x-rar-compressed",
|
|
18002
|
+
".7z": "application/x-7z-compressed",
|
|
18003
|
+
".gz": "application/gzip"
|
|
18004
|
+
};
|
|
18005
|
+
function inferMimeType(fileName) {
|
|
18006
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
18007
|
+
return MIME_MAP[ext] || "application/octet-stream";
|
|
18008
|
+
}
|
|
17959
18009
|
async function resolveAndUploadMedia(params) {
|
|
17960
18010
|
const {
|
|
17961
18011
|
mediaUrl,
|
|
@@ -18005,12 +18055,14 @@ async function resolveAndUploadMedia(params) {
|
|
|
18005
18055
|
});
|
|
18006
18056
|
}
|
|
18007
18057
|
const fileType = detectFileType(fileName);
|
|
18008
|
-
|
|
18058
|
+
const mimeType = inferMimeType(fileName);
|
|
18059
|
+
log3.info("media:fileType", { fileName, fileType, mimeType });
|
|
18009
18060
|
let uploadResult;
|
|
18010
18061
|
try {
|
|
18011
18062
|
uploadResult = await uploadMedia({
|
|
18012
18063
|
file: buffer,
|
|
18013
18064
|
fileName,
|
|
18065
|
+
mimeType,
|
|
18014
18066
|
tokenManager,
|
|
18015
18067
|
serverUrl,
|
|
18016
18068
|
maxFileSizeMb,
|
|
@@ -18027,7 +18079,14 @@ async function resolveAndUploadMedia(params) {
|
|
|
18027
18079
|
};
|
|
18028
18080
|
}
|
|
18029
18081
|
const msgType = fileType;
|
|
18030
|
-
|
|
18082
|
+
let contentPayload;
|
|
18083
|
+
if (fileType === "file") {
|
|
18084
|
+
contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize, mimeType };
|
|
18085
|
+
} else if (fileType === "image") {
|
|
18086
|
+
contentPayload = { fileId: uploadResult.fileKey };
|
|
18087
|
+
} else {
|
|
18088
|
+
contentPayload = { fileId: uploadResult.fileKey, mimeType };
|
|
18089
|
+
}
|
|
18031
18090
|
await messagePipe.sendMessage({
|
|
18032
18091
|
chatId,
|
|
18033
18092
|
senderId: chatId,
|
|
@@ -18929,7 +18988,11 @@ var quantumImPlugin = {
|
|
|
18929
18988
|
})
|
|
18930
18989
|
},
|
|
18931
18990
|
messaging: {
|
|
18932
|
-
normalizeTarget: (raw) =>
|
|
18991
|
+
normalizeTarget: (raw) => {
|
|
18992
|
+
if (!raw) return void 0;
|
|
18993
|
+
const normalized = parseChannelAddress(raw).trim();
|
|
18994
|
+
return normalized || void 0;
|
|
18995
|
+
}
|
|
18933
18996
|
},
|
|
18934
18997
|
outbound: quantumImOutbound,
|
|
18935
18998
|
setup: {
|
|
@@ -19865,6 +19928,9 @@ var WSClient = class extends import_node_events.EventEmitter {
|
|
|
19865
19928
|
ws.on("message", (data) => {
|
|
19866
19929
|
this.emit("message", data);
|
|
19867
19930
|
});
|
|
19931
|
+
ws.on("pong", () => {
|
|
19932
|
+
this.emit("pong");
|
|
19933
|
+
});
|
|
19868
19934
|
ws.on("close", (code, reason) => {
|
|
19869
19935
|
log19.info("ws:closed", { code, reason: reason.toString() });
|
|
19870
19936
|
this.emit("close", code, reason.toString());
|
|
@@ -19887,6 +19953,12 @@ var WSClient = class extends import_node_events.EventEmitter {
|
|
|
19887
19953
|
}
|
|
19888
19954
|
this.ws.send(data);
|
|
19889
19955
|
}
|
|
19956
|
+
/** 发送 WebSocket 协议级 Ping 帧 */
|
|
19957
|
+
ping() {
|
|
19958
|
+
if (this.ws && this.ws.readyState === wrapper_default.OPEN) {
|
|
19959
|
+
this.ws.ping();
|
|
19960
|
+
}
|
|
19961
|
+
}
|
|
19890
19962
|
/** 关闭连接 */
|
|
19891
19963
|
close(code, reason) {
|
|
19892
19964
|
if (this.ws) {
|
|
@@ -20243,6 +20315,8 @@ var ConnectionManager = class {
|
|
|
20243
20315
|
appId = "";
|
|
20244
20316
|
/** 是否正在重连中 (防止并发重连) */
|
|
20245
20317
|
reconnecting = false;
|
|
20318
|
+
/** 是否收到了最近一次 pong 响应 */
|
|
20319
|
+
pongReceived = true;
|
|
20246
20320
|
constructor(client, options) {
|
|
20247
20321
|
this.client = client;
|
|
20248
20322
|
this.options = {
|
|
@@ -20276,6 +20350,15 @@ var ConnectionManager = class {
|
|
|
20276
20350
|
...this.appId ? { "X-App-ID": this.appId } : {}
|
|
20277
20351
|
}
|
|
20278
20352
|
});
|
|
20353
|
+
this.registerEvents();
|
|
20354
|
+
this.startHeartbeat();
|
|
20355
|
+
log22.info("ConnectionManager started \u2713", { url: url2 });
|
|
20356
|
+
}
|
|
20357
|
+
/** 注册 close / error / pong 事件 */
|
|
20358
|
+
registerEvents() {
|
|
20359
|
+
this.client.removeAllListeners("close");
|
|
20360
|
+
this.client.removeAllListeners("error");
|
|
20361
|
+
this.client.removeAllListeners("pong");
|
|
20279
20362
|
this.client.on("close", () => {
|
|
20280
20363
|
if (this.running) {
|
|
20281
20364
|
log22.warn("ws:disconnected, scheduling reconnect");
|
|
@@ -20285,17 +20368,30 @@ var ConnectionManager = class {
|
|
|
20285
20368
|
this.client.on("error", (err) => {
|
|
20286
20369
|
log22.error("ws:connection-error", { error: err.message });
|
|
20287
20370
|
});
|
|
20288
|
-
this.
|
|
20289
|
-
|
|
20371
|
+
this.client.on("pong", () => {
|
|
20372
|
+
this.pongReceived = true;
|
|
20373
|
+
log22.info("heartbeat: pong received");
|
|
20374
|
+
});
|
|
20290
20375
|
}
|
|
20291
|
-
/** 启动心跳保活 —
|
|
20376
|
+
/** 启动心跳保活 — 定时发送 WebSocket Ping 帧 */
|
|
20292
20377
|
startHeartbeat() {
|
|
20293
20378
|
this.stopHeartbeat();
|
|
20379
|
+
this.pongReceived = true;
|
|
20294
20380
|
this.heartbeatTimer = setInterval(() => {
|
|
20295
20381
|
if (this.client.readyState !== wrapper_default.OPEN) {
|
|
20296
20382
|
log22.warn("heartbeat: connection not open, scheduling reconnect");
|
|
20297
20383
|
this.scheduleReconnect();
|
|
20384
|
+
return;
|
|
20385
|
+
}
|
|
20386
|
+
if (!this.pongReceived) {
|
|
20387
|
+
log22.warn("heartbeat: pong timeout, connection dead");
|
|
20388
|
+
this.client.close(4e3, "pong timeout");
|
|
20389
|
+
this.scheduleReconnect();
|
|
20390
|
+
return;
|
|
20298
20391
|
}
|
|
20392
|
+
this.pongReceived = false;
|
|
20393
|
+
this.client.ping();
|
|
20394
|
+
log22.info("heartbeat: ping sent");
|
|
20299
20395
|
}, this.options.heartbeatIntervalMs);
|
|
20300
20396
|
}
|
|
20301
20397
|
/** 停止心跳定时器 */
|
|
@@ -20337,6 +20433,7 @@ var ConnectionManager = class {
|
|
|
20337
20433
|
}
|
|
20338
20434
|
});
|
|
20339
20435
|
this.reconnectAttempts = 0;
|
|
20436
|
+
this.registerEvents();
|
|
20340
20437
|
this.startHeartbeat();
|
|
20341
20438
|
log22.info("ws:reconnected", { attempt: this.reconnectAttempts, url: this.url });
|
|
20342
20439
|
return;
|
package/dist/index.d.cts
CHANGED
|
@@ -684,6 +684,8 @@ declare class WSClient extends EventEmitter {
|
|
|
684
684
|
connect(options: WSClientOptions): Promise<void>;
|
|
685
685
|
/** 发送数据到服务器 — 前置检查连接状态 */
|
|
686
686
|
send(data: string | Buffer): void;
|
|
687
|
+
/** 发送 WebSocket 协议级 Ping 帧 */
|
|
688
|
+
ping(): void;
|
|
687
689
|
/** 关闭连接 */
|
|
688
690
|
close(code?: number, reason?: string): void;
|
|
689
691
|
/** 当前连接状态 (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED) */
|
|
@@ -865,6 +867,8 @@ declare class ConnectionManager {
|
|
|
865
867
|
private appId;
|
|
866
868
|
/** 是否正在重连中 (防止并发重连) */
|
|
867
869
|
private reconnecting;
|
|
870
|
+
/** 是否收到了最近一次 pong 响应 */
|
|
871
|
+
private pongReceived;
|
|
868
872
|
constructor(client: WSClient, options?: ConnectionManagerOptions);
|
|
869
873
|
/**
|
|
870
874
|
* 启动连接 — 连接 WS + 开始心跳 + 注册重连逻辑
|
|
@@ -873,7 +877,9 @@ declare class ConnectionManager {
|
|
|
873
877
|
* @param appId - 应用 ID (用于 X-App-ID 头)
|
|
874
878
|
*/
|
|
875
879
|
start(url: string, tokenFn: () => Promise<string>, appId?: string): Promise<void>;
|
|
876
|
-
/**
|
|
880
|
+
/** 注册 close / error / pong 事件 */
|
|
881
|
+
private registerEvents;
|
|
882
|
+
/** 启动心跳保活 — 定时发送 WebSocket Ping 帧 */
|
|
877
883
|
private startHeartbeat;
|
|
878
884
|
/** 停止心跳定时器 */
|
|
879
885
|
private stopHeartbeat;
|
package/dist/setup-entry.cjs
CHANGED
|
@@ -4051,6 +4051,56 @@ function detectFileType(fileName) {
|
|
|
4051
4051
|
if (VIDEO_EXTS.has(ext)) return "video";
|
|
4052
4052
|
return "file";
|
|
4053
4053
|
}
|
|
4054
|
+
var MIME_MAP = {
|
|
4055
|
+
// 图片
|
|
4056
|
+
".jpg": "image/jpeg",
|
|
4057
|
+
".jpeg": "image/jpeg",
|
|
4058
|
+
".png": "image/png",
|
|
4059
|
+
".gif": "image/gif",
|
|
4060
|
+
".bmp": "image/bmp",
|
|
4061
|
+
".webp": "image/webp",
|
|
4062
|
+
".svg": "image/svg+xml",
|
|
4063
|
+
".ico": "image/x-icon",
|
|
4064
|
+
".tiff": "image/tiff",
|
|
4065
|
+
".tif": "image/tiff",
|
|
4066
|
+
// 音频
|
|
4067
|
+
".mp3": "audio/mpeg",
|
|
4068
|
+
".wav": "audio/wav",
|
|
4069
|
+
".ogg": "audio/ogg",
|
|
4070
|
+
".opus": "audio/opus",
|
|
4071
|
+
".flac": "audio/flac",
|
|
4072
|
+
".aac": "audio/aac",
|
|
4073
|
+
".m4a": "audio/mp4",
|
|
4074
|
+
".wma": "audio/x-ms-wma",
|
|
4075
|
+
// 视频
|
|
4076
|
+
".mp4": "video/mp4",
|
|
4077
|
+
".mov": "video/quicktime",
|
|
4078
|
+
".avi": "video/x-msvideo",
|
|
4079
|
+
".mkv": "video/x-matroska",
|
|
4080
|
+
".wmv": "video/x-ms-wmv",
|
|
4081
|
+
".flv": "video/x-flv",
|
|
4082
|
+
".webm": "video/webm",
|
|
4083
|
+
".m4v": "video/mp4",
|
|
4084
|
+
// 文档
|
|
4085
|
+
".pdf": "application/pdf",
|
|
4086
|
+
".doc": "application/msword",
|
|
4087
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
4088
|
+
".xls": "application/vnd.ms-excel",
|
|
4089
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
4090
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
4091
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
4092
|
+
".txt": "text/plain",
|
|
4093
|
+
".csv": "text/csv",
|
|
4094
|
+
".json": "application/json",
|
|
4095
|
+
".zip": "application/zip",
|
|
4096
|
+
".rar": "application/x-rar-compressed",
|
|
4097
|
+
".7z": "application/x-7z-compressed",
|
|
4098
|
+
".gz": "application/gzip"
|
|
4099
|
+
};
|
|
4100
|
+
function inferMimeType(fileName) {
|
|
4101
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
4102
|
+
return MIME_MAP[ext] || "application/octet-stream";
|
|
4103
|
+
}
|
|
4054
4104
|
async function resolveAndUploadMedia(params) {
|
|
4055
4105
|
const {
|
|
4056
4106
|
mediaUrl,
|
|
@@ -4100,12 +4150,14 @@ async function resolveAndUploadMedia(params) {
|
|
|
4100
4150
|
});
|
|
4101
4151
|
}
|
|
4102
4152
|
const fileType = detectFileType(fileName);
|
|
4103
|
-
|
|
4153
|
+
const mimeType = inferMimeType(fileName);
|
|
4154
|
+
log3.info("media:fileType", { fileName, fileType, mimeType });
|
|
4104
4155
|
let uploadResult;
|
|
4105
4156
|
try {
|
|
4106
4157
|
uploadResult = await uploadMedia({
|
|
4107
4158
|
file: buffer,
|
|
4108
4159
|
fileName,
|
|
4160
|
+
mimeType,
|
|
4109
4161
|
tokenManager,
|
|
4110
4162
|
serverUrl,
|
|
4111
4163
|
maxFileSizeMb,
|
|
@@ -4122,7 +4174,14 @@ async function resolveAndUploadMedia(params) {
|
|
|
4122
4174
|
};
|
|
4123
4175
|
}
|
|
4124
4176
|
const msgType = fileType;
|
|
4125
|
-
|
|
4177
|
+
let contentPayload;
|
|
4178
|
+
if (fileType === "file") {
|
|
4179
|
+
contentPayload = { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize, mimeType };
|
|
4180
|
+
} else if (fileType === "image") {
|
|
4181
|
+
contentPayload = { fileId: uploadResult.fileKey };
|
|
4182
|
+
} else {
|
|
4183
|
+
contentPayload = { fileId: uploadResult.fileKey, mimeType };
|
|
4184
|
+
}
|
|
4126
4185
|
await messagePipe.sendMessage({
|
|
4127
4186
|
chatId,
|
|
4128
4187
|
senderId: chatId,
|
|
@@ -18928,6 +18987,9 @@ var WSClient = class extends import_node_events.EventEmitter {
|
|
|
18928
18987
|
ws.on("message", (data) => {
|
|
18929
18988
|
this.emit("message", data);
|
|
18930
18989
|
});
|
|
18990
|
+
ws.on("pong", () => {
|
|
18991
|
+
this.emit("pong");
|
|
18992
|
+
});
|
|
18931
18993
|
ws.on("close", (code, reason) => {
|
|
18932
18994
|
log10.info("ws:closed", { code, reason: reason.toString() });
|
|
18933
18995
|
this.emit("close", code, reason.toString());
|
|
@@ -18950,6 +19012,12 @@ var WSClient = class extends import_node_events.EventEmitter {
|
|
|
18950
19012
|
}
|
|
18951
19013
|
this.ws.send(data);
|
|
18952
19014
|
}
|
|
19015
|
+
/** 发送 WebSocket 协议级 Ping 帧 */
|
|
19016
|
+
ping() {
|
|
19017
|
+
if (this.ws && this.ws.readyState === wrapper_default.OPEN) {
|
|
19018
|
+
this.ws.ping();
|
|
19019
|
+
}
|
|
19020
|
+
}
|
|
18953
19021
|
/** 关闭连接 */
|
|
18954
19022
|
close(code, reason) {
|
|
18955
19023
|
if (this.ws) {
|
|
@@ -19306,6 +19374,8 @@ var ConnectionManager = class {
|
|
|
19306
19374
|
appId = "";
|
|
19307
19375
|
/** 是否正在重连中 (防止并发重连) */
|
|
19308
19376
|
reconnecting = false;
|
|
19377
|
+
/** 是否收到了最近一次 pong 响应 */
|
|
19378
|
+
pongReceived = true;
|
|
19309
19379
|
constructor(client, options) {
|
|
19310
19380
|
this.client = client;
|
|
19311
19381
|
this.options = {
|
|
@@ -19339,6 +19409,15 @@ var ConnectionManager = class {
|
|
|
19339
19409
|
...this.appId ? { "X-App-ID": this.appId } : {}
|
|
19340
19410
|
}
|
|
19341
19411
|
});
|
|
19412
|
+
this.registerEvents();
|
|
19413
|
+
this.startHeartbeat();
|
|
19414
|
+
log13.info("ConnectionManager started \u2713", { url: url2 });
|
|
19415
|
+
}
|
|
19416
|
+
/** 注册 close / error / pong 事件 */
|
|
19417
|
+
registerEvents() {
|
|
19418
|
+
this.client.removeAllListeners("close");
|
|
19419
|
+
this.client.removeAllListeners("error");
|
|
19420
|
+
this.client.removeAllListeners("pong");
|
|
19342
19421
|
this.client.on("close", () => {
|
|
19343
19422
|
if (this.running) {
|
|
19344
19423
|
log13.warn("ws:disconnected, scheduling reconnect");
|
|
@@ -19348,17 +19427,30 @@ var ConnectionManager = class {
|
|
|
19348
19427
|
this.client.on("error", (err) => {
|
|
19349
19428
|
log13.error("ws:connection-error", { error: err.message });
|
|
19350
19429
|
});
|
|
19351
|
-
this.
|
|
19352
|
-
|
|
19430
|
+
this.client.on("pong", () => {
|
|
19431
|
+
this.pongReceived = true;
|
|
19432
|
+
log13.info("heartbeat: pong received");
|
|
19433
|
+
});
|
|
19353
19434
|
}
|
|
19354
|
-
/** 启动心跳保活 —
|
|
19435
|
+
/** 启动心跳保活 — 定时发送 WebSocket Ping 帧 */
|
|
19355
19436
|
startHeartbeat() {
|
|
19356
19437
|
this.stopHeartbeat();
|
|
19438
|
+
this.pongReceived = true;
|
|
19357
19439
|
this.heartbeatTimer = setInterval(() => {
|
|
19358
19440
|
if (this.client.readyState !== wrapper_default.OPEN) {
|
|
19359
19441
|
log13.warn("heartbeat: connection not open, scheduling reconnect");
|
|
19360
19442
|
this.scheduleReconnect();
|
|
19443
|
+
return;
|
|
19361
19444
|
}
|
|
19445
|
+
if (!this.pongReceived) {
|
|
19446
|
+
log13.warn("heartbeat: pong timeout, connection dead");
|
|
19447
|
+
this.client.close(4e3, "pong timeout");
|
|
19448
|
+
this.scheduleReconnect();
|
|
19449
|
+
return;
|
|
19450
|
+
}
|
|
19451
|
+
this.pongReceived = false;
|
|
19452
|
+
this.client.ping();
|
|
19453
|
+
log13.info("heartbeat: ping sent");
|
|
19362
19454
|
}, this.options.heartbeatIntervalMs);
|
|
19363
19455
|
}
|
|
19364
19456
|
/** 停止心跳定时器 */
|
|
@@ -19400,6 +19492,7 @@ var ConnectionManager = class {
|
|
|
19400
19492
|
}
|
|
19401
19493
|
});
|
|
19402
19494
|
this.reconnectAttempts = 0;
|
|
19495
|
+
this.registerEvents();
|
|
19403
19496
|
this.startHeartbeat();
|
|
19404
19497
|
log13.info("ws:reconnected", { attempt: this.reconnectAttempts, url: this.url });
|
|
19405
19498
|
return;
|
|
@@ -20661,7 +20754,11 @@ var quantumImPlugin = {
|
|
|
20661
20754
|
})
|
|
20662
20755
|
},
|
|
20663
20756
|
messaging: {
|
|
20664
|
-
normalizeTarget: (raw) =>
|
|
20757
|
+
normalizeTarget: (raw) => {
|
|
20758
|
+
if (!raw) return void 0;
|
|
20759
|
+
const normalized = parseChannelAddress(raw).trim();
|
|
20760
|
+
return normalized || void 0;
|
|
20761
|
+
}
|
|
20665
20762
|
},
|
|
20666
20763
|
outbound: quantumImOutbound,
|
|
20667
20764
|
setup: {
|