mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
feat(sidecar): supervisor barge endpoints — initiate, mode switch, end
Endpoints: - GET /api/supervisor/barge/sip-credentials — fetch SIP number from pool - POST /api/supervisor/barge — initiate barge via Ozonetel apiId 63 - POST /api/supervisor/barge/mode — update mode (listen/whisper/barge) - POST /api/supervisor/barge/end — cleanup session + Redis SupervisorService extended with barge session tracking (in-memory Map). Mode changes emit SSE events to agent: supervisor-whisper, supervisor-barge, supervisor-left. Listen mode is silent (no event to agent). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
163
src/supervisor/supervisor-barge.controller.ts
Normal file
163
src/supervisor/supervisor-barge.controller.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Controller, Post, Get, Body, HttpException, Logger } from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
import { OzonetelAdminAuthService } from '../ozonetel/ozonetel-admin-auth.service';
|
||||
import { SupervisorService } from './supervisor.service';
|
||||
import { TelephonyConfigService } from '../config/telephony-config.service';
|
||||
|
||||
// Supervisor barge/whisper/listen endpoints.
|
||||
// Proxies requests to Ozonetel's dashboardApi using admin JWT auth.
|
||||
//
|
||||
// API reference (from CA-Admin source code):
|
||||
// apiId 63 → CALL_BARGEIN (initiate barge)
|
||||
// apiId 158 → Redis barge state (insert/delete)
|
||||
// apiId 139 → SIP credential pool (sipSubscribe)
|
||||
|
||||
@Controller('api/supervisor/barge')
|
||||
export class SupervisorBargeController {
|
||||
private readonly logger = new Logger(SupervisorBargeController.name);
|
||||
private readonly dashboardApiUrl = 'https://api.cloudagent.ozonetel.com/dashboardApi/monitor/api';
|
||||
private readonly adminApiUrl = 'https://api.cloudagent.ozonetel.com/ca-admin-Api/CloudAgentAPI';
|
||||
|
||||
constructor(
|
||||
private readonly adminAuth: OzonetelAdminAuthService,
|
||||
private readonly supervisor: SupervisorService,
|
||||
private readonly telephony: TelephonyConfigService,
|
||||
) {}
|
||||
|
||||
@Get('sip-credentials')
|
||||
async getSipCredentials() {
|
||||
if (!this.adminAuth.isConfigured()) {
|
||||
throw new HttpException('Ozonetel admin not configured — add credentials in Settings → Telephony', 503);
|
||||
}
|
||||
|
||||
const config = this.telephony.getConfig();
|
||||
const sipGateway = `${config.sip.domain}:${config.sip.wsPort}`;
|
||||
const headers = await this.adminAuth.getAuthHeaders();
|
||||
|
||||
try {
|
||||
const res = await axios.post(`${this.adminApiUrl}/endpoint/sipnumber/sipSubscribe`, {
|
||||
apiId: 139,
|
||||
sipURL: sipGateway,
|
||||
}, { headers });
|
||||
|
||||
const data = res.data;
|
||||
this.logger.log(`[BARGE] SIP credentials response: ${JSON.stringify(data)}`);
|
||||
|
||||
if (!data?.sip_number) {
|
||||
throw new HttpException('No SIP numbers available in pool', 503);
|
||||
}
|
||||
|
||||
return {
|
||||
sipNumber: data.sip_number,
|
||||
sipPassword: data.password,
|
||||
sipDomain: data.pop_location ?? config.sip.domain,
|
||||
sipPort: config.sip.wsPort,
|
||||
};
|
||||
} catch (err: any) {
|
||||
this.logger.error(`[BARGE] SIP credentials failed: ${err.message}`);
|
||||
if (err instanceof HttpException) throw err;
|
||||
throw new HttpException('Failed to fetch SIP credentials', 502);
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
async initiateBarge(@Body() body: { ucid: string; agentId: string; agentNumber: string; supervisorId?: string }) {
|
||||
if (!body.ucid || !body.agentNumber) {
|
||||
throw new HttpException('ucid and agentNumber required', 400);
|
||||
}
|
||||
if (!this.adminAuth.isConfigured()) {
|
||||
throw new HttpException('Ozonetel admin not configured — add credentials in Settings → Telephony', 503);
|
||||
}
|
||||
|
||||
// Prevent double-barge on same agent
|
||||
const existing = this.supervisor.getBargeSession(body.agentId);
|
||||
if (existing) {
|
||||
throw new HttpException(`Agent ${body.agentId} is already being monitored`, 409);
|
||||
}
|
||||
|
||||
// Get SIP credentials from Ozonetel pool
|
||||
const sipCreds = await this.getSipCredentials();
|
||||
const headers = await this.adminAuth.getAuthHeaders();
|
||||
|
||||
try {
|
||||
const res = await axios.post(this.dashboardApiUrl, {
|
||||
apiId: 63,
|
||||
ucid: body.ucid,
|
||||
action: 'CALL_BARGEIN',
|
||||
isSip: true,
|
||||
phoneno: sipCreds.sipNumber,
|
||||
agentNumber: body.agentNumber,
|
||||
cbURL: 'helix-engage',
|
||||
}, { headers });
|
||||
|
||||
this.logger.log(`[BARGE] Initiated: ucid=${body.ucid} agent=${body.agentId} sip=${sipCreds.sipNumber} response=${JSON.stringify(res.data)}`);
|
||||
|
||||
// Track the session
|
||||
this.supervisor.startBargeSession({
|
||||
supervisorId: body.supervisorId ?? 'admin',
|
||||
agentId: body.agentId,
|
||||
sipNumber: sipCreds.sipNumber,
|
||||
mode: 'listen',
|
||||
startedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
...sipCreds,
|
||||
ozonetelResponse: res.data,
|
||||
};
|
||||
} catch (err: any) {
|
||||
this.logger.error(`[BARGE] Initiation failed: ${err.message} ${err.response?.data ? JSON.stringify(err.response.data) : ''}`);
|
||||
throw new HttpException(`Barge failed: ${err.response?.data?.Message ?? err.message}`, 502);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('mode')
|
||||
async updateMode(@Body() body: { agentId: string; mode: 'listen' | 'whisper' | 'barge' }) {
|
||||
if (!body.agentId || !body.mode) {
|
||||
throw new HttpException('agentId and mode required', 400);
|
||||
}
|
||||
if (!['listen', 'whisper', 'barge'].includes(body.mode)) {
|
||||
throw new HttpException('mode must be listen, whisper, or barge', 400);
|
||||
}
|
||||
|
||||
const session = this.supervisor.getBargeSession(body.agentId);
|
||||
if (!session) {
|
||||
throw new HttpException('No active barge session for this agent', 404);
|
||||
}
|
||||
|
||||
this.supervisor.updateBargeMode(body.agentId, body.mode);
|
||||
return { status: 'ok', mode: body.mode };
|
||||
}
|
||||
|
||||
@Post('end')
|
||||
async endBarge(@Body() body: { agentId: string }) {
|
||||
if (!body.agentId) {
|
||||
throw new HttpException('agentId required', 400);
|
||||
}
|
||||
|
||||
const session = this.supervisor.getBargeSession(body.agentId);
|
||||
if (!session) {
|
||||
return { status: 'ok', message: 'No active session' };
|
||||
}
|
||||
|
||||
// Clear Redis tracking on Ozonetel side (best-effort)
|
||||
if (this.adminAuth.isConfigured()) {
|
||||
try {
|
||||
const headers = await this.adminAuth.getAuthHeaders();
|
||||
await axios.post(this.dashboardApiUrl, {
|
||||
apiId: 158,
|
||||
Action: 'delete',
|
||||
AgentId: body.agentId,
|
||||
Sip: session.sipNumber,
|
||||
}, { headers });
|
||||
this.logger.log(`[BARGE] Redis cleanup: ${body.agentId}`);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`[BARGE] Redis cleanup failed (non-critical): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.supervisor.endBargeSession(body.agentId);
|
||||
return { status: 'ok' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user