Files
helix-engage-server/src/supervisor/supervisor-barge.controller.ts
saridsa2 ea60787da0 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>
2026-04-12 16:06:57 +05:30

164 lines
6.4 KiB
TypeScript

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' };
}
}