@koi-design/callkit 2.3.0-beta.1 → 2.3.0-beta.11
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.d.ts +22 -1
- package/dist/index.global.js +275 -34
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +270 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +270 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -72,7 +72,7 @@ var Api = class {
|
|
|
72
72
|
const res = await this.post({
|
|
73
73
|
url: "/auth/agentUser/login",
|
|
74
74
|
method: "post",
|
|
75
|
-
data: params
|
|
75
|
+
data: { ...params, browserVersion: navigator.userAgent }
|
|
76
76
|
});
|
|
77
77
|
return res;
|
|
78
78
|
} finally {
|
|
@@ -535,7 +535,11 @@ var SocketReceiveEvent = {
|
|
|
535
535
|
ERROR: "ERROR",
|
|
536
536
|
SESSION_ERROR: "SESSION_ERROR",
|
|
537
537
|
WAITING_QUEUE: "WAITING_QUEUE",
|
|
538
|
-
CUSTOMER_MATCH_BLACK_PHONE: "CUSTOMER_MATCH_BLACK_PHONE"
|
|
538
|
+
CUSTOMER_MATCH_BLACK_PHONE: "CUSTOMER_MATCH_BLACK_PHONE",
|
|
539
|
+
/**
|
|
540
|
+
* Agent RTP loss
|
|
541
|
+
*/
|
|
542
|
+
AGENT_RTP_LOSS: "AGENT_RTP_LOSS"
|
|
539
543
|
};
|
|
540
544
|
var EncryptionMethod = {
|
|
541
545
|
NONE: "NONE",
|
|
@@ -724,7 +728,7 @@ var Call = class {
|
|
|
724
728
|
// package.json
|
|
725
729
|
var package_default = {
|
|
726
730
|
name: "@koi-design/callkit",
|
|
727
|
-
version: "2.3.0-beta.
|
|
731
|
+
version: "2.3.0-beta.11",
|
|
728
732
|
description: "callkit",
|
|
729
733
|
author: "koi",
|
|
730
734
|
license: "ISC",
|
|
@@ -15824,8 +15828,6 @@ var Connect = class {
|
|
|
15824
15828
|
});
|
|
15825
15829
|
return;
|
|
15826
15830
|
}
|
|
15827
|
-
const { userInfo } = this.callKit.config.getConfig();
|
|
15828
|
-
const { userPart, fsIp, fsPort } = userInfo;
|
|
15829
15831
|
const { registererOptions = {} } = this.reconnectConfig;
|
|
15830
15832
|
this.registerer = new Registerer(this.userAgent, registererOptions);
|
|
15831
15833
|
this.registerer.stateChange.addListener((state) => {
|
|
@@ -15859,27 +15861,26 @@ var Connect = class {
|
|
|
15859
15861
|
});
|
|
15860
15862
|
this.setRegister(true);
|
|
15861
15863
|
if (this.isReConnected) {
|
|
15862
|
-
if (this.
|
|
15863
|
-
const selfUri = `sip:manualCallAgent${userPart}@${fsIp}:${fsPort}`;
|
|
15864
|
+
if (this.canReferInCallToSelf()) {
|
|
15864
15865
|
this.callKit.logger.info(
|
|
15865
15866
|
"Reconnected, referring active session to self",
|
|
15866
15867
|
{
|
|
15867
15868
|
caller: "Connect.setupRegisterer.registererStateChange",
|
|
15868
15869
|
type: "SIP",
|
|
15869
15870
|
content: {
|
|
15870
|
-
selfUri,
|
|
15871
|
+
selfUri: this.getSelfReferUri(),
|
|
15871
15872
|
sessionState: this.currentSession.state,
|
|
15872
15873
|
connectStatus: this.connectStatus
|
|
15873
15874
|
}
|
|
15874
15875
|
}
|
|
15875
15876
|
);
|
|
15876
|
-
this.
|
|
15877
|
+
this.referInCallToSelf().catch((err) => {
|
|
15877
15878
|
this.callKit.logger.error(err, {
|
|
15878
15879
|
caller: "Connect.setupRegisterer.registererStateChange",
|
|
15879
15880
|
type: "SIP",
|
|
15880
15881
|
content: {
|
|
15881
15882
|
errCode: ErrorCode.WEBRTC_CALL_INVITE_ERROR,
|
|
15882
|
-
selfUri
|
|
15883
|
+
selfUri: this.getSelfReferUri()
|
|
15883
15884
|
}
|
|
15884
15885
|
});
|
|
15885
15886
|
});
|
|
@@ -16708,6 +16709,199 @@ var Connect = class {
|
|
|
16708
16709
|
}
|
|
16709
16710
|
this.currentSession.refer(target, extra?.sessionReferOptions);
|
|
16710
16711
|
}
|
|
16712
|
+
/**
|
|
16713
|
+
* Get the SIP URI for "refer to self" (same as reconnection refer logic).
|
|
16714
|
+
* Shared by referInCallToSelf and internal reconnection refer.
|
|
16715
|
+
*/
|
|
16716
|
+
getSelfReferUri() {
|
|
16717
|
+
const { userInfo } = this.callKit.config.getConfig();
|
|
16718
|
+
const { userPart, fsIp, fsPort } = userInfo;
|
|
16719
|
+
return `sip:manualCallAgent${userPart}@${fsIp}:${fsPort}`;
|
|
16720
|
+
}
|
|
16721
|
+
/**
|
|
16722
|
+
* Whether we can refer the current call to self (has active session in Established/Establishing and is calling).
|
|
16723
|
+
* Shared by reconnection logic and referInCallToSelf.
|
|
16724
|
+
*/
|
|
16725
|
+
canReferInCallToSelf() {
|
|
16726
|
+
return !!this.currentSession && (this.currentSession.state === SessionState2.Established || this.currentSession.state === SessionState2.Establishing) && this.isCalling();
|
|
16727
|
+
}
|
|
16728
|
+
/**
|
|
16729
|
+
* Refer the current call to self (e.g. Agent RTP loss recovery, post-reconnect recovery).
|
|
16730
|
+
* Socket and other callers can use this without constructing referTo.
|
|
16731
|
+
*/
|
|
16732
|
+
async referInCallToSelf(callUuid, extra) {
|
|
16733
|
+
if (callUuid && this.currentCallId !== callUuid) {
|
|
16734
|
+
this.callKit.logger.warn(
|
|
16735
|
+
"Cannot refer in call to self: callUuid mismatch",
|
|
16736
|
+
{
|
|
16737
|
+
caller: "Connect.referInCallToSelf",
|
|
16738
|
+
type: "SIP",
|
|
16739
|
+
content: {
|
|
16740
|
+
currentCallId: this.currentCallId,
|
|
16741
|
+
callUuid
|
|
16742
|
+
}
|
|
16743
|
+
}
|
|
16744
|
+
);
|
|
16745
|
+
return;
|
|
16746
|
+
}
|
|
16747
|
+
if (!this.canReferInCallToSelf()) {
|
|
16748
|
+
this.callKit.logger.warn(
|
|
16749
|
+
"Cannot refer in call to self: preconditions not met",
|
|
16750
|
+
{
|
|
16751
|
+
caller: "Connect.referInCallToSelf",
|
|
16752
|
+
type: "SIP",
|
|
16753
|
+
content: {
|
|
16754
|
+
hasCurrentSession: !!this.currentSession,
|
|
16755
|
+
sessionState: this.currentSession?.state,
|
|
16756
|
+
isCalling: this.isCalling()
|
|
16757
|
+
}
|
|
16758
|
+
}
|
|
16759
|
+
);
|
|
16760
|
+
return;
|
|
16761
|
+
}
|
|
16762
|
+
const selfUri = this.getSelfReferUri();
|
|
16763
|
+
return this.referInCall(selfUri, extra);
|
|
16764
|
+
}
|
|
16765
|
+
};
|
|
16766
|
+
|
|
16767
|
+
// core/heartbeat-worker.ts
|
|
16768
|
+
var workerCode = `
|
|
16769
|
+
let timer = null;
|
|
16770
|
+
let interval = 30000;
|
|
16771
|
+
|
|
16772
|
+
self.onmessage = function(e) {
|
|
16773
|
+
const { type, interval: newInterval } = e.data;
|
|
16774
|
+
|
|
16775
|
+
if (type === 'start') {
|
|
16776
|
+
if (timer) {
|
|
16777
|
+
clearInterval(timer);
|
|
16778
|
+
}
|
|
16779
|
+
interval = newInterval || interval;
|
|
16780
|
+
timer = setInterval(() => {
|
|
16781
|
+
self.postMessage({ type: 'tick' });
|
|
16782
|
+
}, interval);
|
|
16783
|
+
}
|
|
16784
|
+
|
|
16785
|
+
if (type === 'stop') {
|
|
16786
|
+
if (timer) {
|
|
16787
|
+
clearInterval(timer);
|
|
16788
|
+
timer = null;
|
|
16789
|
+
}
|
|
16790
|
+
}
|
|
16791
|
+
|
|
16792
|
+
if (type === 'updateInterval') {
|
|
16793
|
+
interval = newInterval;
|
|
16794
|
+
if (timer) {
|
|
16795
|
+
clearInterval(timer);
|
|
16796
|
+
timer = setInterval(() => {
|
|
16797
|
+
self.postMessage({ type: 'tick' });
|
|
16798
|
+
}, interval);
|
|
16799
|
+
}
|
|
16800
|
+
}
|
|
16801
|
+
};
|
|
16802
|
+
`;
|
|
16803
|
+
function createHeartbeatWorker() {
|
|
16804
|
+
try {
|
|
16805
|
+
const blob = new Blob([workerCode], { type: "application/javascript" });
|
|
16806
|
+
const workerUrl = URL.createObjectURL(blob);
|
|
16807
|
+
const worker = new Worker(workerUrl);
|
|
16808
|
+
URL.revokeObjectURL(workerUrl);
|
|
16809
|
+
return worker;
|
|
16810
|
+
} catch {
|
|
16811
|
+
return null;
|
|
16812
|
+
}
|
|
16813
|
+
}
|
|
16814
|
+
var HeartbeatManager = class {
|
|
16815
|
+
worker = null;
|
|
16816
|
+
fallbackTimer = null;
|
|
16817
|
+
interval = 3e4;
|
|
16818
|
+
onTick = null;
|
|
16819
|
+
isRunning = false;
|
|
16820
|
+
constructor() {
|
|
16821
|
+
this.worker = createHeartbeatWorker();
|
|
16822
|
+
if (this.worker) {
|
|
16823
|
+
this.worker.onmessage = (e) => {
|
|
16824
|
+
if (e.data.type === "tick" && this.onTick) {
|
|
16825
|
+
this.onTick();
|
|
16826
|
+
}
|
|
16827
|
+
};
|
|
16828
|
+
}
|
|
16829
|
+
}
|
|
16830
|
+
/**
|
|
16831
|
+
* Start the heartbeat
|
|
16832
|
+
* @param interval - Interval in milliseconds
|
|
16833
|
+
* @param onTick - Callback function to execute on each tick
|
|
16834
|
+
*/
|
|
16835
|
+
start(interval, onTick) {
|
|
16836
|
+
this.stop();
|
|
16837
|
+
this.interval = interval;
|
|
16838
|
+
this.onTick = onTick;
|
|
16839
|
+
this.isRunning = true;
|
|
16840
|
+
if (this.worker) {
|
|
16841
|
+
this.worker.postMessage({
|
|
16842
|
+
type: "start",
|
|
16843
|
+
interval
|
|
16844
|
+
});
|
|
16845
|
+
} else {
|
|
16846
|
+
this.fallbackTimer = setInterval(() => {
|
|
16847
|
+
if (this.onTick) {
|
|
16848
|
+
this.onTick();
|
|
16849
|
+
}
|
|
16850
|
+
}, interval);
|
|
16851
|
+
}
|
|
16852
|
+
}
|
|
16853
|
+
/**
|
|
16854
|
+
* Stop the heartbeat
|
|
16855
|
+
*/
|
|
16856
|
+
stop() {
|
|
16857
|
+
this.isRunning = false;
|
|
16858
|
+
this.onTick = null;
|
|
16859
|
+
if (this.worker) {
|
|
16860
|
+
this.worker.postMessage({ type: "stop" });
|
|
16861
|
+
}
|
|
16862
|
+
if (this.fallbackTimer) {
|
|
16863
|
+
clearInterval(this.fallbackTimer);
|
|
16864
|
+
this.fallbackTimer = null;
|
|
16865
|
+
}
|
|
16866
|
+
}
|
|
16867
|
+
/**
|
|
16868
|
+
* Update the heartbeat interval
|
|
16869
|
+
* @param interval - New interval in milliseconds
|
|
16870
|
+
*/
|
|
16871
|
+
updateInterval(interval) {
|
|
16872
|
+
this.interval = interval;
|
|
16873
|
+
if (!this.isRunning)
|
|
16874
|
+
return;
|
|
16875
|
+
if (this.worker) {
|
|
16876
|
+
this.worker.postMessage({
|
|
16877
|
+
type: "updateInterval",
|
|
16878
|
+
interval
|
|
16879
|
+
});
|
|
16880
|
+
} else if (this.fallbackTimer && this.onTick) {
|
|
16881
|
+
clearInterval(this.fallbackTimer);
|
|
16882
|
+
this.fallbackTimer = setInterval(() => {
|
|
16883
|
+
if (this.onTick) {
|
|
16884
|
+
this.onTick();
|
|
16885
|
+
}
|
|
16886
|
+
}, interval);
|
|
16887
|
+
}
|
|
16888
|
+
}
|
|
16889
|
+
/**
|
|
16890
|
+
* Destroy the heartbeat manager and release resources
|
|
16891
|
+
*/
|
|
16892
|
+
destroy() {
|
|
16893
|
+
this.stop();
|
|
16894
|
+
if (this.worker) {
|
|
16895
|
+
this.worker.terminate();
|
|
16896
|
+
this.worker = null;
|
|
16897
|
+
}
|
|
16898
|
+
}
|
|
16899
|
+
/**
|
|
16900
|
+
* Check if using Web Worker
|
|
16901
|
+
*/
|
|
16902
|
+
isUsingWorker() {
|
|
16903
|
+
return this.worker !== null;
|
|
16904
|
+
}
|
|
16711
16905
|
};
|
|
16712
16906
|
|
|
16713
16907
|
// core/socket.ts
|
|
@@ -16715,7 +16909,7 @@ var Socket = class {
|
|
|
16715
16909
|
callKit;
|
|
16716
16910
|
ws;
|
|
16717
16911
|
lastPingTime = void 0;
|
|
16718
|
-
|
|
16912
|
+
heartbeatManager;
|
|
16719
16913
|
/**
|
|
16720
16914
|
* @description reconnect timer
|
|
16721
16915
|
*/
|
|
@@ -16748,10 +16942,18 @@ var Socket = class {
|
|
|
16748
16942
|
}
|
|
16749
16943
|
constructor(callKit) {
|
|
16750
16944
|
this.callKit = callKit;
|
|
16945
|
+
this.heartbeatManager = new HeartbeatManager();
|
|
16751
16946
|
}
|
|
16752
16947
|
get reconnectConfig() {
|
|
16753
16948
|
return this.callKit.config.getReconnectConfig("incall");
|
|
16754
16949
|
}
|
|
16950
|
+
get pingInterval() {
|
|
16951
|
+
const { keepaliveInterval } = this.callKit.config.getConfig().userInfo;
|
|
16952
|
+
if (Number.isInteger(keepaliveInterval) && keepaliveInterval > 0) {
|
|
16953
|
+
return keepaliveInterval * 1e3;
|
|
16954
|
+
}
|
|
16955
|
+
return this.reconnectConfig.pingInterval;
|
|
16956
|
+
}
|
|
16755
16957
|
isConnected() {
|
|
16756
16958
|
return this.connectAuthState.isConnected;
|
|
16757
16959
|
}
|
|
@@ -16933,6 +17135,28 @@ var Socket = class {
|
|
|
16933
17135
|
this.setConnectAuthState("startConfirm", true);
|
|
16934
17136
|
this.cleanReconnectState();
|
|
16935
17137
|
}
|
|
17138
|
+
if (data.event === SocketReceiveEvent.AGENT_RTP_LOSS) {
|
|
17139
|
+
this.callKit.logger.warn("Agent RTP loss", {
|
|
17140
|
+
caller: "Socket.onMessage",
|
|
17141
|
+
type: "INCALL",
|
|
17142
|
+
content: {
|
|
17143
|
+
data: {
|
|
17144
|
+
callUuid,
|
|
17145
|
+
...content
|
|
17146
|
+
},
|
|
17147
|
+
event: SocketReceiveEvent.AGENT_RTP_LOSS
|
|
17148
|
+
}
|
|
17149
|
+
});
|
|
17150
|
+
if (callUuid) {
|
|
17151
|
+
this.callKit.connect.referInCallToSelf(callUuid).catch((err) => {
|
|
17152
|
+
this.callKit.logger.error(err, {
|
|
17153
|
+
caller: "Socket.onMessage:AGENT_RTP_LOSS",
|
|
17154
|
+
type: "INCALL",
|
|
17155
|
+
content: { callUuid, event: SocketReceiveEvent.AGENT_RTP_LOSS }
|
|
17156
|
+
});
|
|
17157
|
+
});
|
|
17158
|
+
}
|
|
17159
|
+
}
|
|
16936
17160
|
if (data.event === SocketReceiveEvent.CUSTOMER_RINGING) {
|
|
16937
17161
|
this.callKit.trigger(KitEvent.CALL_RINGING, {
|
|
16938
17162
|
time: /* @__PURE__ */ new Date(),
|
|
@@ -17045,7 +17269,7 @@ var Socket = class {
|
|
|
17045
17269
|
return;
|
|
17046
17270
|
}
|
|
17047
17271
|
const { userInfo, version } = this.callKit.config.getConfig();
|
|
17048
|
-
const { sessionId, extno, agentId
|
|
17272
|
+
const { sessionId, extno, agentId } = userInfo;
|
|
17049
17273
|
if (!sessionId) {
|
|
17050
17274
|
this.callKit.logger.error("sessionId is empty", {
|
|
17051
17275
|
caller: "Socket.send",
|
|
@@ -17065,7 +17289,6 @@ var Socket = class {
|
|
|
17065
17289
|
if (SocketSendEvent.CALL === event) {
|
|
17066
17290
|
msg.phoneNum = extno;
|
|
17067
17291
|
msg.agentId = agentId;
|
|
17068
|
-
msg.phoneType = phoneType;
|
|
17069
17292
|
if (message?.sourceType === CallSourceType.phoneNum) {
|
|
17070
17293
|
delete msg.workOrderId;
|
|
17071
17294
|
} else if (message?.sourceType === CallSourceType.workOrderId) {
|
|
@@ -17087,8 +17310,8 @@ var Socket = class {
|
|
|
17087
17310
|
return;
|
|
17088
17311
|
this.send(SocketSendEvent.PING);
|
|
17089
17312
|
const now = Date.now();
|
|
17090
|
-
const {
|
|
17091
|
-
if (now - this.lastPingTime > pingInterval + pingTimeout) {
|
|
17313
|
+
const { pingTimeout } = this.reconnectConfig;
|
|
17314
|
+
if (now - this.lastPingTime > this.pingInterval + pingTimeout) {
|
|
17092
17315
|
this.callKit.logger.warn("Ping timeout not connected", {
|
|
17093
17316
|
caller: "Socket.ping",
|
|
17094
17317
|
type: "INCALL",
|
|
@@ -17103,23 +17326,27 @@ var Socket = class {
|
|
|
17103
17326
|
}
|
|
17104
17327
|
}
|
|
17105
17328
|
checkPing() {
|
|
17106
|
-
|
|
17107
|
-
clearInterval(this.pingTimer);
|
|
17108
|
-
}
|
|
17109
|
-
const { pingInterval } = this.reconnectConfig;
|
|
17110
|
-
this.pingTimer = setInterval(() => {
|
|
17329
|
+
this.heartbeatManager.start(this.pingInterval, () => {
|
|
17111
17330
|
this.ping();
|
|
17112
|
-
}
|
|
17331
|
+
});
|
|
17332
|
+
this.callKit.logger.info(
|
|
17333
|
+
`Heartbeat started with Worker: ${this.heartbeatManager.isUsingWorker()}`,
|
|
17334
|
+
{
|
|
17335
|
+
caller: "Socket.checkPing",
|
|
17336
|
+
type: "INCALL",
|
|
17337
|
+
content: {
|
|
17338
|
+
pingInterval: this.pingInterval,
|
|
17339
|
+
usingWorker: this.heartbeatManager.isUsingWorker()
|
|
17340
|
+
}
|
|
17341
|
+
}
|
|
17342
|
+
);
|
|
17113
17343
|
}
|
|
17114
17344
|
/**
|
|
17115
17345
|
* reset socket connection and all states
|
|
17116
17346
|
*/
|
|
17117
17347
|
async reset(config) {
|
|
17118
17348
|
const { force = false } = config || {};
|
|
17119
|
-
|
|
17120
|
-
clearInterval(this.pingTimer);
|
|
17121
|
-
this.pingTimer = void 0;
|
|
17122
|
-
}
|
|
17349
|
+
this.heartbeatManager.stop();
|
|
17123
17350
|
if (force) {
|
|
17124
17351
|
this.callKit.trigger(KitEvent.INCALL_CONNECT_EVENT, {
|
|
17125
17352
|
event: "INCALL_RESET"
|
|
@@ -17130,6 +17357,12 @@ var Socket = class {
|
|
|
17130
17357
|
this.setConnectAuthState("startConfirm", false);
|
|
17131
17358
|
this.clearWebSocket();
|
|
17132
17359
|
}
|
|
17360
|
+
/**
|
|
17361
|
+
* Destroy the heartbeat manager
|
|
17362
|
+
*/
|
|
17363
|
+
destroyHeartbeat() {
|
|
17364
|
+
this.heartbeatManager.destroy();
|
|
17365
|
+
}
|
|
17133
17366
|
attemptReconnect() {
|
|
17134
17367
|
if (this.reconnectTimer) {
|
|
17135
17368
|
clearTimeout(this.reconnectTimer);
|
|
@@ -17255,6 +17488,7 @@ var CallKit = class {
|
|
|
17255
17488
|
content: {
|
|
17256
17489
|
username,
|
|
17257
17490
|
password,
|
|
17491
|
+
ua: navigator.userAgent,
|
|
17258
17492
|
encryptionMethod,
|
|
17259
17493
|
encryptionPassword
|
|
17260
17494
|
}
|
|
@@ -17288,6 +17522,7 @@ var CallKit = class {
|
|
|
17288
17522
|
iceInfo: user.iceInfo,
|
|
17289
17523
|
iceGatheringTimeout: user.iceGatheringTimeout,
|
|
17290
17524
|
logGather: user.logGather,
|
|
17525
|
+
keepaliveInterval: user.keepaliveInterval,
|
|
17291
17526
|
phoneType: user.phoneType,
|
|
17292
17527
|
// encryptionType is in extra
|
|
17293
17528
|
...extra
|
|
@@ -17385,7 +17620,10 @@ var CallKit = class {
|
|
|
17385
17620
|
caller: "CallKit.register",
|
|
17386
17621
|
content: {}
|
|
17387
17622
|
});
|
|
17388
|
-
this.
|
|
17623
|
+
const { userInfo } = this.config.getConfig();
|
|
17624
|
+
if (userInfo.phoneType === PhoneTypeEnum.SIP) {
|
|
17625
|
+
this.connect.register();
|
|
17626
|
+
}
|
|
17389
17627
|
}
|
|
17390
17628
|
async unregister() {
|
|
17391
17629
|
if (!this.config.check())
|
|
@@ -17495,10 +17733,13 @@ var CallKit = class {
|
|
|
17495
17733
|
force
|
|
17496
17734
|
}
|
|
17497
17735
|
});
|
|
17498
|
-
|
|
17499
|
-
|
|
17736
|
+
const { userInfo } = this.config.getConfig();
|
|
17737
|
+
if (userInfo.phoneType === PhoneTypeEnum.SIP) {
|
|
17738
|
+
if (this.connect.isCalling()) {
|
|
17739
|
+
await this.hangup();
|
|
17740
|
+
}
|
|
17741
|
+
await this.connect.reset({ force });
|
|
17500
17742
|
}
|
|
17501
|
-
await this.connect.reset({ force });
|
|
17502
17743
|
if (this.config.isLogin()) {
|
|
17503
17744
|
await this.logout({ isReset: false });
|
|
17504
17745
|
} else {
|