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