feat(routing): add rule-based SIP routing for inbound and outbound calls with dashboard route management

This commit is contained in:
2026-04-10 08:22:12 +00:00
parent f3e1c96872
commit fd3a408cc2
13 changed files with 893 additions and 114 deletions

View File

@@ -25,7 +25,7 @@ import {
} from '../sip/index.ts';
import type { IEndpoint } from '../sip/index.ts';
import type { IAppConfig, IProviderConfig } from '../config.ts';
import { getProvider, getProviderForOutbound, getDevicesForInbound } from '../config.ts';
import { getProvider, getDevice, resolveOutboundRoute, resolveInboundRoute } from '../config.ts';
import { RtpPortPool } from './rtp-port-pool.ts';
import { Call } from './call.ts';
import { SipLeg } from './sip-leg.ts';
@@ -211,10 +211,27 @@ export class CallManager {
* Dials the device first (leg A), then the provider (leg B) when device answers.
*/
createOutboundCall(number: string, deviceId?: string, providerId?: string): Call | null {
// Resolve provider.
const provider = providerId
? getProvider(this.config.appConfig, providerId)
: getProviderForOutbound(this.config.appConfig);
// Resolve provider via routing (or explicit providerId override).
let provider: IProviderConfig | null;
let dialNumber = number;
if (providerId) {
provider = getProvider(this.config.appConfig, providerId);
} else {
const routeResult = resolveOutboundRoute(
this.config.appConfig,
number,
deviceId,
(pid) => !!this.config.getProviderState(pid)?.registeredAor,
);
if (routeResult) {
provider = routeResult.provider;
dialNumber = routeResult.transformedNumber;
} else {
provider = null;
}
}
if (!provider) {
this.config.log('[call-mgr] no provider found');
return null;
@@ -259,7 +276,7 @@ export class CallManager {
// then webrtc-accept (which links the leg to this call and starts the provider).
this.pendingBrowserCalls.set(callId, {
provider,
number,
number: dialNumber,
ps,
rtpPort: rtpA.port,
rtpSock: rtpA.sock,
@@ -312,7 +329,7 @@ export class CallManager {
}
// Start dialing provider in parallel with announcement.
this.startProviderLeg(call, provider, number, ps);
this.startProviderLeg(call, provider, dialNumber, ps);
};
legA.onTerminated = (leg) => {
@@ -505,9 +522,13 @@ export class CallManager {
return null;
}
// Resolve target device.
const deviceConfigs = getDevicesForInbound(this.config.appConfig, provider.id);
const deviceTarget = this.resolveFirstDevice(deviceConfigs.map((d) => d.id));
// Resolve inbound routing — determine target devices and browser ring.
const calledNumber = SipMessage.extractUri(invite.requestUri || '') || '';
const routeResult = resolveInboundRoute(this.config.appConfig, provider.id, calledNumber, call.callerNumber);
const targetDeviceIds = routeResult.deviceIds.length
? routeResult.deviceIds
: this.config.appConfig.devices.map((d) => d.id);
const deviceTarget = this.resolveFirstDevice(targetDeviceIds);
if (!deviceTarget) {
this.config.log('[call-mgr] cannot handle inbound — no device target');
this.portPool.release(rtpAlloc.port);
@@ -563,8 +584,8 @@ export class CallManager {
this.config.sendSip(fwdInvite.serialize(), deviceTarget);
// Notify browsers if configured.
if (this.config.appConfig.routing.ringBrowsers?.[provider.id]) {
// Notify browsers if route says so.
if (routeResult.ringBrowsers) {
const ids = this.config.getAllBrowserDeviceIds();
for (const deviceIdBrowser of ids) {
this.config.sendToBrowserDevice(deviceIdBrowser, {
@@ -868,10 +889,27 @@ export class CallManager {
const call = this.calls.get(callId);
if (!call) return false;
// Resolve provider.
const provider = providerId
? getProvider(this.config.appConfig, providerId)
: getProviderForOutbound(this.config.appConfig);
// Resolve provider via routing (or explicit providerId override).
let provider: IProviderConfig | null;
let dialNumber = number;
if (providerId) {
provider = getProvider(this.config.appConfig, providerId);
} else {
const routeResult = resolveOutboundRoute(
this.config.appConfig,
number,
undefined,
(pid) => !!this.config.getProviderState(pid)?.registeredAor,
);
if (routeResult) {
provider = routeResult.provider;
dialNumber = routeResult.transformedNumber;
} else {
provider = null;
}
}
if (!provider) {
this.config.log(`[call-mgr] addExternalToCall: no provider`);
return false;
@@ -915,7 +953,7 @@ export class CallManager {
call.addLeg(newLeg);
const sipCallId = `${callId}-${legId}`;
const destUri = `sip:${number}@${provider.domain}`;
const destUri = `sip:${dialNumber}@${provider.domain}`;
newLeg.sendInvite({
fromUri: ps.registeredAor,
toUri: destUri,
@@ -923,7 +961,7 @@ export class CallManager {
});
this.sipCallIdIndex.set(sipCallId, call);
this.config.log(`[call-mgr] ${callId} dialing external ${number} via ${provider.displayName}`);
this.config.log(`[call-mgr] ${callId} dialing external ${dialNumber} via ${provider.displayName}`);
return true;
}