@easemob-community/callkit-core 2.0.1

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.
@@ -0,0 +1,2823 @@
1
+ (function(global, factory) {
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.EaseMobCallKitCore = {}));
3
+ })(this, (function(exports2) {
4
+ "use strict";
5
+ const rtcEventTypes = /* @__PURE__ */ new Set([
6
+ "shouldJoinRtc",
7
+ "shouldLeaveRtc",
8
+ "shouldPublishTracks",
9
+ "localAudioChanged",
10
+ "localVideoChanged"
11
+ ]);
12
+ function isUIEvent(event) {
13
+ return !rtcEventTypes.has(event.type);
14
+ }
15
+ function isRtcEvent(event) {
16
+ return rtcEventTypes.has(event.type);
17
+ }
18
+ const defaultLogger = {
19
+ error: (msg, ...args) => console.error(`[CallKitCore] ${msg}`, ...args),
20
+ warn: (msg, ...args) => console.warn(`[CallKitCore] ${msg}`, ...args),
21
+ info: (msg, ...args) => console.info(`[CallKitCore] ${msg}`, ...args),
22
+ debug: (msg, ...args) => console.log(`[CallKitCore] ${msg}`, ...args),
23
+ verbose: () => {
24
+ }
25
+ // 默认关闭 verbose
26
+ };
27
+ let globalLogger = defaultLogger;
28
+ function setLogger(logger) {
29
+ globalLogger = logger;
30
+ }
31
+ function getLogger() {
32
+ return globalLogger;
33
+ }
34
+ class EventBus {
35
+ constructor(logger) {
36
+ this.listeners = /* @__PURE__ */ new Map();
37
+ this.logger = logger || getLogger();
38
+ }
39
+ on(event, handler) {
40
+ if (!this.listeners.has(event)) {
41
+ this.listeners.set(event, /* @__PURE__ */ new Set());
42
+ }
43
+ this.listeners.get(event).add(handler);
44
+ this.logger.verbose?.(`[EventBus] 订阅事件: ${String(event)}`);
45
+ return () => this.off(event, handler);
46
+ }
47
+ once(event, handler) {
48
+ const onceHandler = (payload) => {
49
+ this.off(event, onceHandler);
50
+ handler(payload);
51
+ };
52
+ return this.on(event, onceHandler);
53
+ }
54
+ off(event, handler) {
55
+ this.listeners.get(event)?.delete(handler);
56
+ }
57
+ emit(event, payload) {
58
+ const handlers = this.listeners.get(event);
59
+ if (!handlers || handlers.size === 0) {
60
+ this.logger.verbose?.(`[EventBus] 事件 ${String(event)} 无订阅者,跳过`);
61
+ return;
62
+ }
63
+ this.logger.debug?.(`[EventBus] 触发事件: ${String(event)}`, payload);
64
+ handlers.forEach((handler) => {
65
+ try {
66
+ handler(payload);
67
+ } catch (error) {
68
+ this.logger.error(`[EventBus] 事件 ${String(event)} 的 handler 执行失败:`, error);
69
+ }
70
+ });
71
+ }
72
+ clear(event) {
73
+ if (event) {
74
+ this.listeners.delete(event);
75
+ } else {
76
+ this.listeners.clear();
77
+ }
78
+ }
79
+ listenerCount(event) {
80
+ return this.listeners.get(event)?.size ?? 0;
81
+ }
82
+ }
83
+ const CALL_STATUS = {
84
+ IDLE: 0,
85
+ INVITING: 1,
86
+ ALERTING: 2,
87
+ CONFIRM_RING: 3,
88
+ RECEIVED_CONFIRM_RING: 4,
89
+ ANSWER_CALL: 5,
90
+ CONFIRM_CALLEE: 6,
91
+ IN_CALL: 7
92
+ };
93
+ const CALL_TYPE = {
94
+ AUDIO_1V1: 0,
95
+ // 一对一语音通话
96
+ VIDEO_1V1: 1,
97
+ // 一对一视频通话
98
+ VIDEO_MULTI: 2,
99
+ // 多人视频通话
100
+ AUDIO_MULTI: 3
101
+ // 多人语音通话
102
+ };
103
+ const HANGUP_REASON = {
104
+ HANGUP: "hangup",
105
+ // Hang up call
106
+ CANCEL: "cancel",
107
+ // Cancel call
108
+ REMOTE_CANCEL: "remoteCancel",
109
+ // Remote cancel call
110
+ REFUSE: "refuse",
111
+ // Refuse call
112
+ REMOTE_REFUSE: "remoteRefuse",
113
+ // Remote refuse call
114
+ BUSY: "busy",
115
+ // Busy
116
+ NO_RESPONSE: "noResponse",
117
+ // No response (timeout)
118
+ REMOTE_NO_RESPONSE: "remoteNoResponse",
119
+ // Remote no response
120
+ HANDLE_ON_OTHER_DEVICE: "handleOnOtherDevice",
121
+ // Handled on other device
122
+ ABNORMAL_END: "abnormalEnd"
123
+ // Abnormal end
124
+ };
125
+ const DEFAULT_TIMEOUT = 3e4;
126
+ function createIdleState(preserve) {
127
+ return {
128
+ status: CALL_STATUS.IDLE,
129
+ callId: "",
130
+ channel: "",
131
+ token: "",
132
+ type: CALL_TYPE.AUDIO_1V1,
133
+ callerDevId: preserve?.callerDevId ?? "",
134
+ calleeDevId: "",
135
+ callerUserId: preserve?.callerUserId ?? "",
136
+ calleeUserId: "",
137
+ inviteTimeout: DEFAULT_TIMEOUT,
138
+ inviteTimeoutTimer: null,
139
+ startTime: null,
140
+ audioEnabled: true,
141
+ videoEnabled: true
142
+ };
143
+ }
144
+ class SingleCallStateMachine {
145
+ constructor(logger) {
146
+ this.state = createIdleState();
147
+ this.logger = logger || getLogger();
148
+ }
149
+ // ─── 查询 ───
150
+ getState() {
151
+ return Object.freeze({ ...this.state });
152
+ }
153
+ isIdle() {
154
+ return this.state.status === CALL_STATUS.IDLE;
155
+ }
156
+ isInCall() {
157
+ return this.state.status === CALL_STATUS.IN_CALL;
158
+ }
159
+ /**
160
+ * 当前是否处于可被接听的状态(被叫端弹窗显示区间)
161
+ */
162
+ isWaitingCalleeAction() {
163
+ return this.state.status === CALL_STATUS.ALERTING || this.state.status === CALL_STATUS.CONFIRM_RING || this.state.status === CALL_STATUS.RECEIVED_CONFIRM_RING;
164
+ }
165
+ /**
166
+ * 当前是否处于活跃通话中(已进入 RTC 或即将进入)
167
+ */
168
+ isInActiveCall() {
169
+ return this.state.status === CALL_STATUS.ANSWER_CALL || this.state.status === CALL_STATUS.CONFIRM_CALLEE || this.state.status === CALL_STATUS.IN_CALL;
170
+ }
171
+ /**
172
+ * 当前是否可以接听(被叫端按钮可点击)
173
+ */
174
+ canAccept() {
175
+ return this.state.status === CALL_STATUS.ALERTING || this.state.status === CALL_STATUS.RECEIVED_CONFIRM_RING;
176
+ }
177
+ /**
178
+ * 当前是否可以拒绝(被叫端按钮可点击)
179
+ */
180
+ canReject() {
181
+ return this.state.status === CALL_STATUS.ALERTING || this.state.status === CALL_STATUS.RECEIVED_CONFIRM_RING;
182
+ }
183
+ /**
184
+ * 当前是否可以挂断(主叫/被叫端的挂断按钮可点击)
185
+ */
186
+ canHangup() {
187
+ return this.state.status !== CALL_STATUS.IDLE;
188
+ }
189
+ /**
190
+ * 当前是否处于呼叫/响铃中(主叫等待对方接听)
191
+ */
192
+ isCalling() {
193
+ return this.state.status === CALL_STATUS.INVITING || this.state.status === CALL_STATUS.CONFIRM_RING;
194
+ }
195
+ isCallIdMatch(incomingCallId) {
196
+ return this.state.callId === incomingCallId;
197
+ }
198
+ getDuration() {
199
+ if (!this.state.startTime) return 0;
200
+ return Date.now() - this.state.startTime;
201
+ }
202
+ // ─── 初始化 ───
203
+ /**
204
+ * 主叫方发起邀请
205
+ */
206
+ initInvite(params) {
207
+ this.clearTimeout();
208
+ const oldStatus = this.state.status;
209
+ this.state = {
210
+ ...createIdleState({
211
+ callerDevId: params.callerDevId,
212
+ callerUserId: params.callerUserId
213
+ }),
214
+ status: CALL_STATUS.INVITING,
215
+ calleeUserId: params.calleeUserId,
216
+ type: params.callType,
217
+ callId: params.callId,
218
+ channel: params.channel,
219
+ token: params.token,
220
+ inviteTimeout: params.timeout ?? DEFAULT_TIMEOUT
221
+ };
222
+ this.logger.stateChange?.(oldStatus, CALL_STATUS.INVITING, { callId: params.callId });
223
+ return {
224
+ ok: true,
225
+ events: [
226
+ {
227
+ type: "STATUS_CHANGED",
228
+ from: oldStatus,
229
+ to: CALL_STATUS.INVITING,
230
+ callId: params.callId
231
+ },
232
+ {
233
+ type: "CALL_INVITED",
234
+ callId: params.callId,
235
+ isCaller: true,
236
+ channel: params.channel,
237
+ callType: params.callType
238
+ }
239
+ ]
240
+ };
241
+ }
242
+ /**
243
+ * 被叫方收到 invite,初始化响铃状态
244
+ */
245
+ initIncoming(params) {
246
+ this.clearTimeout();
247
+ const oldStatus = this.state.status;
248
+ this.state = {
249
+ ...createIdleState(),
250
+ status: CALL_STATUS.ALERTING,
251
+ callId: params.callId,
252
+ channel: params.channel,
253
+ token: params.token,
254
+ type: params.callType,
255
+ callerDevId: params.callerDevId,
256
+ callerUserId: params.callerUserId,
257
+ calleeDevId: params.calleeDevId,
258
+ calleeUserId: params.calleeUserId
259
+ };
260
+ this.logger.stateChange?.(oldStatus, CALL_STATUS.ALERTING, { callId: params.callId });
261
+ return {
262
+ ok: true,
263
+ events: [
264
+ {
265
+ type: "STATUS_CHANGED",
266
+ from: oldStatus,
267
+ to: CALL_STATUS.ALERTING,
268
+ callId: params.callId
269
+ },
270
+ {
271
+ type: "CALL_INVITED",
272
+ callId: params.callId,
273
+ isCaller: false,
274
+ channel: params.channel,
275
+ callType: params.callType
276
+ }
277
+ ]
278
+ };
279
+ }
280
+ // ─── 信令响应 ───
281
+ /**
282
+ * 主叫方收到 alert(被叫已响铃)
283
+ *
284
+ * 与 lib 对齐:收到 alert 后保持 INVITING 状态不变(lib 的 handleAlertSignalMessage
285
+ * 不修改 callState.status),只记录 calleeDevId 和发送 confirmRing。
286
+ * 状态直到收到 confirmRing 后才变为 RECEIVED_CONFIRM_RING。
287
+ */
288
+ receiveAlert(calleeDevId) {
289
+ if (this.state.status !== CALL_STATUS.INVITING) {
290
+ this.logger.warn("[SingleCallStateMachine] receiveAlert: 当前状态不是 INVITING,忽略", {
291
+ status: this.state.status
292
+ });
293
+ return { ok: false, events: [] };
294
+ }
295
+ this.state.calleeDevId = calleeDevId;
296
+ this.logger.info("[SingleCallStateMachine] receiveAlert: 保持 INVITING,记录 calleeDevId", {
297
+ callId: this.state.callId,
298
+ calleeDevId
299
+ });
300
+ return { ok: true, events: [] };
301
+ }
302
+ /**
303
+ * 被叫方收到 confirmRing
304
+ */
305
+ receiveConfirmRing(status) {
306
+ if (this.state.status < CALL_STATUS.ALERTING) {
307
+ this.logger.warn("[SingleCallStateMachine] receiveConfirmRing: 当前状态 < ALERTING,忽略");
308
+ return { ok: false, events: [] };
309
+ }
310
+ if (this.state.status === CALL_STATUS.RECEIVED_CONFIRM_RING) {
311
+ this.logger.info("[SingleCallStateMachine] receiveConfirmRing: 已是 RECEIVED_CONFIRM_RING,忽略");
312
+ return { ok: false, events: [] };
313
+ }
314
+ if (!status) {
315
+ this.logger.warn("[SingleCallStateMachine] receiveConfirmRing: status=false,忽略");
316
+ return { ok: false, events: [] };
317
+ }
318
+ this.clearTimeout();
319
+ const oldStatus = this.state.status;
320
+ this.state.status = CALL_STATUS.RECEIVED_CONFIRM_RING;
321
+ this.logger.stateChange?.(oldStatus, CALL_STATUS.RECEIVED_CONFIRM_RING, { callId: this.state.callId });
322
+ return {
323
+ ok: true,
324
+ events: [
325
+ {
326
+ type: "STATUS_CHANGED",
327
+ from: oldStatus,
328
+ to: CALL_STATUS.RECEIVED_CONFIRM_RING,
329
+ callId: this.state.callId
330
+ }
331
+ ]
332
+ };
333
+ }
334
+ /**
335
+ * 收到 answerCall 信令
336
+ */
337
+ receiveAnswer(result, fromCaller) {
338
+ this.clearTimeout();
339
+ if (this.state.status === CALL_STATUS.IN_CALL) {
340
+ this.logger.info("[SingleCallStateMachine] receiveAnswer: 已在 IN_CALL,忽略");
341
+ return { ok: false, events: [] };
342
+ }
343
+ if (result === "accept") {
344
+ const oldStatus2 = this.state.status;
345
+ this.state.status = CALL_STATUS.IN_CALL;
346
+ this.state.startTime = Date.now();
347
+ this.logger.stateChange?.(oldStatus2, CALL_STATUS.IN_CALL, { callId: this.state.callId });
348
+ const isCaller = !fromCaller;
349
+ const events2 = [
350
+ {
351
+ type: "STATUS_CHANGED",
352
+ from: oldStatus2,
353
+ to: CALL_STATUS.IN_CALL,
354
+ callId: this.state.callId
355
+ },
356
+ {
357
+ type: "CALL_ACCEPTED",
358
+ callId: this.state.callId,
359
+ isCaller,
360
+ channel: this.state.channel,
361
+ callType: this.state.type
362
+ },
363
+ {
364
+ type: "CALL_STARTED",
365
+ callId: this.state.callId,
366
+ isCaller,
367
+ channel: this.state.channel,
368
+ callType: this.state.type
369
+ },
370
+ {
371
+ type: "SHOULD_JOIN_RTC",
372
+ callId: this.state.callId,
373
+ channel: this.state.channel,
374
+ token: this.state.token,
375
+ role: fromCaller ? "callee" : "caller",
376
+ callType: this.state.type
377
+ }
378
+ ];
379
+ return { ok: true, events: events2 };
380
+ }
381
+ const oldStatus = this.state.status;
382
+ const duration = 0;
383
+ const reason = result === "busy" ? HANGUP_REASON.BUSY : HANGUP_REASON.REMOTE_REFUSE;
384
+ const callId = this.state.callId;
385
+ this.resetCore();
386
+ this.logger.stateChange?.(oldStatus, CALL_STATUS.IDLE, { callId, trigger: `answer:${result}` });
387
+ const events = [
388
+ {
389
+ type: "CALL_ENDED",
390
+ callId,
391
+ reason,
392
+ duration
393
+ }
394
+ ];
395
+ if (result === "busy") {
396
+ events.unshift({ type: "CALL_BUSY", callId });
397
+ } else {
398
+ events.unshift({ type: "CALL_REFUSED", callId, isRemote: true });
399
+ }
400
+ return { ok: true, events };
401
+ }
402
+ /**
403
+ * 收到 cancelCall 信令
404
+ *
405
+ * 注:callId 校验和多端容错由 Handler 负责,状态机只处理匹配后的状态流转。
406
+ */
407
+ receiveCancel() {
408
+ const currentStatus = this.state.status;
409
+ const callId = this.state.callId;
410
+ if (currentStatus === CALL_STATUS.IDLE) {
411
+ this.logger.warn("[SingleCallStateMachine] receiveCancel: 当前状态 IDLE,忽略");
412
+ return { ok: false, events: [] };
413
+ }
414
+ this.clearTimeout();
415
+ this.resetCore();
416
+ this.logger.stateChange?.(currentStatus, CALL_STATUS.IDLE, { callId, trigger: "cancel" });
417
+ return {
418
+ ok: true,
419
+ events: [
420
+ { type: "CALL_CANCELED", callId, isRemote: true },
421
+ { type: "CALL_ENDED", callId, reason: HANGUP_REASON.REMOTE_CANCEL, duration: 0 }
422
+ ]
423
+ };
424
+ }
425
+ /**
426
+ * 收到 leaveCall 信令
427
+ *
428
+ * 注:callId 校验和多端容错由 Handler 负责,状态机只处理匹配后的状态流转。
429
+ */
430
+ receiveLeave() {
431
+ const currentStatus = this.state.status;
432
+ const callId = this.state.callId;
433
+ if (currentStatus === CALL_STATUS.IDLE) {
434
+ return { ok: false, events: [] };
435
+ }
436
+ if (currentStatus === CALL_STATUS.IN_CALL) {
437
+ const duration = this.getDuration();
438
+ this.resetCore();
439
+ this.logger.stateChange?.(currentStatus, CALL_STATUS.IDLE, { callId, trigger: "leave" });
440
+ return {
441
+ ok: true,
442
+ events: [{ type: "CALL_ENDED", callId, reason: HANGUP_REASON.HANGUP, duration }]
443
+ };
444
+ }
445
+ this.resetCore();
446
+ this.logger.stateChange?.(currentStatus, CALL_STATUS.IDLE, { callId, trigger: "leave:alerting" });
447
+ return {
448
+ ok: true,
449
+ events: [{ type: "CALL_ENDED", callId, reason: HANGUP_REASON.HANGUP, duration: 0 }]
450
+ };
451
+ }
452
+ /**
453
+ * 收到 confirmCallee 信令(被叫方)
454
+ */
455
+ receiveConfirmCallee() {
456
+ if (this.state.status === CALL_STATUS.IDLE) {
457
+ this.logger.warn("[SingleCallStateMachine] receiveConfirmCallee: 当前 IDLE,忽略");
458
+ return { ok: false, events: [] };
459
+ }
460
+ if (this.state.status === CALL_STATUS.IN_CALL) {
461
+ this.logger.info("[SingleCallStateMachine] receiveConfirmCallee: 已是 IN_CALL,跳过");
462
+ return { ok: false, events: [] };
463
+ }
464
+ const oldStatus = this.state.status;
465
+ this.state.status = CALL_STATUS.IN_CALL;
466
+ this.state.startTime = Date.now();
467
+ this.clearTimeout();
468
+ this.logger.stateChange?.(oldStatus, CALL_STATUS.IN_CALL, { callId: this.state.callId });
469
+ const events = [
470
+ {
471
+ type: "STATUS_CHANGED",
472
+ from: oldStatus,
473
+ to: CALL_STATUS.IN_CALL,
474
+ callId: this.state.callId
475
+ },
476
+ {
477
+ type: "CALL_CONNECTED",
478
+ callId: this.state.callId,
479
+ channel: this.state.channel,
480
+ callType: this.state.type
481
+ },
482
+ {
483
+ type: "CALL_STARTED",
484
+ callId: this.state.callId,
485
+ isCaller: false,
486
+ channel: this.state.channel,
487
+ callType: this.state.type
488
+ },
489
+ {
490
+ type: "SHOULD_JOIN_RTC",
491
+ callId: this.state.callId,
492
+ channel: this.state.channel,
493
+ token: this.state.token,
494
+ role: "callee",
495
+ callType: this.state.type
496
+ }
497
+ ];
498
+ return { ok: true, events };
499
+ }
500
+ // ─── 本地动作 ───
501
+ /**
502
+ * 本地挂断/取消
503
+ */
504
+ hangup(reason = HANGUP_REASON.HANGUP) {
505
+ const currentStatus = this.state.status;
506
+ if (currentStatus === CALL_STATUS.IDLE) {
507
+ this.logger.warn("[SingleCallStateMachine] hangup: 当前状态 IDLE,忽略");
508
+ return { ok: false, events: [] };
509
+ }
510
+ const duration = currentStatus === CALL_STATUS.IN_CALL ? this.getDuration() : 0;
511
+ const callId = this.state.callId;
512
+ this.clearTimeout();
513
+ this.resetCore();
514
+ this.logger.stateChange?.(currentStatus, CALL_STATUS.IDLE, { callId, trigger: "hangup" });
515
+ return {
516
+ ok: true,
517
+ events: [{ type: "CALL_ENDED", callId, reason, duration }]
518
+ };
519
+ }
520
+ /**
521
+ * 邀请超时
522
+ */
523
+ timeout() {
524
+ const currentStatus = this.state.status;
525
+ if (currentStatus === CALL_STATUS.IDLE || currentStatus === CALL_STATUS.IN_CALL) {
526
+ return { ok: false, events: [] };
527
+ }
528
+ const callId = this.state.callId;
529
+ this.resetCore();
530
+ this.logger.stateChange?.(currentStatus, CALL_STATUS.IDLE, { callId, trigger: "timeout" });
531
+ return {
532
+ ok: true,
533
+ events: [
534
+ { type: "CALL_TIMEOUT", callId },
535
+ { type: "CALL_ENDED", callId, reason: HANGUP_REASON.NO_RESPONSE, duration: 0 }
536
+ ]
537
+ };
538
+ }
539
+ /**
540
+ * 强制重置状态机
541
+ */
542
+ reset() {
543
+ this.clearTimeout();
544
+ const oldStatus = this.state.status;
545
+ const preserved = { callerDevId: this.state.callerDevId, callerUserId: this.state.callerUserId };
546
+ this.state = createIdleState(preserved);
547
+ this.logger.stateChange?.(oldStatus, CALL_STATUS.IDLE, { trigger: "forceReset" });
548
+ }
549
+ // ─── 超时管理(由外部调用) ───
550
+ /**
551
+ * 启动超时定时器。
552
+ * 超时后自动调用 onTimeout 回调(由 CallKitCore 注册),确保事件不会被丢弃。
553
+ */
554
+ startTimeout(onTimeout) {
555
+ this.clearTimeout();
556
+ this.state.inviteTimeoutTimer = setTimeout(() => {
557
+ const result = this.timeout();
558
+ if (onTimeout && result.ok) {
559
+ onTimeout(result);
560
+ }
561
+ }, this.state.inviteTimeout);
562
+ }
563
+ clearTimeout() {
564
+ if (this.state.inviteTimeoutTimer) {
565
+ clearTimeout(this.state.inviteTimeoutTimer);
566
+ this.state.inviteTimeoutTimer = null;
567
+ }
568
+ }
569
+ /**
570
+ * 核心重置:保留 callerDevId / callerUserId,其余清空
571
+ * 与现有 callStateStore.resetCallState() 行为一致
572
+ */
573
+ resetCore() {
574
+ const preserved = { callerDevId: this.state.callerDevId, callerUserId: this.state.callerUserId };
575
+ this.state = createIdleState(preserved);
576
+ }
577
+ // ─── 媒体状态 ───
578
+ /**
579
+ * 切换本地音频状态
580
+ */
581
+ toggleAudio() {
582
+ this.state.audioEnabled = !this.state.audioEnabled;
583
+ return {
584
+ ok: true,
585
+ events: [
586
+ {
587
+ type: "LOCAL_AUDIO_CHANGED",
588
+ callId: this.state.callId,
589
+ enabled: this.state.audioEnabled
590
+ }
591
+ ]
592
+ };
593
+ }
594
+ /**
595
+ * 切换本地视频状态
596
+ */
597
+ toggleVideo() {
598
+ this.state.videoEnabled = !this.state.videoEnabled;
599
+ return {
600
+ ok: true,
601
+ events: [
602
+ {
603
+ type: "LOCAL_VIDEO_CHANGED",
604
+ callId: this.state.callId,
605
+ enabled: this.state.videoEnabled
606
+ }
607
+ ]
608
+ };
609
+ }
610
+ }
611
+ class GroupCallSession {
612
+ constructor(logger) {
613
+ this.session = null;
614
+ this.participants = /* @__PURE__ */ new Map();
615
+ this.logger = logger || getLogger();
616
+ }
617
+ /**
618
+ * 初始化会话
619
+ */
620
+ init(params) {
621
+ this.session = {
622
+ ...params,
623
+ status: "inviting",
624
+ startTime: Date.now()
625
+ };
626
+ this.participants.clear();
627
+ this.logger.info("[GroupCallSession] 初始化", params);
628
+ }
629
+ /**
630
+ * 添加参与者
631
+ */
632
+ addParticipant(info) {
633
+ this.participants.set(info.userId, { ...info });
634
+ this.logger.info("[GroupCallSession] 添加参与者", { userId: info.userId, state: info.state });
635
+ }
636
+ /**
637
+ * 移除参与者
638
+ */
639
+ removeParticipant(userId) {
640
+ const removed = this.participants.delete(userId);
641
+ if (removed) {
642
+ this.logger.info("[GroupCallSession] 移除参与者", { userId });
643
+ }
644
+ return removed;
645
+ }
646
+ /**
647
+ * 标记参与者状态
648
+ */
649
+ setParticipantState(userId, state) {
650
+ const p = this.participants.get(userId);
651
+ if (!p) return false;
652
+ p.state = state;
653
+ this.logger.info("[GroupCallSession] 更新参与者状态", { userId, state });
654
+ return true;
655
+ }
656
+ /**
657
+ * 标记已接受
658
+ */
659
+ markAccepted(userId) {
660
+ return this.setParticipantState(userId, "accepted");
661
+ }
662
+ /**
663
+ * 标记已加入 RTC
664
+ */
665
+ markJoinedRtc(userId) {
666
+ const ok = this.setParticipantState(userId, "joinedRtc");
667
+ if (ok && this.session && this.session.status === "inviting") {
668
+ this.session.status = "inCall";
669
+ }
670
+ return ok;
671
+ }
672
+ /**
673
+ * 标记已离开 RTC
674
+ */
675
+ markLeftRtc(userId) {
676
+ return this.setParticipantState(userId, "left");
677
+ }
678
+ /**
679
+ * 标记音频静音状态
680
+ */
681
+ markAudioMuted(userId, muted) {
682
+ const p = this.participants.get(userId);
683
+ if (!p) return false;
684
+ p.isMuted = muted;
685
+ this.logger.info("[GroupCallSession] 更新音频状态", { userId, muted });
686
+ return true;
687
+ }
688
+ /**
689
+ * 标记视频开关状态
690
+ */
691
+ markVideoOn(userId, on) {
692
+ const p = this.participants.get(userId);
693
+ if (!p) return false;
694
+ p.isCameraOn = on;
695
+ this.logger.info("[GroupCallSession] 更新视频状态", { userId, on });
696
+ return true;
697
+ }
698
+ /**
699
+ * 获取参与者
700
+ */
701
+ getParticipant(userId) {
702
+ const p = this.participants.get(userId);
703
+ return p ? Object.freeze({ ...p }) : void 0;
704
+ }
705
+ /**
706
+ * 获取所有参与者
707
+ */
708
+ getAllParticipants() {
709
+ return Array.from(this.participants.values()).map((p) => Object.freeze({ ...p }));
710
+ }
711
+ /**
712
+ * 获取当前在线参与者(未离开)
713
+ */
714
+ getActiveParticipants() {
715
+ return this.getAllParticipants().filter((p) => p.state !== "left");
716
+ }
717
+ /**
718
+ * 获取会话快照
719
+ */
720
+ getSnapshot() {
721
+ return this.session ? Object.freeze({ ...this.session }) : null;
722
+ }
723
+ /**
724
+ * 结束会话
725
+ */
726
+ end() {
727
+ if (this.session) {
728
+ this.session.status = "ended";
729
+ }
730
+ this.logger.info("[GroupCallSession] 会话结束");
731
+ }
732
+ /**
733
+ * 销毁会话
734
+ */
735
+ destroy() {
736
+ this.session = null;
737
+ this.participants.clear();
738
+ this.logger.info("[GroupCallSession] 已销毁");
739
+ }
740
+ }
741
+ class SignalRouter {
742
+ constructor(logger) {
743
+ this.handlers = /* @__PURE__ */ new Map();
744
+ this.logger = logger || getLogger();
745
+ this.logger.warn("📡 [SignalRouter] 信令路由器已初始化");
746
+ }
747
+ register(action, handler) {
748
+ if (!this.handlers.has(action)) {
749
+ this.handlers.set(action, []);
750
+ }
751
+ this.handlers.get(action).push(handler);
752
+ }
753
+ dispatch(message) {
754
+ const action = message.ext?.action;
755
+ if (!action) {
756
+ this.logger.warn("[SignalRouter] 消息缺少 action,无法分发", message);
757
+ return [];
758
+ }
759
+ this.logger.signal?.("recv", action, {
760
+ from: message.from,
761
+ to: message.to,
762
+ callId: message.ext?.callId,
763
+ result: message.ext?.result,
764
+ deviceId: message.ext?.callerDevId || message.ext?.calleeDevId
765
+ });
766
+ const handlers = this.handlers.get(action) || [];
767
+ if (handlers.length === 0) {
768
+ this.logger.warn(`[SignalRouter] 未注册 action "${action}" 的处理器`);
769
+ return [];
770
+ }
771
+ const allEvents = [];
772
+ handlers.forEach((h) => {
773
+ try {
774
+ const result = h.handle(message);
775
+ if (result && Array.isArray(result)) {
776
+ allEvents.push(...result);
777
+ }
778
+ } catch (err) {
779
+ this.logger.error("[SignalRouter] Handler 执行失败:", err);
780
+ }
781
+ });
782
+ return allEvents;
783
+ }
784
+ }
785
+ class SignalSender {
786
+ constructor(imClient, logger, createMessageFn) {
787
+ this.imClient = imClient;
788
+ this.logger = logger || getLogger();
789
+ this.createMessageFn = createMessageFn;
790
+ }
791
+ /**
792
+ * 发送 invite 文本消息
793
+ */
794
+ async sendInviteMessage(targetId, chatType, message, ext, groupId) {
795
+ const isGroupChat = Array.isArray(targetId);
796
+ const to = isGroupChat ? groupId || "" : targetId;
797
+ const msgBody = {
798
+ type: "txt",
799
+ to,
800
+ msg: message,
801
+ chatType: isGroupChat ? "groupChat" : chatType,
802
+ ext
803
+ };
804
+ if (isGroupChat && Array.isArray(targetId)) {
805
+ msgBody.receiverList = targetId;
806
+ }
807
+ const msg = this.createMessage(msgBody);
808
+ this.logger.debug?.("[SignalSender] sendInviteMessage msgBody", JSON.parse(JSON.stringify(msgBody)));
809
+ this.logger.debug?.("[SignalSender] sendInviteMessage ext", {
810
+ action: msgBody.ext?.action,
811
+ callId: msgBody.ext?.callId,
812
+ callerIMName: msgBody.ext?.callerIMName,
813
+ calleeIMName: msgBody.ext?.calleeIMName,
814
+ callerDevId: msgBody.ext?.callerDevId,
815
+ channelName: msgBody.ext?.channelName,
816
+ chatType: msgBody.ext?.chatType,
817
+ type: msgBody.ext?.type,
818
+ msgType: msgBody.ext?.msgType,
819
+ invitedMembers: msgBody.ext?.invitedMembers,
820
+ em_push_ext: msgBody.ext?.em_push_ext,
821
+ em_apns_ext: msgBody.ext?.em_apns_ext,
822
+ ease_chat_uikit_user_info: msgBody.ext?.ease_chat_uikit_user_info,
823
+ callkitGroupInfo: msgBody.ext?.callkitGroupInfo
824
+ });
825
+ const result = await this.imClient.send(msg);
826
+ this.logger.signal?.("send", "invite", { to, callId: ext.callId });
827
+ return result;
828
+ }
829
+ /**
830
+ * 发送 CMD 信令消息
831
+ */
832
+ async sendCmdMessage(targetId, chatType, ext, options) {
833
+ const msgBody = {
834
+ type: "cmd",
835
+ to: targetId,
836
+ chatType,
837
+ action: "rtcCall",
838
+ ext,
839
+ deliverOnlineOnly: options?.deliverOnlineOnly || false
840
+ };
841
+ if (options?.receiverList) {
842
+ msgBody.receiverList = options.receiverList;
843
+ }
844
+ const msg = this.createMessage(msgBody);
845
+ const result = await this.imClient.send(msg);
846
+ this.logger.signal?.("send", ext.action, { to: targetId, callId: ext.callId });
847
+ return result;
848
+ }
849
+ /**
850
+ * 兼容 full 版与 miniCore 版的消息创建
851
+ * full 版: ChatSDK.message.create(options)
852
+ * miniCore 版: client.Message.create(options)
853
+ */
854
+ createMessage(options) {
855
+ if (this.createMessageFn) {
856
+ return this.createMessageFn(options);
857
+ }
858
+ const client = this.imClient;
859
+ if (typeof client !== "undefined" && client.message?.create) {
860
+ return client.message.create(options);
861
+ }
862
+ if (client?.Message?.create) {
863
+ return client.Message.create(options);
864
+ }
865
+ throw new Error(
866
+ "[SignalSender] 无法创建消息:当前环境缺少 message.create API。请确认 easemob-websdk 已安装(full 版),或 miniCore 已注册消息插件。"
867
+ );
868
+ }
869
+ }
870
+ class SingleCallSignalHandler {
871
+ constructor(stateMachine, sender, deviceIdProvider, logger) {
872
+ this.stateMachine = stateMachine;
873
+ this.sender = sender;
874
+ this.getDeviceId = typeof deviceIdProvider === "function" ? deviceIdProvider : () => deviceIdProvider;
875
+ this.logger = logger || getLogger();
876
+ this.logger.warn("📞 [SingleCallSignalHandler] 单聊信令处理器已初始化");
877
+ }
878
+ get deviceId() {
879
+ return this.getDeviceId();
880
+ }
881
+ handle(message) {
882
+ const action = message.ext?.action;
883
+ switch (action) {
884
+ case "alert":
885
+ return this.handleAlert(message);
886
+ case "confirmRing":
887
+ return this.handleConfirmRing(message);
888
+ case "answerCall":
889
+ return this.handleAnswerCall(message);
890
+ case "cancelCall":
891
+ return this.handleCancelCall(message);
892
+ case "leaveCall":
893
+ return this.handleLeaveCall(message);
894
+ case "confirmCallee":
895
+ return this.handleConfirmCallee(message);
896
+ default:
897
+ this.logger.warn(`[SingleCallSignalHandler] 未知 action: ${action}`);
898
+ return [];
899
+ }
900
+ }
901
+ // ───────────────────────────────────────────────
902
+ // alert — 主叫方收到被叫已响铃
903
+ // ───────────────────────────────────────────────
904
+ handleAlert(message) {
905
+ const ext = message.ext;
906
+ if (!ext) return [];
907
+ const currentState = this.stateMachine.getState();
908
+ const isGroupCall = currentState.type === CALL_TYPE.VIDEO_MULTI || currentState.type === CALL_TYPE.AUDIO_MULTI;
909
+ this.logger.signal?.("recv", "alert", {
910
+ from: message.from,
911
+ callId: ext?.callId,
912
+ callerDevId: ext?.callerDevId,
913
+ isGroupCall
914
+ });
915
+ if (ext.callerDevId !== this.deviceId) {
916
+ this.logger.warn(
917
+ `[SingleCallSignalHandler] 主叫多端: callerDevId(${ext.callerDevId}) ≠ 当前设备(${this.deviceId})`
918
+ );
919
+ return [];
920
+ }
921
+ const stateResult = this.stateMachine.receiveAlert(ext.calleeDevId);
922
+ const shouldSendConfirmRing = stateResult.ok || isGroupCall;
923
+ if (shouldSendConfirmRing) {
924
+ const confirmRingPayload = this.buildConfirmRingPayload(message);
925
+ if (confirmRingPayload) {
926
+ this.sender.sendCmdMessage(
927
+ message.from,
928
+ "singleChat",
929
+ {
930
+ action: "confirmRing",
931
+ callId: ext.callId,
932
+ status: confirmRingPayload.status,
933
+ callerDevId: ext.callerDevId,
934
+ calleeDevId: ext.calleeDevId,
935
+ ts: Date.now(),
936
+ msgType: "rtcCallWithAgora"
937
+ }
938
+ ).catch(() => {
939
+ });
940
+ }
941
+ }
942
+ if (!stateResult.ok) {
943
+ return [];
944
+ }
945
+ return stateResult.events;
946
+ }
947
+ buildConfirmRingPayload(message) {
948
+ const ext = message.ext;
949
+ if (!ext) return null;
950
+ const currentState = this.stateMachine.getState();
951
+ if (!currentState.callId) {
952
+ this.logger.warn("[SingleCallSignalHandler] 当前无通话,无法构建 confirmRing");
953
+ return null;
954
+ }
955
+ const isGroupCall = currentState.type === CALL_TYPE.VIDEO_MULTI || currentState.type === CALL_TYPE.AUDIO_MULTI;
956
+ let status = true;
957
+ if (ext.callId !== currentState.callId) {
958
+ status = false;
959
+ this.logger.warn(
960
+ `[SingleCallSignalHandler] callId 不匹配: ext(${ext.callId}) ≠ current(${currentState.callId})`
961
+ );
962
+ }
963
+ if (currentState.status !== void 0 && currentState.status > CALL_STATUS.RECEIVED_CONFIRM_RING && !isGroupCall) {
964
+ status = false;
965
+ this.logger.warn(
966
+ `[SingleCallSignalHandler] 单聊状态已超前: ${currentState.status},confirmRing status=false`
967
+ );
968
+ }
969
+ return { status };
970
+ }
971
+ // ───────────────────────────────────────────────
972
+ // confirmRing — 被叫方收到主叫确认响铃
973
+ // ───────────────────────────────────────────────
974
+ handleConfirmRing(message) {
975
+ const { ext } = message;
976
+ if (!ext) return [];
977
+ const currentState = this.stateMachine.getState();
978
+ currentState.type === CALL_TYPE.VIDEO_MULTI || currentState.type === CALL_TYPE.AUDIO_MULTI;
979
+ if (ext.callerDevId !== currentState.callerDevId) {
980
+ this.logger.warn(
981
+ `[SingleCallSignalHandler] confirmRing 主叫设备不匹配: ext(${ext.callerDevId}) ≠ state(${currentState.callerDevId})`
982
+ );
983
+ return [];
984
+ }
985
+ if (ext.calleeDevId !== this.deviceId) {
986
+ this.logger.warn(
987
+ `[SingleCallSignalHandler] confirmRing 被叫设备不匹配: ext(${ext.calleeDevId}) ≠ current(${this.deviceId})`
988
+ );
989
+ return [];
990
+ }
991
+ const stateResult = this.stateMachine.receiveConfirmRing(!!ext.status);
992
+ return stateResult.events;
993
+ }
994
+ // ───────────────────────────────────────────────
995
+ // answerCall — 主叫方收到被叫应答
996
+ // ───────────────────────────────────────────────
997
+ handleAnswerCall(message) {
998
+ const ext = message.ext;
999
+ if (!ext) return [];
1000
+ const currentState = this.stateMachine.getState();
1001
+ if (ext.callId !== currentState.callId) {
1002
+ this.logger.warn(
1003
+ `[SingleCallSignalHandler] answerCall callId 不匹配: ext(${ext.callId}) ≠ current(${currentState.callId})`
1004
+ );
1005
+ return [];
1006
+ }
1007
+ const isGroupCall = currentState.type === CALL_TYPE.VIDEO_MULTI || currentState.type === CALL_TYPE.AUDIO_MULTI;
1008
+ if (isGroupCall) {
1009
+ this.logger.debug("[SingleCallSignalHandler] 群聊 answerCall 由 GroupCallSignalHandler 处理,本 Handler 忽略");
1010
+ return [];
1011
+ }
1012
+ if (currentState.status === CALL_STATUS.IN_CALL) {
1013
+ this.logger.debug("[SingleCallSignalHandler] 单聊已在 IN_CALL,忽略 answerCall");
1014
+ return [];
1015
+ }
1016
+ if (ext.callerDevId !== this.deviceId) {
1017
+ if (message.from === currentState.callerUserId) {
1018
+ const reason = ext.result === "accept" ? "已被其他端接听" : "已被其他端拒绝";
1019
+ this.logger.warn(`[SingleCallSignalHandler] answerCall ${reason}`);
1020
+ return [];
1021
+ }
1022
+ this.logger.warn(
1023
+ `[SingleCallSignalHandler] answerCall callerDevId 不匹配: ext(${ext.callerDevId}) ≠ current(${this.deviceId})`
1024
+ );
1025
+ return [];
1026
+ }
1027
+ const allEvents = [];
1028
+ if (ext.result !== "accept") {
1029
+ this.logger.signal?.("recv", "answerCall", {
1030
+ from: message.from,
1031
+ result: ext.result,
1032
+ callId: ext.callId
1033
+ });
1034
+ const stateResult = this.stateMachine.receiveAnswer(
1035
+ ext.result
1036
+ );
1037
+ allEvents.push(...stateResult.events);
1038
+ this.sendConfirmCallee(message.from, {
1039
+ callId: ext.callId,
1040
+ callerDevId: ext.callerDevId,
1041
+ calleeDevId: ext.calleeDevId,
1042
+ result: ext.result
1043
+ });
1044
+ } else {
1045
+ this.logger.signal?.("recv", "answerCall", {
1046
+ from: message.from,
1047
+ result: "accept",
1048
+ callId: ext.callId
1049
+ });
1050
+ this.sendConfirmCallee(message.from, {
1051
+ callId: ext.callId,
1052
+ callerDevId: ext.callerDevId,
1053
+ calleeDevId: ext.calleeDevId,
1054
+ result: "accept"
1055
+ });
1056
+ this.logger.info("[SingleCallSignalHandler] 一对一通话接受,进入 IN_CALL");
1057
+ const stateResult = this.stateMachine.receiveAnswer("accept");
1058
+ allEvents.push(...stateResult.events);
1059
+ }
1060
+ return allEvents;
1061
+ }
1062
+ // ───────────────────────────────────────────────
1063
+ // cancelCall
1064
+ // ───────────────────────────────────────────────
1065
+ handleCancelCall(message) {
1066
+ const ext = message.ext;
1067
+ if (!ext) return [];
1068
+ const currentState = this.stateMachine.getState();
1069
+ if (ext.callId !== currentState.callId) {
1070
+ this.logger.warn(
1071
+ `[SingleCallSignalHandler] cancelCall callId 不匹配: ext(${ext.callId}) ≠ current(${currentState.callId})`
1072
+ );
1073
+ if (currentState.status === CALL_STATUS.IDLE) {
1074
+ this.logger.info("[SingleCallSignalHandler] 当前 IDLE,忽略");
1075
+ return [];
1076
+ }
1077
+ if (currentState.type === CALL_TYPE.VIDEO_MULTI || currentState.type === CALL_TYPE.AUDIO_MULTI) {
1078
+ this.logger.debug("[SingleCallSignalHandler] 群聊 cancelCall 由 GroupCallSignalHandler 处理");
1079
+ return [];
1080
+ }
1081
+ const isFromCaller = message.from === currentState.callerUserId;
1082
+ if (isFromCaller && (currentState.status === CALL_STATUS.ALERTING || currentState.status === CALL_STATUS.INVITING)) {
1083
+ this.logger.info("[SingleCallSignalHandler] 单聊收到主叫方取消(callId 不匹配),执行挂断");
1084
+ const stateResult2 = this.stateMachine.receiveCancel();
1085
+ return stateResult2.events;
1086
+ }
1087
+ return [];
1088
+ }
1089
+ this.logger.signal?.("recv", "cancelCall", {
1090
+ from: message.from,
1091
+ callId: ext.callId
1092
+ });
1093
+ this.logger.info("[SingleCallSignalHandler] 收到对方取消");
1094
+ const stateResult = this.stateMachine.receiveCancel();
1095
+ return stateResult.events;
1096
+ }
1097
+ // ───────────────────────────────────────────────
1098
+ // leaveCall
1099
+ // ───────────────────────────────────────────────
1100
+ handleLeaveCall(message) {
1101
+ const ext = message.ext;
1102
+ if (!ext) return [];
1103
+ const currentState = this.stateMachine.getState();
1104
+ if (ext.callId !== currentState.callId) {
1105
+ this.logger.warn(
1106
+ `[SingleCallSignalHandler] leaveCall callId 不匹配: ext(${ext.callId}) ≠ current(${currentState.callId})`
1107
+ );
1108
+ if (currentState.status === CALL_STATUS.IDLE) {
1109
+ return [];
1110
+ }
1111
+ if (currentState.type === CALL_TYPE.VIDEO_MULTI || currentState.type === CALL_TYPE.AUDIO_MULTI) {
1112
+ this.logger.debug("[SingleCallSignalHandler] 群聊 leaveCall 由 GroupCallSignalHandler 处理");
1113
+ return [];
1114
+ }
1115
+ if (currentState.status === CALL_STATUS.IN_CALL) {
1116
+ this.logger.info("[SingleCallSignalHandler] 通话中对方离开,执行挂断");
1117
+ const stateResult2 = this.stateMachine.receiveLeave();
1118
+ return stateResult2.events;
1119
+ } else if (currentState.status === CALL_STATUS.ALERTING && message.from === currentState.callerUserId) {
1120
+ this.logger.info("[SingleCallSignalHandler] ALERTING 状态收到主叫方离开,执行挂断");
1121
+ const stateResult2 = this.stateMachine.receiveLeave();
1122
+ return stateResult2.events;
1123
+ }
1124
+ return [];
1125
+ }
1126
+ this.logger.signal?.("recv", "leaveCall", {
1127
+ from: message.from,
1128
+ callId: ext.callId
1129
+ });
1130
+ if (currentState.type === CALL_TYPE.VIDEO_MULTI || currentState.type === CALL_TYPE.AUDIO_MULTI) {
1131
+ this.logger.debug("[SingleCallSignalHandler] 群聊 leaveCall 由 GroupCallSignalHandler 处理");
1132
+ return [];
1133
+ }
1134
+ if (currentState.status === CALL_STATUS.IDLE) {
1135
+ return [];
1136
+ }
1137
+ if (currentState.status === CALL_STATUS.ALERTING || currentState.status === CALL_STATUS.INVITING) {
1138
+ if (message.from !== currentState.callerUserId) {
1139
+ this.logger.warn("[SingleCallSignalHandler] leaveCall callId 匹配但发送者不是主叫方,忽略");
1140
+ return [];
1141
+ }
1142
+ }
1143
+ const stateResult = this.stateMachine.receiveLeave();
1144
+ return stateResult.events;
1145
+ }
1146
+ // ───────────────────────────────────────────────
1147
+ // confirmCallee — 被叫方收到主叫确认 callee 就绪
1148
+ // ───────────────────────────────────────────────
1149
+ handleConfirmCallee(message) {
1150
+ const ext = message.ext;
1151
+ if (!ext) return [];
1152
+ this.logger.signal?.("recv", "confirmCallee", {
1153
+ from: message.from,
1154
+ callId: ext.callId,
1155
+ result: ext.result
1156
+ });
1157
+ this.logger.info("[SingleCallSignalHandler] 收到 confirmCallee");
1158
+ const currentState = this.stateMachine.getState();
1159
+ if (ext.callId !== currentState.callId) {
1160
+ this.logger.warn(
1161
+ `[SingleCallSignalHandler] confirmCallee callId 不匹配: ext(${ext.callId}) ≠ current(${currentState.callId})`
1162
+ );
1163
+ return [];
1164
+ }
1165
+ if (ext.result && ext.result !== "accept") {
1166
+ this.logger.info(`[SingleCallSignalHandler] confirmCallee result=${ext.result},忽略`);
1167
+ return [];
1168
+ }
1169
+ const stateResult = this.stateMachine.receiveConfirmCallee();
1170
+ return stateResult.events;
1171
+ }
1172
+ // ─── 私有辅助 ───
1173
+ sendConfirmCallee(to, payload) {
1174
+ this.sender.sendCmdMessage(
1175
+ to,
1176
+ "singleChat",
1177
+ {
1178
+ action: "confirmCallee",
1179
+ callId: payload.callId,
1180
+ callerDevId: payload.callerDevId,
1181
+ calleeDevId: payload.calleeDevId,
1182
+ result: payload.result,
1183
+ ts: Date.now(),
1184
+ msgType: "rtcCallWithAgora"
1185
+ }
1186
+ ).catch(() => {
1187
+ });
1188
+ }
1189
+ }
1190
+ class GroupCallSignalHandler {
1191
+ constructor(session, stateMachine, sender, userIdProvider, logger) {
1192
+ this.session = session;
1193
+ this.stateMachine = stateMachine;
1194
+ this.sender = sender;
1195
+ this.getUserId = typeof userIdProvider === "function" ? userIdProvider : () => userIdProvider;
1196
+ this.logger = logger || getLogger();
1197
+ this.logger.warn("👥 [GroupCallSignalHandler] 群聊信令处理器已初始化");
1198
+ }
1199
+ get userId() {
1200
+ return this.getUserId();
1201
+ }
1202
+ /**
1203
+ * 处理 invite 文本消息中的群聊初始化
1204
+ * 由 IMListener 在收到 invite 文本消息时直接调用(不走 SignalRouter)
1205
+ */
1206
+ handleInviteTextMessage(message) {
1207
+ const ext = message.ext;
1208
+ const callerUserId = ext?.callerIMName || message.from || "";
1209
+ const groupId = ext?.callkitGroupInfo?.groupId || "";
1210
+ const groupName = ext?.callkitGroupInfo?.groupName || "";
1211
+ const channel = ext?.channelName || "";
1212
+ const callType = ext?.type === CALL_TYPE.VIDEO_MULTI ? "video" : "audio";
1213
+ const invitedMembers = ext?.invitedMembers || [];
1214
+ const callId = ext?.callId || "";
1215
+ const callerUserInfo = ext?.ease_chat_uikit_user_info;
1216
+ const callerNickname = callerUserInfo?.nickname || callerUserId;
1217
+ const callerAvatar = callerUserInfo?.avatarURL || "";
1218
+ this.session.init({
1219
+ sessionId: channel,
1220
+ groupId,
1221
+ groupName,
1222
+ callType,
1223
+ callerUserId
1224
+ });
1225
+ this.session.addParticipant({
1226
+ userId: this.userId,
1227
+ nickname: this.userId,
1228
+ avatarUrl: "",
1229
+ state: "invited",
1230
+ isLocal: true,
1231
+ isMuted: false,
1232
+ isCameraOn: callType === "video",
1233
+ isSpeaking: false
1234
+ });
1235
+ if (callerUserId && callerUserId !== this.userId) {
1236
+ this.session.addParticipant({
1237
+ userId: callerUserId,
1238
+ nickname: callerNickname,
1239
+ avatarUrl: callerAvatar,
1240
+ state: "joinedRtc",
1241
+ isLocal: false,
1242
+ isMuted: false,
1243
+ isCameraOn: false,
1244
+ isSpeaking: false
1245
+ });
1246
+ }
1247
+ invitedMembers.forEach((m) => {
1248
+ if (m !== this.userId && m !== callerUserId) {
1249
+ this.session.addParticipant({
1250
+ userId: m,
1251
+ nickname: m,
1252
+ avatarUrl: "",
1253
+ state: "invited",
1254
+ isLocal: false,
1255
+ isMuted: false,
1256
+ isCameraOn: false,
1257
+ isSpeaking: false
1258
+ });
1259
+ }
1260
+ });
1261
+ this.logger.event?.("groupCallInit", {
1262
+ groupId,
1263
+ groupName,
1264
+ channel,
1265
+ callType,
1266
+ participants: this.session.getAllParticipants().map((p) => ({
1267
+ userId: p.userId,
1268
+ nickname: p.nickname,
1269
+ state: p.state
1270
+ }))
1271
+ });
1272
+ return [
1273
+ {
1274
+ type: "GROUP_CALL_INIT",
1275
+ callId,
1276
+ groupId,
1277
+ groupName,
1278
+ channel,
1279
+ callType,
1280
+ callerUserId,
1281
+ invitedMembers
1282
+ }
1283
+ ];
1284
+ }
1285
+ handle(message) {
1286
+ const action = message.ext?.action;
1287
+ switch (action) {
1288
+ case "answerCall":
1289
+ return this.handleAnswerCall(message);
1290
+ case "cancelCall":
1291
+ return this.handleCancelCall(message);
1292
+ case "leaveCall":
1293
+ return this.handleLeaveCall(message);
1294
+ default:
1295
+ this.logger.warn(`[GroupCallSignalHandler] 未知 action: ${action}`);
1296
+ return [];
1297
+ }
1298
+ }
1299
+ // ───────────────────────────────────────────────
1300
+ // answerCall — 群聊成员应答
1301
+ // ───────────────────────────────────────────────
1302
+ handleAnswerCall(message) {
1303
+ const ext = message.ext;
1304
+ if (!ext) return [];
1305
+ const currentState = this.stateMachine.getState();
1306
+ if (currentState.type !== CALL_TYPE.VIDEO_MULTI && currentState.type !== CALL_TYPE.AUDIO_MULTI) {
1307
+ return [];
1308
+ }
1309
+ if (ext.callId !== currentState.callId) {
1310
+ this.logger.warn(
1311
+ `[GroupCallSignalHandler] answerCall callId 不匹配: ext(${ext.callId}) ≠ current(${currentState.callId})`
1312
+ );
1313
+ return [];
1314
+ }
1315
+ const fromUserId = message.from;
1316
+ const callId = currentState.callId;
1317
+ const channel = currentState.channel;
1318
+ const groupSnapshot = this.session.getSnapshot();
1319
+ const groupId = groupSnapshot?.groupId;
1320
+ if (ext.result !== "accept") {
1321
+ this.logger.signal?.("recv", "answerCall", {
1322
+ from: fromUserId,
1323
+ result: ext.result,
1324
+ callId,
1325
+ type: "group"
1326
+ });
1327
+ this.logger.info(`[GroupCallSignalHandler] 群聊成员拒绝: ${fromUserId}`);
1328
+ this.session.removeParticipant(fromUserId);
1329
+ return [
1330
+ {
1331
+ type: "PARTICIPANT_LEFT",
1332
+ callId,
1333
+ userId: fromUserId,
1334
+ channel,
1335
+ callType: currentState.type,
1336
+ reason: ext.result === "busy" ? "busy" : "refused",
1337
+ groupId
1338
+ }
1339
+ ];
1340
+ }
1341
+ this.logger.signal?.("recv", "answerCall", {
1342
+ from: fromUserId,
1343
+ result: "accept",
1344
+ callId,
1345
+ type: "group"
1346
+ });
1347
+ this.logger.info(`[GroupCallSignalHandler] 群聊成员接受: ${fromUserId}`);
1348
+ this.session.markAccepted(fromUserId);
1349
+ this.sendConfirmCallee(fromUserId, {
1350
+ callId,
1351
+ callerDevId: currentState.callerDevId || "",
1352
+ calleeDevId: ext.calleeDevId,
1353
+ result: "accept"
1354
+ });
1355
+ return [
1356
+ {
1357
+ type: "PARTICIPANT_STATE_CHANGED",
1358
+ callId,
1359
+ userId: fromUserId,
1360
+ state: "accepted",
1361
+ groupId
1362
+ },
1363
+ {
1364
+ type: "PARTICIPANT_JOINED",
1365
+ callId,
1366
+ userId: fromUserId,
1367
+ channel,
1368
+ callType: currentState.type,
1369
+ groupId
1370
+ }
1371
+ ];
1372
+ }
1373
+ // ─── 私有辅助 ───
1374
+ sendConfirmCallee(to, payload) {
1375
+ this.sender.sendCmdMessage(
1376
+ to,
1377
+ "singleChat",
1378
+ {
1379
+ action: "confirmCallee",
1380
+ callId: payload.callId,
1381
+ callerDevId: payload.callerDevId,
1382
+ calleeDevId: payload.calleeDevId,
1383
+ result: payload.result,
1384
+ ts: Date.now(),
1385
+ msgType: "rtcCallWithAgora"
1386
+ }
1387
+ ).catch(() => {
1388
+ });
1389
+ }
1390
+ // ───────────────────────────────────────────────
1391
+ // cancelCall — 群聊容错
1392
+ // ───────────────────────────────────────────────
1393
+ handleCancelCall(message) {
1394
+ const ext = message.ext;
1395
+ if (!ext) return [];
1396
+ const currentState = this.stateMachine.getState();
1397
+ if (currentState.type !== CALL_TYPE.VIDEO_MULTI && currentState.type !== CALL_TYPE.AUDIO_MULTI) {
1398
+ return [];
1399
+ }
1400
+ const currentStatus = currentState.status;
1401
+ const isFromCaller = message.from === currentState.callerUserId;
1402
+ if (ext.callId !== currentState.callId) {
1403
+ if (isFromCaller && (currentStatus === CALL_STATUS.ALERTING || currentStatus === CALL_STATUS.INVITING)) {
1404
+ this.logger.info("[GroupCallSignalHandler] 群聊收到主叫方取消(callId 不匹配),执行挂断");
1405
+ const stateResult = this.stateMachine.receiveCancel();
1406
+ return stateResult.events;
1407
+ }
1408
+ return [];
1409
+ }
1410
+ this.logger.signal?.("recv", "cancelCall", {
1411
+ from: message.from,
1412
+ callId: ext.callId,
1413
+ type: "group"
1414
+ });
1415
+ if (isFromCaller && (currentStatus === CALL_STATUS.ALERTING || currentStatus === CALL_STATUS.INVITING)) {
1416
+ this.logger.info("[GroupCallSignalHandler] 群聊收到主叫方 cancelCall,执行远程挂断");
1417
+ const stateResult = this.stateMachine.receiveCancel();
1418
+ return stateResult.events;
1419
+ }
1420
+ this.logger.debug("[GroupCallSignalHandler] 群聊 cancelCall 状态不符,忽略");
1421
+ return [];
1422
+ }
1423
+ // ───────────────────────────────────────────────
1424
+ // leaveCall — 群聊成员离开
1425
+ // ───────────────────────────────────────────────
1426
+ handleLeaveCall(message) {
1427
+ const ext = message.ext;
1428
+ if (!ext) return [];
1429
+ const currentState = this.stateMachine.getState();
1430
+ if (currentState.type !== CALL_TYPE.VIDEO_MULTI && currentState.type !== CALL_TYPE.AUDIO_MULTI) {
1431
+ return [];
1432
+ }
1433
+ const currentStatus = currentState.status;
1434
+ const isFromCaller = message.from === currentState.callerUserId;
1435
+ const fromUserId = message.from;
1436
+ if (ext.callId !== currentState.callId) {
1437
+ if (currentStatus === CALL_STATUS.IDLE) {
1438
+ this.logger.info("[GroupCallSignalHandler] 当前 IDLE,忽略 leaveCall");
1439
+ return [];
1440
+ }
1441
+ if (currentStatus === CALL_STATUS.IN_CALL) {
1442
+ this.logger.info("[GroupCallSignalHandler] 通话中对方离开,继续处理(callId 不匹配)");
1443
+ } else if (currentStatus === CALL_STATUS.ALERTING && isFromCaller) {
1444
+ this.logger.info("[GroupCallSignalHandler] ALERTING 收到主叫方 leaveCall,继续处理");
1445
+ } else {
1446
+ this.logger.warn("[GroupCallSignalHandler] leaveCall callId 不匹配且状态不符,忽略");
1447
+ return [];
1448
+ }
1449
+ }
1450
+ if ((currentStatus === CALL_STATUS.ALERTING || currentStatus === CALL_STATUS.INVITING) && isFromCaller) {
1451
+ this.logger.info(`[GroupCallSignalHandler] 被叫方收到主叫方(${fromUserId})离开,挂断通话`);
1452
+ const stateResult = this.stateMachine.receiveCancel();
1453
+ return stateResult.events;
1454
+ }
1455
+ this.logger.signal?.("recv", "leaveCall", {
1456
+ from: fromUserId,
1457
+ callId: ext.callId,
1458
+ type: "group"
1459
+ });
1460
+ this.logger.info(`[GroupCallSignalHandler] 群聊成员离开: ${fromUserId}`);
1461
+ const groupSnapshot = this.session.getSnapshot();
1462
+ const groupId = groupSnapshot?.groupId;
1463
+ this.session.setParticipantState(fromUserId, "left");
1464
+ setTimeout(() => this.session.removeParticipant(fromUserId), 2e3);
1465
+ return [
1466
+ {
1467
+ type: "PARTICIPANT_LEFT",
1468
+ callId: currentState.callId,
1469
+ userId: fromUserId,
1470
+ channel: currentState.channel,
1471
+ callType: currentState.type,
1472
+ reason: "left",
1473
+ groupId
1474
+ }
1475
+ ];
1476
+ }
1477
+ }
1478
+ class IMListener {
1479
+ constructor(imClient, callbacks, logger) {
1480
+ this.mounted = false;
1481
+ this.handlerId = "callkit-core-listener";
1482
+ this.imClient = imClient;
1483
+ this.callbacks = callbacks;
1484
+ this.logger = logger || getLogger();
1485
+ }
1486
+ mount() {
1487
+ if (this.mounted) return;
1488
+ this.mounted = true;
1489
+ this.imClient.addEventHandler(this.handlerId, {
1490
+ onTextMessage: async (msg) => {
1491
+ this.logger.debug("[IMListener] onTextMessage", { from: msg.from, id: msg.id });
1492
+ await this.callbacks.onTextMessage?.(msg);
1493
+ },
1494
+ onCmdMessage: async (msg) => {
1495
+ this.logger.debug("[IMListener] onCmdMessage", { from: msg.from, action: msg.action });
1496
+ await this.callbacks.onCmdMessage?.(msg);
1497
+ },
1498
+ onConnected: () => {
1499
+ this.logger.info("[IMListener] IM 已连接");
1500
+ this.callbacks.onConnected?.();
1501
+ },
1502
+ onDisconnected: () => {
1503
+ this.logger.warn("[IMListener] IM 已断开");
1504
+ this.callbacks.onDisconnected?.();
1505
+ }
1506
+ });
1507
+ this.logger.warn("🔵 [IMListener] CORE 链路 IM 监听已挂载 | handlerId:", this.handlerId);
1508
+ }
1509
+ unmount() {
1510
+ if (!this.mounted) return;
1511
+ this.mounted = false;
1512
+ this.imClient.removeEventHandler(this.handlerId);
1513
+ this.logger.info("[IMListener] 监听已卸载");
1514
+ }
1515
+ }
1516
+ class MessageBuilder {
1517
+ /**
1518
+ * 构建 invite 文本消息的 ext
1519
+ */
1520
+ static buildInviteExt(params) {
1521
+ const ts = params.ts ?? Date.now();
1522
+ const invitedMembers = params.invitedMembers && params.invitedMembers.length > 0 ? params.invitedMembers : void 0;
1523
+ return {
1524
+ action: "invite",
1525
+ callId: params.callId || "",
1526
+ callerIMName: params.callerUserId || "",
1527
+ calleeIMName: params.calleeUserId || "",
1528
+ callerDevId: params.callerDevId || "",
1529
+ channelName: params.channel || "",
1530
+ chatType: params.callType || CALL_TYPE.AUDIO_1V1,
1531
+ type: params.callType || CALL_TYPE.AUDIO_1V1,
1532
+ ts,
1533
+ msgType: "rtcCallWithAgora",
1534
+ invitedMembers,
1535
+ em_push_ext: {
1536
+ type: "call",
1537
+ custom: {
1538
+ action: "invite",
1539
+ channelName: params.channel || "",
1540
+ type: params.callType || CALL_TYPE.AUDIO_1V1,
1541
+ callerDevId: params.callerDevId || "",
1542
+ callId: params.callId || "",
1543
+ ts,
1544
+ msgType: "rtcCallWithAgora",
1545
+ callerIMName: params.callerUserId || "",
1546
+ calleeIMName: params.calleeUserId || "",
1547
+ // 与旧版 ChatService 对齐:无昵称时 fallback 到 callerUserId,避免 iOS/APNS 解析异常
1548
+ callerNickname: params.callerInfo?.nickname || params.callerUserId || "",
1549
+ chatType: params.callType || CALL_TYPE.AUDIO_1V1
1550
+ }
1551
+ },
1552
+ em_apns_ext: {
1553
+ em_push_type: "voip"
1554
+ },
1555
+ ease_chat_uikit_user_info: params.callerInfo ? {
1556
+ nickname: params.callerInfo.nickname || params.callerUserId || "",
1557
+ avatarURL: params.callerInfo.avatarURL || ""
1558
+ } : void 0,
1559
+ callkitGroupInfo: params.groupInfo,
1560
+ // 兼容新版 iOS EaseCallUIKit:ext 最外层携带 groupId / receiverList
1561
+ groupId: params.groupInfo?.groupId,
1562
+ receiverList: invitedMembers
1563
+ };
1564
+ }
1565
+ /**
1566
+ * 构建 CMD 信令消息的 ext
1567
+ */
1568
+ static buildCmdExt(params) {
1569
+ const ts = params.ts ?? Date.now();
1570
+ const base = { callId: params.callId || "", ts, msgType: "rtcCallWithAgora" };
1571
+ switch (params.action) {
1572
+ case "alert":
1573
+ return {
1574
+ ...base,
1575
+ action: "alert",
1576
+ calleeDevId: params.calleeDevId || "",
1577
+ callerDevId: params.callerDevId || ""
1578
+ };
1579
+ case "confirmRing":
1580
+ return {
1581
+ ...base,
1582
+ action: "confirmRing",
1583
+ status: params.status ?? false,
1584
+ callerDevId: params.callerDevId || "",
1585
+ calleeDevId: params.calleeDevId || ""
1586
+ };
1587
+ case "answerCall":
1588
+ return {
1589
+ ...base,
1590
+ action: "answerCall",
1591
+ result: params.result || "busy",
1592
+ callerDevId: params.callerDevId || "",
1593
+ calleeDevId: params.calleeDevId || ""
1594
+ };
1595
+ case "confirmCallee":
1596
+ return {
1597
+ ...base,
1598
+ action: "confirmCallee",
1599
+ result: params.result || "accept",
1600
+ callerDevId: params.callerDevId || "",
1601
+ calleeDevId: params.calleeDevId || ""
1602
+ };
1603
+ case "cancelCall":
1604
+ return {
1605
+ ...base,
1606
+ action: "cancelCall",
1607
+ callerDevId: params.callerDevId || ""
1608
+ };
1609
+ case "leaveCall":
1610
+ return {
1611
+ ...base,
1612
+ action: "leaveCall"
1613
+ };
1614
+ default:
1615
+ throw new Error(`[MessageBuilder] 未知的信令动作: ${params.action}`);
1616
+ }
1617
+ }
1618
+ }
1619
+ const generateRandomChannel = (length = 8) => {
1620
+ const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
1621
+ let result = "";
1622
+ for (let i = 0; i < length; i++) {
1623
+ result += CHARS[Math.floor(Math.random() * CHARS.length)];
1624
+ }
1625
+ return result;
1626
+ };
1627
+ const isMessageExpired = (messageTime, toleranceMs = 4e4) => {
1628
+ if (!messageTime || messageTime <= 0) return false;
1629
+ return Date.now() - messageTime > toleranceMs;
1630
+ };
1631
+ const isCmdMessageExpired = (messageTime) => {
1632
+ return isMessageExpired(messageTime, 6e4);
1633
+ };
1634
+ const formatCallDuration = (seconds) => {
1635
+ const hours = Math.floor(seconds / 3600);
1636
+ const minutes = Math.floor(seconds % 3600 / 60);
1637
+ const secs = seconds % 60;
1638
+ if (hours > 0) {
1639
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
1640
+ }
1641
+ return `${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
1642
+ };
1643
+ class CallKitCore {
1644
+ constructor(config) {
1645
+ this.destroyed = false;
1646
+ this.inviteTimer = null;
1647
+ this.pendingIncomingInvites = /* @__PURE__ */ new Map();
1648
+ this.rtcAppId = "";
1649
+ this.rtcUid = 0;
1650
+ this.durationTimer = null;
1651
+ this.durationStartTime = 0;
1652
+ this.durationCallInfo = null;
1653
+ this.config = config;
1654
+ this.logger = config.logger || getLogger();
1655
+ this.eventBus = new EventBus(this.logger);
1656
+ this.inviteTimeoutMs = config.inviteTimeout || 3e4;
1657
+ this.singleCallState = new SingleCallStateMachine(this.logger);
1658
+ this.groupCallSession = new GroupCallSession(this.logger);
1659
+ this.signalRouter = new SignalRouter(this.logger);
1660
+ this.signalSender = new SignalSender(config.imClient, this.logger, config.createMessage);
1661
+ this.singleCallHandler = new SingleCallSignalHandler(
1662
+ this.singleCallState,
1663
+ this.signalSender,
1664
+ () => this.deviceId,
1665
+ this.logger
1666
+ );
1667
+ this.groupCallHandler = new GroupCallSignalHandler(
1668
+ this.groupCallSession,
1669
+ this.singleCallState,
1670
+ this.signalSender,
1671
+ () => this.userId,
1672
+ this.logger
1673
+ );
1674
+ this.signalRouter.register("alert", this.singleCallHandler);
1675
+ this.signalRouter.register("confirmRing", this.singleCallHandler);
1676
+ this.signalRouter.register("answerCall", this.singleCallHandler);
1677
+ this.signalRouter.register("cancelCall", this.singleCallHandler);
1678
+ this.signalRouter.register("leaveCall", this.singleCallHandler);
1679
+ this.signalRouter.register("confirmCallee", this.singleCallHandler);
1680
+ this.signalRouter.register("answerCall", this.groupCallHandler);
1681
+ this.signalRouter.register("cancelCall", this.groupCallHandler);
1682
+ this.signalRouter.register("leaveCall", this.groupCallHandler);
1683
+ this.imListener = new IMListener(
1684
+ config.imClient,
1685
+ {
1686
+ onTextMessage: (msg) => this.handleTextMessage(msg),
1687
+ onCmdMessage: (msg) => this.handleCmdMessage(msg),
1688
+ onConnected: () => this.handleIMConnected(),
1689
+ onDisconnected: () => this.handleIMDisconnected()
1690
+ },
1691
+ this.logger
1692
+ );
1693
+ this.imListener.mount();
1694
+ this.logger.info("[CallKitCore] 初始化完成(userId/deviceId 将在登录后实时读取)");
1695
+ this.logger.warn("🚀 ========== CallKitCore 链路已激活 ========== 🚀");
1696
+ this.logger.warn(" 版本: callkit-core | 当前用户:", this.userId || "(尚未登录)", "| 设备:", this.deviceId || "(尚未登录)");
1697
+ }
1698
+ get userId() {
1699
+ return this.config.imClient.context?.userId || this.config.imClient.user || "";
1700
+ }
1701
+ get deviceId() {
1702
+ return this.config.imClient.context?.jid?.clientResource || "";
1703
+ }
1704
+ // ───────────────────────────────────────────────
1705
+ // 单聊 API
1706
+ // ───────────────────────────────────────────────
1707
+ /**
1708
+ * 发起单聊通话
1709
+ */
1710
+ async inviteCall(params) {
1711
+ if (this.destroyed) throw new Error("CallKitCore 已销毁");
1712
+ const callId = generateRandomChannel(16);
1713
+ const channel = generateRandomChannel(12);
1714
+ const token = await this.fetchRtcToken(channel);
1715
+ const stateResult = this.singleCallState.initInvite({
1716
+ calleeUserId: params.calleeUserId,
1717
+ callType: params.callType,
1718
+ callerDevId: this.deviceId,
1719
+ callerUserId: this.userId,
1720
+ callId,
1721
+ channel,
1722
+ token,
1723
+ timeout: this.inviteTimeoutMs
1724
+ });
1725
+ this.startInviteTimeout();
1726
+ const callerInfo = {
1727
+ ...this.config.userProfile,
1728
+ ...params.callerInfo
1729
+ };
1730
+ const ext = MessageBuilder.buildInviteExt({
1731
+ callId,
1732
+ callerUserId: this.userId,
1733
+ calleeUserId: params.calleeUserId,
1734
+ callerDevId: this.deviceId,
1735
+ channel,
1736
+ callType: params.callType,
1737
+ callerInfo
1738
+ });
1739
+ await this.signalSender.sendInviteMessage(
1740
+ params.calleeUserId,
1741
+ "singleChat",
1742
+ "[通话邀请]",
1743
+ ext
1744
+ );
1745
+ this.processEvents(stateResult.events, this.singleCallState.getState());
1746
+ }
1747
+ /**
1748
+ * 响应来电(接受/拒绝)
1749
+ */
1750
+ async answerCall(params) {
1751
+ if (this.destroyed) throw new Error("CallKitCore 已销毁");
1752
+ const state = this.singleCallState.getState();
1753
+ const result = params.result ?? (params.accept ? "accept" : "refuse");
1754
+ const isGroupCall = state.type === CALL_TYPE.VIDEO_MULTI || state.type === CALL_TYPE.AUDIO_MULTI;
1755
+ const ext = MessageBuilder.buildCmdExt({
1756
+ action: "answerCall",
1757
+ callId: state.callId,
1758
+ callerDevId: state.callerDevId,
1759
+ calleeDevId: this.deviceId,
1760
+ result
1761
+ });
1762
+ await this.signalSender.sendCmdMessage(
1763
+ state.callerUserId,
1764
+ "singleChat",
1765
+ ext,
1766
+ { deliverOnlineOnly: true }
1767
+ );
1768
+ if (result === "accept") {
1769
+ if (isGroupCall) {
1770
+ this.logger.info("[CallKitCore] 群聊接受,等待 confirmCallee 后再进入 IN_CALL");
1771
+ this.clearInviteTimeout();
1772
+ } else {
1773
+ this.logger.info("[CallKitCore] 单聊接受,等待 confirmCallee");
1774
+ }
1775
+ } else {
1776
+ const hangupReason = result === "busy" ? HANGUP_REASON.BUSY : HANGUP_REASON.REFUSE;
1777
+ const hangupResult = this.singleCallState.hangup(hangupReason);
1778
+ this.processEvents(hangupResult.events, state);
1779
+ }
1780
+ }
1781
+ /**
1782
+ * 挂断/取消通话
1783
+ */
1784
+ async hangup(params) {
1785
+ if (this.destroyed) throw new Error("CallKitCore 已销毁");
1786
+ const state = this.singleCallState.getState();
1787
+ const currentStatus = state.status;
1788
+ if (currentStatus === CALL_STATUS.IDLE) {
1789
+ this.logger.warn("[CallKitCore] hangup: 当前 IDLE,忽略");
1790
+ return;
1791
+ }
1792
+ const isGroupCall = state.type === CALL_TYPE.VIDEO_MULTI || state.type === CALL_TYPE.AUDIO_MULTI;
1793
+ if (currentStatus === CALL_STATUS.INVITING || currentStatus === CALL_STATUS.ALERTING) {
1794
+ this.clearInviteTimeout();
1795
+ const ext = MessageBuilder.buildCmdExt({
1796
+ action: "cancelCall",
1797
+ callId: state.callId,
1798
+ callerDevId: state.callerDevId
1799
+ });
1800
+ if (isGroupCall) {
1801
+ const groupSnapshot = this.groupCallSession.getSnapshot();
1802
+ const groupId = groupSnapshot?.groupId || state.calleeUserId;
1803
+ const allParticipants = this.groupCallSession.getAllParticipants();
1804
+ const receiverList = allParticipants.filter((p) => p.userId !== this.userId && p.state === "invited").map((p) => p.userId);
1805
+ this.logger.warn(
1806
+ "[CallKitCore] 群聊取消: participants=",
1807
+ allParticipants.map((p) => ({ userId: p.userId, state: p.state })),
1808
+ "| receiverList=",
1809
+ receiverList
1810
+ );
1811
+ if (groupId && receiverList.length > 0) {
1812
+ this.logger.warn("[CallKitCore] 群聊取消: 发送 cancelCall 到", groupId, "receiverList:", receiverList);
1813
+ await this.signalSender.sendCmdMessage(groupId, "groupChat", ext, { receiverList }).catch((err) => {
1814
+ this.logger.error("[CallKitCore] 群聊取消: 发送 cancelCall 失败", err);
1815
+ });
1816
+ } else {
1817
+ this.logger.warn("[CallKitCore] 群聊取消: 无接收者,跳过发送 cancelCall");
1818
+ }
1819
+ } else {
1820
+ const targetId = state.callerUserId === this.userId ? state.calleeUserId : state.callerUserId;
1821
+ await this.signalSender.sendCmdMessage(targetId, "singleChat", ext).catch(() => {
1822
+ });
1823
+ }
1824
+ } else if (currentStatus === CALL_STATUS.IN_CALL) {
1825
+ this.clearInviteTimeout();
1826
+ const ext = MessageBuilder.buildCmdExt({
1827
+ action: "leaveCall",
1828
+ callId: state.callId
1829
+ });
1830
+ if (isGroupCall) {
1831
+ const groupSnapshot = this.groupCallSession.getSnapshot();
1832
+ const groupId = groupSnapshot?.groupId || state.calleeUserId;
1833
+ const receiverList = this.groupCallSession.getAllParticipants().filter((p) => p.userId !== this.userId && p.state !== "left").map((p) => p.userId);
1834
+ if (groupId && receiverList.length > 0) {
1835
+ await this.signalSender.sendCmdMessage(groupId, "groupChat", ext, { receiverList }).catch(() => {
1836
+ });
1837
+ }
1838
+ } else {
1839
+ const targetId = state.callerUserId === this.userId ? state.calleeUserId : state.callerUserId;
1840
+ await this.signalSender.sendCmdMessage(targetId, "singleChat", ext).catch(() => {
1841
+ });
1842
+ }
1843
+ }
1844
+ const reason = params?.reason === "cancel" ? HANGUP_REASON.CANCEL : params?.reason === "timeout" ? HANGUP_REASON.NO_RESPONSE : HANGUP_REASON.HANGUP;
1845
+ const hangupResult = this.singleCallState.hangup(reason);
1846
+ this.clearInviteTimeout();
1847
+ this.processEvents(hangupResult.events, state);
1848
+ }
1849
+ // ───────────────────────────────────────────────
1850
+ // 群聊 API
1851
+ // ───────────────────────────────────────────────
1852
+ /**
1853
+ * 通话中邀请更多成员加入群聊通话
1854
+ */
1855
+ async inviteMoreParticipants(participantIds) {
1856
+ if (this.destroyed) throw new Error("CallKitCore 已销毁");
1857
+ const state = this.singleCallState.getState();
1858
+ if (state.status !== CALL_STATUS.IN_CALL && state.status !== CALL_STATUS.INVITING) {
1859
+ this.logger.warn("[CallKitCore] inviteMoreParticipants: 当前不在通话中,忽略");
1860
+ return;
1861
+ }
1862
+ const isGroupCall = state.type === CALL_TYPE.VIDEO_MULTI || state.type === CALL_TYPE.AUDIO_MULTI;
1863
+ if (!isGroupCall) {
1864
+ this.logger.warn("[CallKitCore] inviteMoreParticipants: 当前不是群聊通话,忽略");
1865
+ return;
1866
+ }
1867
+ const groupSnapshot = this.groupCallSession.getSnapshot();
1868
+ const groupId = groupSnapshot?.groupId;
1869
+ if (!groupId) {
1870
+ this.logger.warn("[CallKitCore] inviteMoreParticipants: 群聊会话未初始化");
1871
+ return;
1872
+ }
1873
+ const ext = MessageBuilder.buildInviteExt({
1874
+ callId: state.callId,
1875
+ callerUserId: this.userId,
1876
+ calleeUserId: groupId,
1877
+ callerDevId: this.deviceId,
1878
+ channel: state.channel,
1879
+ callType: state.type,
1880
+ invitedMembers: participantIds,
1881
+ groupInfo: { groupId, groupName: groupSnapshot?.groupName || groupId },
1882
+ callerInfo: this.config.userProfile
1883
+ });
1884
+ try {
1885
+ await this.signalSender.sendInviteMessage(
1886
+ participantIds,
1887
+ "groupChat",
1888
+ "[群通话邀请]",
1889
+ ext,
1890
+ groupId
1891
+ );
1892
+ } catch (err) {
1893
+ this.emitError("inviteMoreParticipantsFailed", err, { callId: state.callId, participantIds });
1894
+ this.logger.error("[CallKitCore] 发送追加邀请失败", err);
1895
+ throw err;
1896
+ }
1897
+ participantIds.forEach((userId) => {
1898
+ if (!this.groupCallSession.getParticipant(userId)) {
1899
+ this.groupCallSession.addParticipant({
1900
+ userId,
1901
+ nickname: userId,
1902
+ avatarUrl: "",
1903
+ state: "invited",
1904
+ isLocal: false,
1905
+ isMuted: false,
1906
+ isCameraOn: false,
1907
+ isSpeaking: false
1908
+ });
1909
+ }
1910
+ });
1911
+ this.logger.info("[CallKitCore] 已发送追加邀请", { groupId, participantIds });
1912
+ }
1913
+ /**
1914
+ * 发起群聊通话
1915
+ */
1916
+ async inviteGroupCall(params) {
1917
+ if (this.destroyed) throw new Error("CallKitCore 已销毁");
1918
+ const callId = generateRandomChannel(16);
1919
+ const channel = generateRandomChannel(12);
1920
+ const callTypeStr = params.callType === CALL_TYPE.VIDEO_MULTI ? "video" : "audio";
1921
+ const token = await this.fetchRtcToken(channel);
1922
+ const groupName = params.ext?.groupName || params.groupId;
1923
+ const groupAvatar = params.ext?.groupAvatar;
1924
+ this.groupCallSession.init({
1925
+ sessionId: channel,
1926
+ groupId: params.groupId,
1927
+ groupName,
1928
+ callType: callTypeStr,
1929
+ callerUserId: this.userId
1930
+ });
1931
+ this.groupCallSession.addParticipant({
1932
+ userId: this.userId,
1933
+ nickname: this.userId,
1934
+ avatarUrl: "",
1935
+ state: "joinedRtc",
1936
+ isLocal: true,
1937
+ isMuted: false,
1938
+ isCameraOn: callTypeStr === "video",
1939
+ isSpeaking: false
1940
+ });
1941
+ params.participantIds.forEach((userId) => {
1942
+ this.groupCallSession.addParticipant({
1943
+ userId,
1944
+ nickname: userId,
1945
+ avatarUrl: "",
1946
+ state: "invited",
1947
+ isLocal: false,
1948
+ isMuted: false,
1949
+ isCameraOn: false,
1950
+ isSpeaking: false
1951
+ });
1952
+ });
1953
+ this.processEvents(
1954
+ [
1955
+ {
1956
+ type: "GROUP_CALL_INIT",
1957
+ callId,
1958
+ groupId: params.groupId,
1959
+ groupName,
1960
+ channel,
1961
+ callType: callTypeStr,
1962
+ callerUserId: this.userId,
1963
+ invitedMembers: params.participantIds
1964
+ }
1965
+ ],
1966
+ this.singleCallState.getState()
1967
+ );
1968
+ const stateResult = this.singleCallState.initInvite({
1969
+ calleeUserId: params.groupId,
1970
+ callType: params.callType,
1971
+ callerDevId: this.deviceId,
1972
+ callerUserId: this.userId,
1973
+ callId,
1974
+ channel,
1975
+ token,
1976
+ timeout: this.inviteTimeoutMs
1977
+ });
1978
+ const callerInfo = {
1979
+ ...this.config.userProfile,
1980
+ ...params.callerInfo
1981
+ };
1982
+ const ext = MessageBuilder.buildInviteExt({
1983
+ callId,
1984
+ callerUserId: this.userId,
1985
+ calleeUserId: params.groupId,
1986
+ callerDevId: this.deviceId,
1987
+ channel,
1988
+ callType: params.callType,
1989
+ invitedMembers: params.participantIds,
1990
+ groupInfo: { groupId: params.groupId, groupName, groupAvatar },
1991
+ callerInfo
1992
+ });
1993
+ const inviteMessage = params.ext?.message || "[群通话邀请]";
1994
+ await this.signalSender.sendInviteMessage(
1995
+ params.participantIds,
1996
+ "groupChat",
1997
+ inviteMessage,
1998
+ ext,
1999
+ params.groupId
2000
+ );
2001
+ this.processEvents(stateResult.events, this.singleCallState.getState());
2002
+ this.logger.info("[CallKitCore] 群聊主叫方:进入 IN_CALL 并触发 RTC 加入");
2003
+ const answerResult = this.singleCallState.receiveAnswer("accept", false);
2004
+ if (answerResult.ok) {
2005
+ this.processEvents(answerResult.events, this.singleCallState.getState());
2006
+ }
2007
+ }
2008
+ // ───────────────────────────────────────────────
2009
+ // 媒体控制
2010
+ // ───────────────────────────────────────────────
2011
+ /**
2012
+ * 切换本地音频(静音/取消静音)
2013
+ */
2014
+ toggleAudio() {
2015
+ if (this.destroyed) throw new Error("CallKitCore 已销毁");
2016
+ const stateResult = this.singleCallState.toggleAudio();
2017
+ this.processEvents(stateResult.events, this.singleCallState.getState());
2018
+ }
2019
+ /**
2020
+ * 切换本地视频(开启/关闭摄像头)
2021
+ */
2022
+ toggleVideo() {
2023
+ if (this.destroyed) throw new Error("CallKitCore 已销毁");
2024
+ const stateResult = this.singleCallState.toggleVideo();
2025
+ this.processEvents(stateResult.events, this.singleCallState.getState());
2026
+ }
2027
+ // ───────────────────────────────────────────────
2028
+ // RTC 反馈
2029
+ // ───────────────────────────────────────────────
2030
+ /**
2031
+ * 获取 RTC token(并缓存 appId / RTCUId)
2032
+ * - 环信真实 SDK:`{ data: { RTCToken, appId, RTCUId, expireIn } }`
2033
+ * - 早期 / 精简 mock:`{ accessToken, appId }`
2034
+ * 为了与旧版 lib/composables/useJoinChannel.ts 行为对齐,同步保存 rtcAppId、rtcUid供
2035
+ * shouldJoinRtc 事件透传给上层 RtcAdapter(Agora 的 join 必须使用服务端返回的数值型 uid)。
2036
+ */
2037
+ async fetchRtcToken(channel) {
2038
+ try {
2039
+ const tokenRes = await this.config.imClient.getRTCToken(channel);
2040
+ const data = tokenRes?.data ?? tokenRes ?? {};
2041
+ const token = data.RTCToken ?? data.accessToken ?? tokenRes?.accessToken ?? "";
2042
+ const appId = data.appId ?? tokenRes?.appId ?? "";
2043
+ const rtcUid = Number(data.RTCUId ?? data.rtcUid ?? 0) || 0;
2044
+ if (appId) this.rtcAppId = appId;
2045
+ if (rtcUid) this.rtcUid = rtcUid;
2046
+ if (!token) {
2047
+ this.logger.warn("[CallKitCore] getRTCToken 返回 token 为空", { tokenRes });
2048
+ } else {
2049
+ this.logger.info("[CallKitCore] 获取 RTC token 成功", {
2050
+ channel,
2051
+ appId: this.rtcAppId,
2052
+ rtcUid: this.rtcUid,
2053
+ hasToken: !!token
2054
+ });
2055
+ }
2056
+ return token;
2057
+ } catch (err) {
2058
+ this.emitError("rtcTokenFetchFailed", err, { channel });
2059
+ this.logger.warn("[CallKitCore] 获取 RTC token 失败,使用空 token", err);
2060
+ return "";
2061
+ }
2062
+ }
2063
+ /**
2064
+ * 上层调用 RTC SDK 后,通过此方法反馈 RTC 事件给核心库
2065
+ */
2066
+ reportRtcEvent(report) {
2067
+ this.logger.info("[CallKitCore] reportRtcEvent", report);
2068
+ const { type, payload } = report;
2069
+ if (payload.userId) {
2070
+ switch (type) {
2071
+ case "userJoined":
2072
+ case "userPublished":
2073
+ this.groupCallSession.markJoinedRtc(payload.userId);
2074
+ break;
2075
+ case "userLeft":
2076
+ case "userUnpublished":
2077
+ this.groupCallSession.markLeftRtc(payload.userId);
2078
+ break;
2079
+ case "userAudioMuted":
2080
+ this.groupCallSession.markAudioMuted(payload.userId, true);
2081
+ break;
2082
+ case "userAudioUnmuted":
2083
+ this.groupCallSession.markAudioMuted(payload.userId, false);
2084
+ break;
2085
+ case "userVideoMuted":
2086
+ this.groupCallSession.markVideoOn(payload.userId, false);
2087
+ break;
2088
+ case "userVideoUnmuted":
2089
+ this.groupCallSession.markVideoOn(payload.userId, true);
2090
+ break;
2091
+ }
2092
+ }
2093
+ if (type === "networkQuality" || type === "speaking" || type === "stoppedSpeaking" || type === "error") {
2094
+ const rtcEvent = {
2095
+ type: "rtcReport",
2096
+ payload: { type: report.type, payload: report.payload }
2097
+ };
2098
+ this.emitEvent(rtcEvent);
2099
+ }
2100
+ }
2101
+ // ───────────────────────────────────────────────
2102
+ // 状态查询
2103
+ // ───────────────────────────────────────────────
2104
+ getSingleCallState() {
2105
+ return this.singleCallState.getState();
2106
+ }
2107
+ getGroupCallSession() {
2108
+ return this.groupCallSession.getSnapshot();
2109
+ }
2110
+ /**
2111
+ * 获取群聊通话的所有参与者(包含状态信息)
2112
+ */
2113
+ getGroupCallParticipants() {
2114
+ return this.groupCallSession.getAllParticipants();
2115
+ }
2116
+ /**
2117
+ * 当前是否在通话中(IN_CALL 状态)
2118
+ */
2119
+ isInCall() {
2120
+ return this.singleCallState.isInCall();
2121
+ }
2122
+ /**
2123
+ * 当前是否处于可被接听的状态(被叫端弹窗显示区间)
2124
+ */
2125
+ isWaitingCalleeAction() {
2126
+ return this.singleCallState.isWaitingCalleeAction();
2127
+ }
2128
+ /**
2129
+ * 当前是否处于活跃通话中(已进入 RTC 或即将进入)
2130
+ */
2131
+ isInActiveCall() {
2132
+ return this.singleCallState.isInActiveCall();
2133
+ }
2134
+ /**
2135
+ * 当前是否可以接听(被叫端按钮可点击)
2136
+ */
2137
+ canAccept() {
2138
+ return this.singleCallState.canAccept();
2139
+ }
2140
+ /**
2141
+ * 当前是否可以拒绝(被叫端按钮可点击)
2142
+ */
2143
+ canReject() {
2144
+ return this.singleCallState.canReject();
2145
+ }
2146
+ /**
2147
+ * 当前是否可以挂断(主叫/被叫端的挂断按钮可点击)
2148
+ */
2149
+ canHangup() {
2150
+ return this.singleCallState.canHangup();
2151
+ }
2152
+ /**
2153
+ * 当前是否在呼叫/响铃中(INVITING 或 ALERTING 状态)
2154
+ */
2155
+ isCalling() {
2156
+ return this.singleCallState.isCalling();
2157
+ }
2158
+ /**
2159
+ * 获取当前通话类型,无通话时返回 null
2160
+ */
2161
+ getCurrentCallType() {
2162
+ const state = this.singleCallState.getState();
2163
+ return state.status === CALL_STATUS.IDLE ? null : state.type;
2164
+ }
2165
+ /**
2166
+ * 获取当前通话 ID,无通话时返回空字符串
2167
+ */
2168
+ getCurrentCallId() {
2169
+ return this.singleCallState.getState().callId || "";
2170
+ }
2171
+ /**
2172
+ * 当前是否空闲
2173
+ */
2174
+ isIdle() {
2175
+ return this.singleCallState.isIdle();
2176
+ }
2177
+ // ───────────────────────────────────────────────
2178
+ // 事件订阅(供上层精细控制)
2179
+ // ───────────────────────────────────────────────
2180
+ /**
2181
+ * 订阅通话事件
2182
+ * @returns 取消订阅函数
2183
+ */
2184
+ onEvent(handler) {
2185
+ return this.eventBus.on("callKitEvent", handler);
2186
+ }
2187
+ /**
2188
+ * 订阅单次通话事件
2189
+ */
2190
+ onceEvent(handler) {
2191
+ return this.eventBus.once("callKitEvent", handler);
2192
+ }
2193
+ // ───────────────────────────────────────────────
2194
+ // 生命周期
2195
+ // ───────────────────────────────────────────────
2196
+ async destroy() {
2197
+ if (this.destroyed) return;
2198
+ this.destroyed = true;
2199
+ this.clearInviteTimeout();
2200
+ this.stopDurationTimer();
2201
+ this.imListener.unmount();
2202
+ this.eventBus.clear();
2203
+ this.singleCallState.reset();
2204
+ this.groupCallSession.destroy();
2205
+ this.logger.info("[CallKitCore] 已销毁");
2206
+ }
2207
+ // ───────────────────────────────────────────────
2208
+ // 内部:消息处理
2209
+ // ───────────────────────────────────────────────
2210
+ isSelfMessage(msg) {
2211
+ return msg.from === this.userId;
2212
+ }
2213
+ async handleTextMessage(msg) {
2214
+ const rawExt = msg.ext;
2215
+ const bodyExt = msg.body?.ext;
2216
+ const ext = rawExt || bodyExt;
2217
+ this.logger.warn("✉️ [CallKitCore] handleTextMessage 被调用", {
2218
+ from: msg.from,
2219
+ to: msg.to,
2220
+ msgType: msg.type,
2221
+ chatType: msg.chatType,
2222
+ hasRawExt: !!rawExt,
2223
+ hasBodyExt: !!bodyExt,
2224
+ extAction: ext?.action,
2225
+ extCallId: ext?.callId,
2226
+ self: this.isSelfMessage(msg)
2227
+ });
2228
+ if (this.isSelfMessage(msg)) {
2229
+ this.logger.warn("[CallKitCore] ❌ 忽略自己发送的文本消息");
2230
+ return;
2231
+ }
2232
+ if (!ext || ext.action !== "invite") {
2233
+ this.logger.warn("[CallKitCore] ❌ 忽略非 invite 文本消息 | ext.action=", ext?.action);
2234
+ return;
2235
+ }
2236
+ const isGroupCall = ext.chatType === CALL_TYPE.VIDEO_MULTI || ext.chatType === CALL_TYPE.AUDIO_MULTI || ext.callkitGroupInfo?.groupId;
2237
+ const currentStatus = this.singleCallState.getState().status;
2238
+ if (currentStatus > CALL_STATUS.IDLE) {
2239
+ this.logger.warn("[CallKitCore] ❌ 当前已在通话中,发送忙线拒绝 | currentStatus=", currentStatus);
2240
+ const busyExt = MessageBuilder.buildCmdExt({
2241
+ action: "answerCall",
2242
+ callId: ext.callId,
2243
+ callerDevId: ext.callerDevId,
2244
+ calleeDevId: this.deviceId,
2245
+ result: "busy"
2246
+ });
2247
+ this.signalSender.sendCmdMessage(msg.from, "singleChat", busyExt, {
2248
+ deliverOnlineOnly: true
2249
+ }).catch(() => {
2250
+ });
2251
+ return;
2252
+ }
2253
+ if (!isGroupCall) {
2254
+ if (msg.to && msg.to !== this.userId) {
2255
+ this.logger.warn("[CallKitCore] ❌ 单聊 invite 接收者不是当前用户 | msg.to=", msg.to);
2256
+ return;
2257
+ }
2258
+ if (ext.calleeDevId && ext.calleeDevId !== this.deviceId) {
2259
+ this.logger.warn("[CallKitCore] ❌ 单聊 invite calleeDevId 不匹配 | ext=", ext.calleeDevId, "| my=", this.deviceId);
2260
+ return;
2261
+ }
2262
+ } else {
2263
+ const invitedMembers = ext.invitedMembers || [];
2264
+ if (invitedMembers.length > 0 && !invitedMembers.includes(this.userId)) {
2265
+ this.logger.warn("[CallKitCore] ❌ 群聊 invite 当前用户不在被邀请列表中");
2266
+ return;
2267
+ }
2268
+ }
2269
+ const msgTime = msg.time || ext.ts;
2270
+ if (msgTime && isMessageExpired(msgTime, this.inviteTimeoutMs + 1e4)) {
2271
+ this.logger.warn("[CallKitCore] ❌ invite 消息已过期 | msgTime=", msgTime);
2272
+ return;
2273
+ }
2274
+ this.logger.warn(
2275
+ isGroupCall ? "✉️ [CallKitCore] ✅ 【群聊】确认收到 invite 文本消息 → 进入群聊处理分支" : "✉️ [CallKitCore] ✅ 【单聊】确认收到 invite 文本消息 → 进入单聊处理分支",
2276
+ { from: msg.from, callId: ext.callId, channel: ext.channelName, type: ext.type }
2277
+ );
2278
+ if (isGroupCall) {
2279
+ await this.handleGroupCallInvite(msg, ext).catch((err) => {
2280
+ this.emitError("groupCallInviteHandlingFailed", err, { messageId: msg.id });
2281
+ this.logger.error("[CallKitCore] 群聊 invite 处理失败", err);
2282
+ });
2283
+ } else {
2284
+ await this.handleSingleCallInvite(msg, ext);
2285
+ }
2286
+ }
2287
+ async handleGroupCallInvite(msg, ext) {
2288
+ const callId = ext.callId;
2289
+ const channel = ext.channelName;
2290
+ const callType = ext.type;
2291
+ const callerDevId = ext.callerDevId;
2292
+ const callerUserId = ext.callerIMName || msg.from || "";
2293
+ this.pendingIncomingInvites.set(callId, { aborted: false });
2294
+ const token = await this.fetchRtcToken(channel);
2295
+ const pending = this.pendingIncomingInvites.get(callId);
2296
+ this.pendingIncomingInvites.delete(callId);
2297
+ if (pending?.aborted) {
2298
+ this.logger.warn("[CallKitCore] 群聊 invite 在获取 token 期间已被取消/离开,跳过初始化");
2299
+ return;
2300
+ }
2301
+ if (this.singleCallState.getState().status === CALL_STATUS.IDLE) {
2302
+ this.logger.warn("🔄 [CallKitCore] 群聊被叫方:singleCallState 从 IDLE → ALERTING");
2303
+ const groupId = ext?.callkitGroupInfo?.groupId || ext?.groupId || "";
2304
+ const stateResult = this.singleCallState.initIncoming({
2305
+ callId,
2306
+ channel,
2307
+ token,
2308
+ callType,
2309
+ callerDevId,
2310
+ callerUserId,
2311
+ calleeDevId: this.deviceId,
2312
+ calleeUserId: groupId
2313
+ // 群聊时 calleeUserId 使用 groupId,与旧版对齐
2314
+ });
2315
+ this.logger.warn("[CallKitCore] initIncoming 返回事件数:", stateResult.events.length);
2316
+ this.processEvents(stateResult.events, this.singleCallState.getState());
2317
+ this.startInviteTimeout();
2318
+ } else {
2319
+ this.logger.warn(
2320
+ "[CallKitCore] ⚠️ 群聊被叫方:singleCallState 不是 IDLE,跳过 initIncoming | 当前状态=",
2321
+ this.singleCallState.getState().status
2322
+ );
2323
+ }
2324
+ const events = this.groupCallHandler.handleInviteTextMessage(msg);
2325
+ this.logger.warn("[CallKitCore] 群聊 invite 处理后事件:", events.map((e) => e.type));
2326
+ this.processEvents(events, this.singleCallState.getState());
2327
+ const incomingEvent = {
2328
+ type: "incomingCall",
2329
+ payload: {
2330
+ callId,
2331
+ callType,
2332
+ callerUserId,
2333
+ callerDevId,
2334
+ channel,
2335
+ calleeUserId: ext?.callkitGroupInfo?.groupId || ext?.groupId || "",
2336
+ token: "",
2337
+ callerInfo: ext.ease_chat_uikit_user_info,
2338
+ groupId: ext?.callkitGroupInfo?.groupId || ext?.groupId,
2339
+ groupName: ext?.callkitGroupInfo?.groupName
2340
+ }
2341
+ };
2342
+ this.logger.warn("[CallKitCore] 即将发出 incomingCall 事件:", { callId, callerUserId, callType });
2343
+ this.emitEvent(incomingEvent);
2344
+ this.logger.warn("[CallKitCore] incomingCall 事件已发出");
2345
+ this.sendAlertSignal(msg.from, ext.callId, ext.callerDevId);
2346
+ }
2347
+ async handleSingleCallInvite(msg, ext) {
2348
+ const callId = ext.callId;
2349
+ const channel = ext.channelName;
2350
+ const callType = ext.type;
2351
+ const callerDevId = ext.callerDevId;
2352
+ const callerUserId = ext.callerIMName || msg.from || "";
2353
+ const calleeUserId = ext.calleeIMName || msg.to || "";
2354
+ this.logger.debug("[CallKitCore] handleSingleCallInvite", {
2355
+ from: msg.from,
2356
+ callerIMName: ext.callerIMName,
2357
+ calleeIMName: ext.calleeIMName,
2358
+ resolvedCallerId: callerUserId,
2359
+ resolvedCalleeId: calleeUserId
2360
+ });
2361
+ const token = await this.fetchRtcToken(channel);
2362
+ const stateResult = this.singleCallState.initIncoming({
2363
+ callId,
2364
+ channel,
2365
+ token,
2366
+ callType,
2367
+ callerDevId,
2368
+ callerUserId,
2369
+ calleeDevId: this.deviceId,
2370
+ calleeUserId
2371
+ });
2372
+ this.startInviteTimeout();
2373
+ const incomingEvent = {
2374
+ type: "incomingCall",
2375
+ payload: {
2376
+ callId,
2377
+ callType,
2378
+ callerUserId,
2379
+ callerDevId,
2380
+ channel,
2381
+ calleeUserId,
2382
+ token,
2383
+ callerInfo: ext.ease_chat_uikit_user_info
2384
+ }
2385
+ };
2386
+ this.emitEvent(incomingEvent);
2387
+ this.processEvents(stateResult.events, this.singleCallState.getState());
2388
+ this.sendAlertSignal(msg.from, callId, callerDevId);
2389
+ }
2390
+ /**
2391
+ * 发送 alert CMD 信令给主叫方
2392
+ */
2393
+ sendAlertSignal(to, callId, callerDevId) {
2394
+ const alertExt = MessageBuilder.buildCmdExt({
2395
+ action: "alert",
2396
+ callId,
2397
+ callerDevId,
2398
+ calleeDevId: this.deviceId
2399
+ });
2400
+ this.signalSender.sendCmdMessage(to, "singleChat", alertExt, { deliverOnlineOnly: true }).catch(() => {
2401
+ });
2402
+ }
2403
+ handleCmdMessage(msg) {
2404
+ this.logger.warn("📨 [CallKitCore] handleCmdMessage 被调用", {
2405
+ from: msg.from,
2406
+ to: msg.to,
2407
+ action: msg.action,
2408
+ extAction: msg.ext?.action,
2409
+ callId: msg.ext?.callId,
2410
+ self: this.isSelfMessage(msg)
2411
+ });
2412
+ if (this.isSelfMessage(msg)) {
2413
+ this.logger.warn("[CallKitCore] ❌ 忽略自己发送的 CMD 消息");
2414
+ return;
2415
+ }
2416
+ if (msg.action !== "rtcCall") {
2417
+ this.logger.warn("[CallKitCore] ❌ 忽略非 rtcCall CMD 消息 | action=", msg.action);
2418
+ return;
2419
+ }
2420
+ const cmdTime = msg.time || msg.ext?.ts;
2421
+ if (cmdTime && isCmdMessageExpired(cmdTime)) {
2422
+ this.logger.warn("[CallKitCore] ❌ CMD 消息已过期 | cmdTime=", cmdTime);
2423
+ return;
2424
+ }
2425
+ const extAction = msg.ext?.action;
2426
+ const extCallId = msg.ext?.callId;
2427
+ if (extCallId && (extAction === "cancelCall" || extAction === "leaveCall") && this.pendingIncomingInvites.has(extCallId)) {
2428
+ this.logger.warn("[CallKitCore] 待处理 invite 收到取消/离开信令,标记为 aborted", {
2429
+ callId: extCallId,
2430
+ action: extAction
2431
+ });
2432
+ this.pendingIncomingInvites.get(extCallId).aborted = true;
2433
+ return;
2434
+ }
2435
+ const events = this.signalRouter.dispatch(msg);
2436
+ if (events.length > 0) {
2437
+ this.logger.warn("📨 [CallKitCore] ✅ CMD 消息处理后产生事件:", events.map((e) => e.type));
2438
+ this.processEvents(events, this.singleCallState.getState());
2439
+ } else {
2440
+ this.logger.warn("📨 [CallKitCore] ⚠️ CMD 消息未产生任何事件(可能被忽略或 handler 返回空)");
2441
+ }
2442
+ }
2443
+ // ───────────────────────────────────────────────
2444
+ // 内部:事件处理与映射
2445
+ // ───────────────────────────────────────────────
2446
+ processEvents(events, snapshot) {
2447
+ events.forEach((event) => {
2448
+ const callKitEvents = this.mapDomainEvents(event, snapshot);
2449
+ callKitEvents.forEach((callKitEvent) => {
2450
+ this.emitEvent(callKitEvent);
2451
+ this.handleRtcEvent(callKitEvent);
2452
+ if (callKitEvent.type === "callStarted" || callKitEvent.type === "callConnected") {
2453
+ this.startDurationTimer(
2454
+ callKitEvent.payload.callId,
2455
+ callKitEvent.payload.channel,
2456
+ callKitEvent.payload.callType,
2457
+ callKitEvent.payload.callerUserId
2458
+ );
2459
+ }
2460
+ if (callKitEvent.type === "callEnded") {
2461
+ this.stopDurationTimer();
2462
+ }
2463
+ });
2464
+ });
2465
+ }
2466
+ /**
2467
+ * 当配置了 rtcAdapter 时,自动处理 RTC 相关事件
2468
+ */
2469
+ handleRtcEvent(event) {
2470
+ const adapter = this.config.rtcAdapter;
2471
+ if (!adapter) return;
2472
+ switch (event.type) {
2473
+ case "shouldJoinRtc": {
2474
+ const p = event.payload;
2475
+ adapter.joinChannel({
2476
+ channel: p.channel,
2477
+ token: p.token,
2478
+ uid: p.uid,
2479
+ appId: p.appId
2480
+ }).catch((e) => {
2481
+ this.emitError("rtcJoinChannelFailed", e, { channel: p.channel, uid: p.uid });
2482
+ this.logger.error("[CallKitCore] rtcAdapter.joinChannel 失败:", e);
2483
+ });
2484
+ break;
2485
+ }
2486
+ case "shouldLeaveRtc": {
2487
+ adapter.leaveChannel().catch((e) => {
2488
+ this.emitError("rtcLeaveChannelFailed", e, { channel: event.payload.channel });
2489
+ });
2490
+ break;
2491
+ }
2492
+ case "shouldPublishTracks": {
2493
+ const p = event.payload;
2494
+ adapter.publishLocalTracks(p.trackTypes).catch((e) => {
2495
+ this.emitError("rtcPublishTracksFailed", e, { channel: p.channel, trackTypes: p.trackTypes });
2496
+ this.logger.error("[CallKitCore] rtcAdapter.publishLocalTracks 失败:", e);
2497
+ });
2498
+ break;
2499
+ }
2500
+ case "localAudioChanged": {
2501
+ adapter.setAudioEnabled(event.payload.enabled).catch((e) => {
2502
+ this.emitError("rtcSetAudioEnabledFailed", e, { enabled: event.payload.enabled });
2503
+ this.logger.error("[CallKitCore] rtcAdapter.setAudioEnabled 失败:", e);
2504
+ });
2505
+ break;
2506
+ }
2507
+ case "localVideoChanged": {
2508
+ adapter.setVideoEnabled(event.payload.enabled).catch((e) => {
2509
+ this.emitError("rtcSetVideoEnabledFailed", e, { enabled: event.payload.enabled });
2510
+ this.logger.error("[CallKitCore] rtcAdapter.setVideoEnabled 失败:", e);
2511
+ });
2512
+ break;
2513
+ }
2514
+ }
2515
+ }
2516
+ mapDomainEvents(event, snapshot) {
2517
+ const base = {
2518
+ callId: event.callId,
2519
+ channel: snapshot.channel,
2520
+ callType: snapshot.type,
2521
+ callerUserId: snapshot.callerUserId,
2522
+ calleeUserId: snapshot.calleeUserId
2523
+ };
2524
+ const isGroupCall = snapshot.type === CALL_TYPE.VIDEO_MULTI || snapshot.type === CALL_TYPE.AUDIO_MULTI;
2525
+ switch (event.type) {
2526
+ case "STATUS_CHANGED": {
2527
+ return [
2528
+ {
2529
+ type: "statusChanged",
2530
+ payload: {
2531
+ ...base,
2532
+ from: String(event.from),
2533
+ to: String(event.to)
2534
+ }
2535
+ }
2536
+ ];
2537
+ }
2538
+ case "CALL_INVITED": {
2539
+ const common = { ...base, isCaller: event.isCaller };
2540
+ return [
2541
+ { type: "callInvited", payload: common },
2542
+ { type: isGroupCall ? "groupCallInvited" : "singleCallInvited", payload: common }
2543
+ ];
2544
+ }
2545
+ case "CALL_STARTED": {
2546
+ const common = {
2547
+ ...base,
2548
+ isCaller: event.isCaller,
2549
+ startTime: Date.now()
2550
+ };
2551
+ return [
2552
+ { type: "callStarted", payload: common },
2553
+ { type: isGroupCall ? "groupCallStarted" : "singleCallStarted", payload: common }
2554
+ ];
2555
+ }
2556
+ case "CALL_ACCEPTED": {
2557
+ const common = { ...base, isCaller: event.isCaller };
2558
+ return [
2559
+ { type: "callAccepted", payload: common },
2560
+ { type: isGroupCall ? "groupCallAccepted" : "singleCallAccepted", payload: common }
2561
+ ];
2562
+ }
2563
+ case "CALL_CONNECTED": {
2564
+ return [
2565
+ { type: "callConnected", payload: base },
2566
+ { type: isGroupCall ? "groupCallConnected" : "singleCallConnected", payload: base }
2567
+ ];
2568
+ }
2569
+ case "CALL_ENDED": {
2570
+ const common = {
2571
+ ...base,
2572
+ reason: event.reason,
2573
+ duration: event.duration
2574
+ };
2575
+ return [
2576
+ { type: "callEnded", payload: common },
2577
+ { type: isGroupCall ? "groupCallEnded" : "singleCallEnded", payload: common }
2578
+ ];
2579
+ }
2580
+ case "CALL_TIMEOUT": {
2581
+ return [
2582
+ { type: "callTimeout", payload: base },
2583
+ { type: isGroupCall ? "groupCallTimeout" : "singleCallTimeout", payload: base }
2584
+ ];
2585
+ }
2586
+ case "CALL_REFUSED": {
2587
+ const common = { ...base, isRemote: event.isRemote };
2588
+ return [
2589
+ { type: "callRefused", payload: common },
2590
+ { type: isGroupCall ? "groupCallRefused" : "singleCallRefused", payload: common }
2591
+ ];
2592
+ }
2593
+ case "CALL_BUSY": {
2594
+ return [
2595
+ { type: "callBusy", payload: base },
2596
+ { type: isGroupCall ? "groupCallBusy" : "singleCallBusy", payload: base }
2597
+ ];
2598
+ }
2599
+ case "CALL_CANCELED": {
2600
+ const common = { ...base, isRemote: event.isRemote };
2601
+ return [
2602
+ { type: "callCanceled", payload: common },
2603
+ { type: isGroupCall ? "groupCallCanceled" : "singleCallCanceled", payload: common }
2604
+ ];
2605
+ }
2606
+ case "SHOULD_JOIN_RTC": {
2607
+ return [
2608
+ {
2609
+ type: "shouldJoinRtc",
2610
+ payload: {
2611
+ ...base,
2612
+ token: event.token,
2613
+ // Agora 加入频道必须使用服务端返回的 RTCUId(数值型),
2614
+ // 无法获取时兑底为 IM userId(与旧版付费智能兑底逻辑一致)
2615
+ uid: this.rtcUid || this.userId,
2616
+ appId: this.rtcAppId || void 0,
2617
+ role: event.role
2618
+ }
2619
+ }
2620
+ ];
2621
+ }
2622
+ case "GROUP_CALL_INIT": {
2623
+ return [
2624
+ {
2625
+ type: "groupCallInit",
2626
+ payload: {
2627
+ callId: event.callId,
2628
+ groupId: event.groupId,
2629
+ groupName: event.groupName,
2630
+ channel: event.channel,
2631
+ callType: event.callType,
2632
+ callerUserId: event.callerUserId,
2633
+ invitedMembers: event.invitedMembers
2634
+ }
2635
+ }
2636
+ ];
2637
+ }
2638
+ case "PARTICIPANT_STATE_CHANGED": {
2639
+ return [
2640
+ {
2641
+ type: "participantStateChanged",
2642
+ payload: {
2643
+ callId: event.callId,
2644
+ userId: event.userId,
2645
+ state: event.state,
2646
+ groupId: event.groupId
2647
+ }
2648
+ }
2649
+ ];
2650
+ }
2651
+ case "PARTICIPANT_JOINED": {
2652
+ return [
2653
+ {
2654
+ type: "participantJoined",
2655
+ payload: {
2656
+ ...base,
2657
+ userId: event.userId,
2658
+ groupId: event.groupId
2659
+ }
2660
+ }
2661
+ ];
2662
+ }
2663
+ case "PARTICIPANT_LEFT": {
2664
+ return [
2665
+ {
2666
+ type: "participantLeft",
2667
+ payload: {
2668
+ ...base,
2669
+ userId: event.userId,
2670
+ reason: event.reason,
2671
+ groupId: event.groupId
2672
+ }
2673
+ }
2674
+ ];
2675
+ }
2676
+ case "LOCAL_AUDIO_CHANGED": {
2677
+ return [{ type: "localAudioChanged", payload: { enabled: event.enabled } }];
2678
+ }
2679
+ case "LOCAL_VIDEO_CHANGED": {
2680
+ return [{ type: "localVideoChanged", payload: { enabled: event.enabled } }];
2681
+ }
2682
+ default:
2683
+ return [];
2684
+ }
2685
+ }
2686
+ emitEvent(event) {
2687
+ this.logger.debug("[CallKitCore] emitEvent:", event.type, "| onEvent存在=", !!this.config.onEvent, "| onUIEvent存在=", !!this.config.onUIEvent);
2688
+ const hasEventBusListeners = this.eventBus.listenerCount("callKitEvent") > 0;
2689
+ const hasLegacyOnEvent = !!this.config.onEvent;
2690
+ if (hasEventBusListeners && hasLegacyOnEvent) {
2691
+ this.logger.warn(
2692
+ "[CallKitCore] 同时使用了 config.onEvent 和 core.onEvent() 订阅,事件可能重复触发。建议只使用其中一种订阅方式。"
2693
+ );
2694
+ }
2695
+ this.eventBus.emit("callKitEvent", event);
2696
+ if (this.config.onEvent) {
2697
+ try {
2698
+ this.config.onEvent(event);
2699
+ this.logger.debug("[CallKitCore] onEvent 回调执行成功:", event.type);
2700
+ } catch (err) {
2701
+ this.logger.error("[CallKitCore] onEvent 回调执行失败:", err);
2702
+ }
2703
+ }
2704
+ if (isUIEvent(event) && this.config.onUIEvent) {
2705
+ try {
2706
+ this.config.onUIEvent(event);
2707
+ this.logger.debug("[CallKitCore] onUIEvent 回调执行成功:", event.type);
2708
+ } catch (err) {
2709
+ this.logger.error("[CallKitCore] onUIEvent 回调执行失败:", err);
2710
+ }
2711
+ }
2712
+ if (isRtcEvent(event) && this.config.onRtcEvent) {
2713
+ try {
2714
+ this.config.onRtcEvent(event);
2715
+ } catch (err) {
2716
+ this.logger.error("[CallKitCore] onRtcEvent 回调执行失败:", err);
2717
+ }
2718
+ }
2719
+ }
2720
+ emitError(type, error, context) {
2721
+ const errMsg = error instanceof Error ? error.message : String(error);
2722
+ this.logger.error(`[CallKitCore] ${type}:`, error);
2723
+ this.emitEvent({
2724
+ type: "callError",
2725
+ payload: {
2726
+ type,
2727
+ error: errMsg,
2728
+ callId: context?.callId,
2729
+ context
2730
+ }
2731
+ });
2732
+ }
2733
+ startDurationTimer(callId, channel, callType, callerUserId) {
2734
+ if (this.durationTimer) {
2735
+ clearInterval(this.durationTimer);
2736
+ }
2737
+ this.durationStartTime = Date.now();
2738
+ this.durationCallInfo = { callId, channel, callType, callerUserId };
2739
+ this.durationTimer = setInterval(() => {
2740
+ if (!this.durationCallInfo) return;
2741
+ const duration = Date.now() - this.durationStartTime;
2742
+ this.emitEvent({
2743
+ type: "callDurationUpdated",
2744
+ payload: {
2745
+ callId: this.durationCallInfo.callId,
2746
+ channel: this.durationCallInfo.channel,
2747
+ callType: this.durationCallInfo.callType,
2748
+ callerUserId: this.durationCallInfo.callerUserId,
2749
+ duration
2750
+ }
2751
+ });
2752
+ }, 1e3);
2753
+ }
2754
+ stopDurationTimer() {
2755
+ if (this.durationTimer) {
2756
+ clearInterval(this.durationTimer);
2757
+ this.durationTimer = null;
2758
+ }
2759
+ this.durationStartTime = 0;
2760
+ this.durationCallInfo = null;
2761
+ }
2762
+ // ───────────────────────────────────────────────
2763
+ // 内部:IM 连接状态管理
2764
+ // ───────────────────────────────────────────────
2765
+ handleIMConnected() {
2766
+ this.logger.info("[CallKitCore] IM 已重新连接");
2767
+ if (!this.destroyed && this.imListener) {
2768
+ this.logger.info("[CallKitCore] IM 重连恢复:监听状态正常");
2769
+ }
2770
+ }
2771
+ handleIMDisconnected() {
2772
+ this.logger.warn("[CallKitCore] IM 已断开连接");
2773
+ }
2774
+ // ───────────────────────────────────────────────
2775
+ // 内部:超时定时器管理(Critical #1)
2776
+ // ───────────────────────────────────────────────
2777
+ /**
2778
+ * 启动邀请超时定时器。
2779
+ * 超时后调用状态机的 timeout() 并通过 processEvents 消费事件,
2780
+ * 确保上层 UI 能收到 callTimeout + callEnded 事件。
2781
+ */
2782
+ startInviteTimeout() {
2783
+ this.clearInviteTimeout();
2784
+ this.inviteTimer = setTimeout(() => {
2785
+ const result = this.singleCallState.timeout();
2786
+ if (result.ok) {
2787
+ this.processEvents(result.events, this.singleCallState.getState());
2788
+ }
2789
+ }, this.inviteTimeoutMs);
2790
+ }
2791
+ clearInviteTimeout() {
2792
+ if (this.inviteTimer) {
2793
+ clearTimeout(this.inviteTimer);
2794
+ this.inviteTimer = null;
2795
+ }
2796
+ }
2797
+ }
2798
+ const VERSION = "1.1.0";
2799
+ exports2.CALL_STATUS = CALL_STATUS;
2800
+ exports2.CALL_TYPE = CALL_TYPE;
2801
+ exports2.CallKitCore = CallKitCore;
2802
+ exports2.EventBus = EventBus;
2803
+ exports2.GroupCallSession = GroupCallSession;
2804
+ exports2.GroupCallSignalHandler = GroupCallSignalHandler;
2805
+ exports2.HANGUP_REASON = HANGUP_REASON;
2806
+ exports2.IMListener = IMListener;
2807
+ exports2.MessageBuilder = MessageBuilder;
2808
+ exports2.SignalRouter = SignalRouter;
2809
+ exports2.SignalSender = SignalSender;
2810
+ exports2.SingleCallSignalHandler = SingleCallSignalHandler;
2811
+ exports2.SingleCallStateMachine = SingleCallStateMachine;
2812
+ exports2.VERSION = VERSION;
2813
+ exports2.formatCallDuration = formatCallDuration;
2814
+ exports2.generateRandomChannel = generateRandomChannel;
2815
+ exports2.getLogger = getLogger;
2816
+ exports2.isCmdMessageExpired = isCmdMessageExpired;
2817
+ exports2.isMessageExpired = isMessageExpired;
2818
+ exports2.isRtcEvent = isRtcEvent;
2819
+ exports2.isUIEvent = isUIEvent;
2820
+ exports2.setLogger = setLogger;
2821
+ Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
2822
+ }));
2823
+ //# sourceMappingURL=index.umd.js.map