feat: track UCID from SIP headers for Ozonetel disposition

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 18:33:46 +05:30
parent e7c5c13e83
commit d6ef2b70d8
4 changed files with 42 additions and 15 deletions

View File

@@ -11,7 +11,7 @@ export class SIPClient {
constructor(
private config: SIPConfig,
private onConnectionChange: (status: ConnectionStatus) => void,
private onCallStateChange: (state: CallState, callerNumber?: string) => void,
private onCallStateChange: (state: CallState, callerNumber?: string, ucid?: string) => void,
) {}
connect(): void {
@@ -66,8 +66,10 @@ export class SIPClient {
this.currentSession = session;
// Extract caller number — try multiple SIP headers
const callerNumber = this.extractCallerNumber(session);
// Extract caller number and UCID — try event request first, then session
const sipRequest = (data as any).request ?? (session as any)._request ?? null;
const callerNumber = this.extractCallerNumber(session, sipRequest);
const ucid = sipRequest?.getHeader ? sipRequest.getHeader('X-UCID') ?? null : null;
// Setup audio for this session
session.on('peerconnection', (e: PeerConnectionEvent) => {
@@ -83,16 +85,16 @@ export class SIPClient {
});
session.on('accepted', (() => {
this.onCallStateChange('active', callerNumber);
this.onCallStateChange('active', callerNumber, ucid ?? undefined);
}) as CallListener);
session.on('confirmed', () => {
this.onCallStateChange('active', callerNumber);
this.onCallStateChange('active', callerNumber, ucid ?? undefined);
});
session.on('progress', (() => {
if (session.direction === 'outgoing') {
this.onCallStateChange('ringing-out', callerNumber);
this.onCallStateChange('ringing-out', callerNumber, ucid ?? undefined);
}
}) as CallListener);
@@ -107,7 +109,7 @@ export class SIPClient {
});
if (session.direction === 'incoming') {
this.onCallStateChange('ringing-in', callerNumber);
this.onCallStateChange('ringing-in', callerNumber, ucid ?? undefined);
}
});
@@ -222,12 +224,14 @@ export class SIPClient {
}
}
private extractCallerNumber(session: RTCSession): string {
private extractCallerNumber(session: RTCSession, sipRequest?: any): string {
try {
const request = session.direction === 'incoming' ? (session as any)._request : null;
const request = sipRequest ?? (session.direction === 'incoming' ? (session as any)._request : null);
console.log('[SIP] extractCallerNumber: request exists:', !!request, 'direction:', session.direction);
if (request) {
// Ozonetel sends the real caller number in X-CALLERNO header
const xCallerNo = request.getHeader('X-CALLERNO');
const xCallerNo = request.getHeader ? request.getHeader('X-CALLERNO') : null;
console.log('[SIP] X-CALLERNO header:', xCallerNo);
if (xCallerNo) {
// Remove leading 0s or country code prefix (00919... → 919...)
const cleaned = xCallerNo.replace(/^0+/, '');

View File

@@ -8,6 +8,7 @@ import {
sipIsOnHoldAtom,
sipCallDurationAtom,
sipCallStartTimeAtom,
sipCallUcidAtom,
} from '@/state/sip-state';
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager';
import type { SIPConfig } from '@/types/sip';
@@ -24,6 +25,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom);
const [callState, setCallState] = useAtom(sipCallStateAtom);
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
const setCallUcid = useSetAtom(sipCallUcidAtom);
const setCallDuration = useSetAtom(sipCallDurationAtom);
const setCallStartTime = useSetAtom(sipCallStartTimeAtom);
@@ -33,8 +35,9 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
setConnectionStatus,
setCallState,
setCallerNumber,
setCallUcid,
});
}, [setConnectionStatus, setCallState, setCallerNumber]);
}, [setConnectionStatus, setCallState, setCallerNumber, setCallUcid]);
// Auto-connect SIP on mount
useEffect(() => {
@@ -82,6 +85,7 @@ export const useSip = () => {
const [connectionStatus] = useAtom(sipConnectionStatusAtom);
const [callState] = useAtom(sipCallStateAtom);
const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom);
const [callUcid] = useAtom(sipCallUcidAtom);
const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom);
const [isOnHold, setIsOnHold] = useAtom(sipIsOnHoldAtom);
const [callDuration] = useAtom(sipCallDurationAtom);
@@ -117,6 +121,7 @@ export const useSip = () => {
connectionStatus,
callState,
callerNumber,
callUcid,
isMuted,
isOnHold,
callDuration,

View File

@@ -5,11 +5,13 @@ import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip';
let sipClient: SIPClient | null = null;
let connected = false;
let outboundPending = false;
let outboundActive = false;
type StateUpdater = {
setConnectionStatus: (status: ConnectionStatus) => void;
setCallState: (state: CallState) => void;
setCallerNumber: (number: string | null) => void;
setCallUcid: (ucid: string | null) => void;
};
let stateUpdater: StateUpdater | null = null;
@@ -46,21 +48,34 @@ export function connectSip(config: SIPConfig): void {
sipClient = new SIPClient(
config,
(status) => stateUpdater?.setConnectionStatus(status),
(state, number) => {
// Auto-answer SIP when it's a bridge from our outbound Kookoo call
(state, number, ucid) => {
// Auto-answer SIP when it's a bridge from our outbound call
if (state === 'ringing-in' && outboundPending) {
outboundPending = false;
outboundActive = true;
console.log('[SIP] Outbound bridge detected — auto-answering');
setTimeout(() => {
sipClient?.answer();
// Force active state in case SIP callbacks don't fire
setTimeout(() => stateUpdater?.setCallState('active'), 300);
}, 500);
// Store UCID even for outbound bridge calls
if (ucid) stateUpdater?.setCallUcid(ucid);
return;
}
// Don't overwrite caller number on outbound calls — it was set by click-to-call
stateUpdater?.setCallState(state);
if (number !== undefined) stateUpdater?.setCallerNumber(number ?? null);
if (!outboundActive && number !== undefined) {
stateUpdater?.setCallerNumber(number ?? null);
}
// Store UCID if provided
if (ucid) stateUpdater?.setCallUcid(ucid);
// Reset outbound flag when call ends
if (state === 'ended' || state === 'failed') {
outboundActive = false;
}
},
);
@@ -72,7 +87,9 @@ export function disconnectSip(): void {
sipClient = null;
connected = false;
outboundPending = false;
outboundActive = false;
stateUpdater?.setConnectionStatus('disconnected');
stateUpdater?.setCallUcid(null);
}
export function getSipClient(): SIPClient | null {

View File

@@ -8,3 +8,4 @@ export const sipIsMutedAtom = atom<boolean>(false);
export const sipIsOnHoldAtom = atom<boolean>(false);
export const sipCallDurationAtom = atom<number>(0);
export const sipCallStartTimeAtom = atom<Date | null>(null);
export const sipCallUcidAtom = atom<string | null>(null);