feat(call, voicemail, ivr): add voicemail and IVR call flows with DTMF handling, prompt playback, recording, and dashboard management

This commit is contained in:
2026-04-10 08:54:46 +00:00
parent 6ecd3f434c
commit e6bd64a534
25 changed files with 3892 additions and 10 deletions

View File

@@ -78,6 +78,17 @@ export interface ISipRouteAction {
/** Also ring connected browser clients. Default false. */
ringBrowsers?: boolean;
// --- Inbound actions (IVR / voicemail) ---
/** Route directly to a voicemail box (skip ringing devices). */
voicemailBox?: string;
/** Route to an IVR menu by menu ID (skip ringing devices). */
ivrMenuId?: string;
/** Override no-answer timeout (seconds) before routing to voicemail. */
noAnswerTimeout?: number;
// --- Outbound actions (provider selection) ---
/** Provider ID to use for outbound. */
@@ -137,12 +148,95 @@ export interface IContact {
starred?: boolean;
}
// ---------------------------------------------------------------------------
// Voicebox configuration
// ---------------------------------------------------------------------------
export interface IVoiceboxConfig {
/** Unique ID — typically matches device ID or extension. */
id: string;
/** Whether this voicebox is active. */
enabled: boolean;
/** Custom TTS greeting text. */
greetingText?: string;
/** TTS voice ID (default 'af_bella'). */
greetingVoice?: string;
/** Path to uploaded WAV greeting (overrides TTS). */
greetingWavPath?: string;
/** Seconds to wait before routing to voicemail (default 25). */
noAnswerTimeoutSec?: number;
/** Maximum recording duration in seconds (default 120). */
maxRecordingSec?: number;
/** Maximum stored messages per box (default 50). */
maxMessages?: number;
}
// ---------------------------------------------------------------------------
// IVR configuration
// ---------------------------------------------------------------------------
/** An action triggered by a digit press in an IVR menu. */
export type TIvrAction =
| { type: 'route-extension'; extensionId: string }
| { type: 'route-voicemail'; boxId: string }
| { type: 'submenu'; menuId: string }
| { type: 'play-message'; promptId: string }
| { type: 'transfer'; number: string; providerId?: string }
| { type: 'repeat' }
| { type: 'hangup' };
/** A single digit→action mapping in an IVR menu. */
export interface IIvrMenuEntry {
/** Digit: '0'-'9', '*', '#'. */
digit: string;
/** Action to take when this digit is pressed. */
action: TIvrAction;
}
/** An IVR menu with a prompt and digit mappings. */
export interface IIvrMenu {
/** Unique menu ID. */
id: string;
/** Human-readable name. */
name: string;
/** TTS text for the menu prompt. */
promptText: string;
/** TTS voice ID for the prompt. */
promptVoice?: string;
/** Digit→action entries. */
entries: IIvrMenuEntry[];
/** Seconds to wait for a digit after prompt finishes (default 5). */
timeoutSec?: number;
/** Maximum retries before executing timeout action (default 3). */
maxRetries?: number;
/** Action on timeout (no digit pressed). */
timeoutAction: TIvrAction;
/** Action on invalid digit. */
invalidAction: TIvrAction;
}
/** Top-level IVR configuration. */
export interface IIvrConfig {
/** Whether the IVR system is active. */
enabled: boolean;
/** IVR menu definitions. */
menus: IIvrMenu[];
/** The menu to start with for incoming calls. */
entryMenuId: string;
}
// ---------------------------------------------------------------------------
// App config
// ---------------------------------------------------------------------------
export interface IAppConfig {
proxy: IProxyConfig;
providers: IProviderConfig[];
devices: IDeviceConfig[];
routing: IRoutingConfig;
contacts: IContact[];
voiceboxes?: IVoiceboxConfig[];
ivr?: IIvrConfig;
}
// ---------------------------------------------------------------------------
@@ -201,6 +295,27 @@ export function loadConfig(): IAppConfig {
c.starred ??= false;
}
// Voicebox defaults.
cfg.voiceboxes ??= [];
for (const vb of cfg.voiceboxes) {
vb.enabled ??= true;
vb.noAnswerTimeoutSec ??= 25;
vb.maxRecordingSec ??= 120;
vb.maxMessages ??= 50;
vb.greetingVoice ??= 'af_bella';
}
// IVR defaults.
if (cfg.ivr) {
cfg.ivr.enabled ??= false;
cfg.ivr.menus ??= [];
for (const menu of cfg.ivr.menus) {
menu.timeoutSec ??= 5;
menu.maxRetries ??= 3;
menu.entries ??= [];
}
}
return cfg;
}
@@ -251,6 +366,12 @@ export interface IInboundRouteResult {
/** Device IDs to ring (empty = all devices). */
deviceIds: string[];
ringBrowsers: boolean;
/** If set, route directly to this voicemail box (skip ringing). */
voicemailBox?: string;
/** If set, route to this IVR menu (skip ringing). */
ivrMenuId?: string;
/** Override for no-answer timeout in seconds. */
noAnswerTimeout?: number;
}
/**
@@ -332,6 +453,9 @@ export function resolveInboundRoute(
return {
deviceIds: route.action.targets || [],
ringBrowsers: route.action.ringBrowsers ?? false,
voicemailBox: route.action.voicemailBox,
ivrMenuId: route.action.ivrMenuId,
noAnswerTimeout: route.action.noAnswerTimeout,
};
}