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