mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
feat: telephony dispatcher registration — sidecar self-registers on boot
Adds TelephonyRegistrationService that: 1. On startup: queries platform for agent list, registers with the telephony dispatcher at TELEPHONY_DISPATCHER_URL 2. Every 30s: sends heartbeat to keep registration alive (90s TTL) 3. On shutdown: deregisters (best-effort, TTL cleans up anyway) 4. On heartbeat failure: auto re-registers Env vars: TELEPHONY_DISPATCHER_URL — where to register (outbound to dispatcher) TELEPHONY_CALLBACK_URL — where events come back (inbound to sidecar) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import { RulesEngineModule } from './rules-engine/rules-engine.module';
|
|||||||
import { ConfigThemeModule } from './config/config-theme.module';
|
import { ConfigThemeModule } from './config/config-theme.module';
|
||||||
import { WidgetModule } from './widget/widget.module';
|
import { WidgetModule } from './widget/widget.module';
|
||||||
import { TeamModule } from './team/team.module';
|
import { TeamModule } from './team/team.module';
|
||||||
|
import { TelephonyRegistrationService } from './telephony-registration.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -49,5 +50,6 @@ import { TeamModule } from './team/team.module';
|
|||||||
WidgetModule,
|
WidgetModule,
|
||||||
TeamModule,
|
TeamModule,
|
||||||
],
|
],
|
||||||
|
providers: [TelephonyRegistrationService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
114
src/telephony-registration.service.ts
Normal file
114
src/telephony-registration.service.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { PlatformGraphqlService } from './platform/platform-graphql.service';
|
||||||
|
|
||||||
|
// On startup, registers this sidecar with the telephony dispatcher
|
||||||
|
// so Ozonetel events are routed to the correct sidecar by agentId.
|
||||||
|
//
|
||||||
|
// Flow:
|
||||||
|
// 1. Load agent list from platform (Agent entities in this workspace)
|
||||||
|
// 2. POST /api/supervisor/register to the dispatcher
|
||||||
|
// 3. Start heartbeat interval (every 30s)
|
||||||
|
// 4. On shutdown, DELETE /api/supervisor/register
|
||||||
|
|
||||||
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TelephonyRegistrationService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(TelephonyRegistrationService.name);
|
||||||
|
private heartbeatTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private config: ConfigService,
|
||||||
|
private platform: PlatformGraphqlService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private get dispatcherUrl(): string {
|
||||||
|
return this.config.get<string>('TELEPHONY_DISPATCHER_URL') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private get sidecarUrl(): string {
|
||||||
|
return this.config.get<string>('TELEPHONY_CALLBACK_URL') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private get workspace(): string {
|
||||||
|
return process.env.PLATFORM_WORKSPACE_SUBDOMAIN ?? 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
if (!this.dispatcherUrl || !this.sidecarUrl) {
|
||||||
|
this.logger.warn('TELEPHONY_DISPATCHER_URL or TELEPHONY_CALLBACK_URL not set — skipping telephony registration');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.register();
|
||||||
|
|
||||||
|
this.heartbeatTimer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await axios.post(`${this.dispatcherUrl}/api/supervisor/heartbeat`, {
|
||||||
|
sidecarUrl: this.sidecarUrl,
|
||||||
|
}, { timeout: 5000 });
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`Heartbeat failed: ${err.message} — attempting re-registration`);
|
||||||
|
await this.register();
|
||||||
|
}
|
||||||
|
}, HEARTBEAT_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
||||||
|
|
||||||
|
if (this.dispatcherUrl && this.sidecarUrl) {
|
||||||
|
try {
|
||||||
|
await axios.delete(`${this.dispatcherUrl}/api/supervisor/register`, {
|
||||||
|
data: { sidecarUrl: this.sidecarUrl },
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
this.logger.log('Deregistered from telephony dispatcher');
|
||||||
|
} catch {
|
||||||
|
// Best-effort — TTL will clean up anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async register() {
|
||||||
|
try {
|
||||||
|
const agents = await this.loadAgentIds();
|
||||||
|
if (agents.length === 0) {
|
||||||
|
this.logger.warn('No agents found in workspace — skipping registration');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(`${this.dispatcherUrl}/api/supervisor/register`, {
|
||||||
|
sidecarUrl: this.sidecarUrl,
|
||||||
|
workspace: this.workspace,
|
||||||
|
agents,
|
||||||
|
}, { timeout: 5000 });
|
||||||
|
|
||||||
|
this.logger.log(`Registered with telephony dispatcher: ${agents.length} agents (${agents.join(', ')})`);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.error(`Registration failed: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadAgentIds(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const apiKey = this.config.get<string>('PLATFORM_API_KEY');
|
||||||
|
if (!apiKey) return [];
|
||||||
|
|
||||||
|
const data = await this.platform.queryWithAuth<any>(
|
||||||
|
`{ agents(first: 50) { edges { node { ozonetelAgentId } } } }`,
|
||||||
|
undefined,
|
||||||
|
`Bearer ${apiKey}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (data.agents?.edges ?? [])
|
||||||
|
.map((e: any) => e.node.ozonetelAgentId)
|
||||||
|
.filter((id: string) => id && id !== 'PENDING');
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`Failed to load agents from platform: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user