Add Gateway WebSocket integration and CI/CD pipeline
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
043349f529
commit
dced747666
6 changed files with 452 additions and 67 deletions
69
.github/workflows/build-deploy.yml
vendored
Normal file
69
.github/workflows/build-deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
AZURE_CONTAINER_APP_NAME: teams-browser-bot
|
||||
AZURE_RESOURCE_GROUP: rg-poweron-int
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
outputs:
|
||||
image_tag: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,prefix=
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- name: Azure Login
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: Deploy to Azure Container Apps
|
||||
uses: azure/container-apps-deploy-action@v1
|
||||
with:
|
||||
resourceGroup: ${{ env.AZURE_RESOURCE_GROUP }}
|
||||
containerAppName: ${{ env.AZURE_CONTAINER_APP_NAME }}
|
||||
imageToDeploy: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
200
AZURE_SETUP.md
Normal file
200
AZURE_SETUP.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# Azure Setup für Teams Browser Bot
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
1. Azure CLI installiert und eingeloggt
|
||||
2. GitHub Repository erstellt unter `valueonag/service-teams-browser-bot`
|
||||
|
||||
---
|
||||
|
||||
## 1. Azure Container App erstellen
|
||||
|
||||
### 1.1 Resource Group (falls nicht vorhanden)
|
||||
|
||||
```bash
|
||||
az group create \
|
||||
--name rg-poweron-int \
|
||||
--location westeurope
|
||||
```
|
||||
|
||||
### 1.2 Container Apps Environment
|
||||
|
||||
```bash
|
||||
az containerapp env create \
|
||||
--name cae-poweron-int \
|
||||
--resource-group rg-poweron-int \
|
||||
--location westeurope
|
||||
```
|
||||
|
||||
### 1.3 Container App erstellen
|
||||
|
||||
```bash
|
||||
az containerapp create \
|
||||
--name teams-browser-bot \
|
||||
--resource-group rg-poweron-int \
|
||||
--environment cae-poweron-int \
|
||||
--image ghcr.io/valueonag/service-teams-browser-bot:latest \
|
||||
--target-port 4100 \
|
||||
--ingress external \
|
||||
--cpu 2 \
|
||||
--memory 4Gi \
|
||||
--min-replicas 0 \
|
||||
--max-replicas 3 \
|
||||
--env-vars \
|
||||
NODE_ENV=production \
|
||||
PORT=4100 \
|
||||
GATEWAY_WS_URL=wss://gateway-int.poweron-center.net/api/teamsbot/bot/ws \
|
||||
BOT_NAME="PowerOn AI" \
|
||||
BOT_HEADLESS=true \
|
||||
LOG_LEVEL=info \
|
||||
SCREENSHOT_ON_ERROR=true
|
||||
```
|
||||
|
||||
### 1.4 Container App URL notieren
|
||||
|
||||
```bash
|
||||
az containerapp show \
|
||||
--name teams-browser-bot \
|
||||
--resource-group rg-poweron-int \
|
||||
--query properties.configuration.ingress.fqdn \
|
||||
--output tsv
|
||||
```
|
||||
|
||||
Ergebnis z.B.: `teams-browser-bot.happysky-12345.westeurope.azurecontainerapps.io`
|
||||
|
||||
---
|
||||
|
||||
## 2. GitHub Actions Setup
|
||||
|
||||
### 2.1 Azure Service Principal erstellen
|
||||
|
||||
```bash
|
||||
az ad sp create-for-rbac \
|
||||
--name "github-teams-browser-bot" \
|
||||
--role contributor \
|
||||
--scopes /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/rg-poweron-int \
|
||||
--sdk-auth
|
||||
```
|
||||
|
||||
### 2.2 GitHub Secret hinzufügen
|
||||
|
||||
1. GitHub Repo → Settings → Secrets and variables → Actions
|
||||
2. New repository secret: `AZURE_CREDENTIALS`
|
||||
3. Wert: JSON Output vom vorherigen Befehl
|
||||
|
||||
### 2.3 GitHub Container Registry Zugriff
|
||||
|
||||
Der Workflow verwendet `GITHUB_TOKEN` automatisch für ghcr.io.
|
||||
|
||||
Falls Azure die Images nicht pullen kann:
|
||||
|
||||
```bash
|
||||
# PAT mit read:packages Scope erstellen auf GitHub
|
||||
# Dann in Azure:
|
||||
az containerapp registry set \
|
||||
--name teams-browser-bot \
|
||||
--resource-group rg-poweron-int \
|
||||
--server ghcr.io \
|
||||
--username <GITHUB_USERNAME> \
|
||||
--password <GITHUB_PAT>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Gateway Konfiguration
|
||||
|
||||
### 3.1 Environment Variable im Gateway
|
||||
|
||||
In `env_int.env` (oder Azure App Service Configuration):
|
||||
|
||||
```
|
||||
TEAMSBOT_BROWSER_BOT_URL=https://teams-browser-bot.happysky-12345.westeurope.azurecontainerapps.io
|
||||
```
|
||||
|
||||
### 3.2 Gateway neu deployen
|
||||
|
||||
```bash
|
||||
# Push to int branch triggers deployment
|
||||
git push origin int
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. DNS (Optional)
|
||||
|
||||
Falls du eine eigene Domain verwenden möchtest:
|
||||
|
||||
```bash
|
||||
az containerapp hostname add \
|
||||
--name teams-browser-bot \
|
||||
--resource-group rg-poweron-int \
|
||||
--hostname bot.poweron.swiss
|
||||
|
||||
# Dann DNS A-Record oder CNAME auf die Container App zeigen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Monitoring
|
||||
|
||||
### Logs anzeigen
|
||||
|
||||
```bash
|
||||
az containerapp logs show \
|
||||
--name teams-browser-bot \
|
||||
--resource-group rg-poweron-int \
|
||||
--follow
|
||||
```
|
||||
|
||||
### Metriken
|
||||
|
||||
```bash
|
||||
az containerapp show \
|
||||
--name teams-browser-bot \
|
||||
--resource-group rg-poweron-int \
|
||||
--query properties.latestRevisionFqdn
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Kosten
|
||||
|
||||
Azure Container Apps (Consumption Plan):
|
||||
- **vCPU**: ~$0.000024/vCPU-second
|
||||
- **Memory**: ~$0.000003/GiB-second
|
||||
- **Requests**: Erste 2M/Monat kostenlos
|
||||
|
||||
Geschätzte Kosten bei 10h Bot-Nutzung/Tag:
|
||||
- ~$15-25/Monat (deutlich günstiger als die alte VM!)
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting
|
||||
|
||||
### Container startet nicht
|
||||
|
||||
```bash
|
||||
# Logs prüfen
|
||||
az containerapp logs show \
|
||||
--name teams-browser-bot \
|
||||
--resource-group rg-poweron-int \
|
||||
--type system
|
||||
|
||||
# Revision Status
|
||||
az containerapp revision list \
|
||||
--name teams-browser-bot \
|
||||
--resource-group rg-poweron-int \
|
||||
--output table
|
||||
```
|
||||
|
||||
### Playwright/Chrome Probleme
|
||||
|
||||
Container Apps unterstützen keine GPU. Falls Chrome-Probleme:
|
||||
1. Sicherstellen dass `BOT_HEADLESS=true`
|
||||
2. Shared memory erhöhen (im Dockerfile bereits konfiguriert)
|
||||
|
||||
### WebSocket Verbindung fehlschlägt
|
||||
|
||||
1. Prüfen ob Gateway CORS erlaubt
|
||||
2. Prüfen ob Container App WebSockets unterstützt (Standard: ja)
|
||||
3. Gateway Logs prüfen
|
||||
|
|
@ -3,10 +3,11 @@ import { Logger } from 'winston';
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
import { config } from '../config';
|
||||
import { createSessionLogger } from '../utils/logger';
|
||||
import { BotSession, BotState, TranscriptEntry } from '../types';
|
||||
import { BotSession, BotState, TranscriptEntry, StatusMessage, TranscriptMessage, PlayAudioMessage } from '../types';
|
||||
import { JoinProcedure } from './joinProcedure';
|
||||
import { CaptionsProcedure } from './captionsProcedure';
|
||||
import { AudioProcedure } from './audioProcedure';
|
||||
|
|
@ -18,12 +19,19 @@ export interface OrchestratorCallbacks {
|
|||
onError: (error: Error) => void;
|
||||
}
|
||||
|
||||
export interface OrchestratorOptions {
|
||||
gatewayWsUrl: string;
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orchestrates the entire bot lifecycle:
|
||||
* - Connects to Gateway via WebSocket
|
||||
* - Launches browser
|
||||
* - Joins meeting
|
||||
* - Enables captions
|
||||
* - Handles audio playback
|
||||
* - Sends transcripts to Gateway
|
||||
* - Handles audio playback from Gateway
|
||||
* - Leaves meeting
|
||||
*/
|
||||
export class BotOrchestrator {
|
||||
|
|
@ -32,10 +40,12 @@ export class BotOrchestrator {
|
|||
private _botName: string;
|
||||
private _logger: Logger;
|
||||
private _callbacks: OrchestratorCallbacks;
|
||||
private _options: OrchestratorOptions;
|
||||
|
||||
private _browser: Browser | null = null;
|
||||
private _context: BrowserContext | null = null;
|
||||
private _page: Page | null = null;
|
||||
private _gatewayWs: WebSocket | null = null;
|
||||
|
||||
private _joinProcedure: JoinProcedure | null = null;
|
||||
private _captionsProcedure: CaptionsProcedure | null = null;
|
||||
|
|
@ -48,12 +58,14 @@ export class BotOrchestrator {
|
|||
sessionId: string,
|
||||
meetingUrl: string,
|
||||
botName: string,
|
||||
callbacks: OrchestratorCallbacks
|
||||
callbacks: OrchestratorCallbacks,
|
||||
options: OrchestratorOptions
|
||||
) {
|
||||
this._sessionId = sessionId;
|
||||
this._meetingUrl = meetingUrl;
|
||||
this._botName = botName || config.botName;
|
||||
this._callbacks = callbacks;
|
||||
this._options = options;
|
||||
this._logger = createSessionLogger(sessionId);
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +78,7 @@ export class BotOrchestrator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Start the bot - launch browser, join meeting, enable captions.
|
||||
* Start the bot - connect to Gateway, launch browser, join meeting, enable captions.
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (!isValidMeetingUrl(this._meetingUrl)) {
|
||||
|
|
@ -76,6 +88,9 @@ export class BotOrchestrator {
|
|||
try {
|
||||
this._setState('launching');
|
||||
|
||||
// Connect to Gateway WebSocket first
|
||||
await this._connectToGateway();
|
||||
|
||||
// Launch browser
|
||||
await this._launchBrowser();
|
||||
|
||||
|
|
@ -115,7 +130,118 @@ export class BotOrchestrator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Stop the bot - leave meeting, close browser.
|
||||
* Connect to the Gateway WebSocket for this session.
|
||||
*/
|
||||
private async _connectToGateway(): Promise<void> {
|
||||
const wsUrl = `${this._options.gatewayWsUrl}/${this._options.instanceId}/bot/ws/${this._sessionId}`;
|
||||
this._logger.info(`Connecting to Gateway: ${wsUrl}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._gatewayWs = new WebSocket(wsUrl);
|
||||
|
||||
this._gatewayWs.on('open', () => {
|
||||
this._logger.info('Connected to Gateway');
|
||||
resolve();
|
||||
});
|
||||
|
||||
this._gatewayWs.on('message', (data) => {
|
||||
this._handleGatewayMessage(data.toString());
|
||||
});
|
||||
|
||||
this._gatewayWs.on('close', (code, reason) => {
|
||||
this._logger.warn(`Gateway connection closed: ${code} - ${reason}`);
|
||||
if (!this._isShuttingDown) {
|
||||
this._setState('error', 'Gateway connection lost');
|
||||
}
|
||||
});
|
||||
|
||||
this._gatewayWs.on('error', (error) => {
|
||||
this._logger.error('Gateway WebSocket error:', error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (this._gatewayWs?.readyState !== WebSocket.OPEN) {
|
||||
reject(new Error('Gateway connection timeout'));
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming messages from the Gateway.
|
||||
*/
|
||||
private _handleGatewayMessage(data: string): void {
|
||||
try {
|
||||
const message = JSON.parse(data);
|
||||
|
||||
switch (message.type) {
|
||||
case 'playAudio':
|
||||
const audioMsg = message as PlayAudioMessage;
|
||||
this.playAudio(audioMsg.audio.data, audioMsg.audio.format);
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
// Heartbeat response
|
||||
break;
|
||||
|
||||
default:
|
||||
this._logger.debug('Unknown Gateway message type:', message.type);
|
||||
}
|
||||
} catch (error) {
|
||||
this._logger.error('Error parsing Gateway message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the Gateway.
|
||||
*/
|
||||
private _sendToGateway(message: object): void {
|
||||
if (!this._gatewayWs || this._gatewayWs.readyState !== WebSocket.OPEN) {
|
||||
this._logger.warn('Cannot send to Gateway - not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._gatewayWs.send(JSON.stringify(message));
|
||||
} catch (error) {
|
||||
this._logger.error('Error sending to Gateway:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a transcript to the Gateway.
|
||||
*/
|
||||
private _sendTranscript(speaker: string, text: string, isFinal: boolean): void {
|
||||
const message: TranscriptMessage = {
|
||||
type: 'transcript',
|
||||
sessionId: this._sessionId,
|
||||
transcript: {
|
||||
speaker,
|
||||
text,
|
||||
timestamp: new Date().toISOString(),
|
||||
isFinal,
|
||||
},
|
||||
};
|
||||
this._sendToGateway(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a status update to the Gateway.
|
||||
*/
|
||||
private _sendStatus(status: StatusMessage['status'], message?: string): void {
|
||||
const statusMessage: StatusMessage = {
|
||||
type: 'status',
|
||||
sessionId: this._sessionId,
|
||||
status,
|
||||
message,
|
||||
};
|
||||
this._sendToGateway(statusMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the bot - leave meeting, close browser, disconnect from Gateway.
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (this._isShuttingDown) {
|
||||
|
|
@ -148,6 +274,13 @@ export class BotOrchestrator {
|
|||
} finally {
|
||||
// Close browser
|
||||
await this._closeBrowser();
|
||||
|
||||
// Close Gateway connection
|
||||
if (this._gatewayWs) {
|
||||
this._gatewayWs.close(1000, 'Bot stopping');
|
||||
this._gatewayWs = null;
|
||||
}
|
||||
|
||||
this._setState('disconnected');
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +325,9 @@ export class BotOrchestrator {
|
|||
// Initialize procedures
|
||||
this._joinProcedure = new JoinProcedure(this._page, this._logger, this._botName);
|
||||
this._captionsProcedure = new CaptionsProcedure(this._page, this._logger, (entry) => {
|
||||
// Send transcript to Gateway
|
||||
this._sendTranscript(entry.speaker, entry.text, entry.isFinal);
|
||||
// Also notify local callbacks
|
||||
this._callbacks.onTranscript(entry);
|
||||
});
|
||||
this._audioProcedure = new AudioProcedure(this._page, this._logger);
|
||||
|
|
@ -278,12 +414,25 @@ export class BotOrchestrator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the bot state and notify callbacks.
|
||||
* Update the bot state and notify callbacks + Gateway.
|
||||
*/
|
||||
private _setState(state: BotState, message?: string): void {
|
||||
this._state = state;
|
||||
this._logger.info(`State changed: ${state}${message ? ` - ${message}` : ''}`);
|
||||
this._callbacks.onStateChange(state, message);
|
||||
|
||||
// Send status to Gateway
|
||||
const statusMap: Record<BotState, StatusMessage['status']> = {
|
||||
'idle': 'connecting',
|
||||
'launching': 'connecting',
|
||||
'navigating': 'connecting',
|
||||
'in_lobby': 'in_lobby',
|
||||
'in_meeting': 'joined',
|
||||
'leaving': 'left',
|
||||
'error': 'error',
|
||||
'disconnected': 'left',
|
||||
};
|
||||
this._sendStatus(statusMap[state], message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ async function main(): Promise<void> {
|
|||
|
||||
// Start HTTP server
|
||||
httpServer = new HttpServer({
|
||||
onJoinRequest: async (sessionId, meetingUrl, botName) => {
|
||||
await sessionManager.createSession(sessionId, meetingUrl, botName);
|
||||
onJoinRequest: async (sessionId, meetingUrl, botName, instanceId) => {
|
||||
await sessionManager.createSession(sessionId, meetingUrl, botName, instanceId);
|
||||
},
|
||||
onLeaveRequest: async (sessionId) => {
|
||||
await sessionManager.endSession(sessionId);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { logger } from '../utils/logger';
|
|||
import { config } from '../config';
|
||||
|
||||
export interface HttpServerCallbacks {
|
||||
onJoinRequest: (sessionId: string, meetingUrl: string, botName?: string) => Promise<void>;
|
||||
onJoinRequest: (sessionId: string, meetingUrl: string, botName?: string, instanceId?: string) => Promise<void>;
|
||||
onLeaveRequest: (sessionId: string) => Promise<void>;
|
||||
onStatusRequest: (sessionId: string) => { state: string; error?: string } | null;
|
||||
}
|
||||
|
|
@ -77,14 +77,14 @@ export class HttpServer {
|
|||
// Deploy a new bot
|
||||
this._app.post('/api/bot', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sessionId, meetingUrl, botName } = req.body;
|
||||
const { sessionId, meetingUrl, botName, instanceId } = req.body;
|
||||
|
||||
if (!sessionId || !meetingUrl) {
|
||||
res.status(400).json({ error: 'Missing required fields: sessionId, meetingUrl' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this._callbacks.onJoinRequest(sessionId, meetingUrl, botName);
|
||||
await this._callbacks.onJoinRequest(sessionId, meetingUrl, botName, instanceId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -1,59 +1,40 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { BotOrchestrator, OrchestratorCallbacks } from './bot/orchestrator';
|
||||
import { GatewayClient } from './server/gatewayClient';
|
||||
import { BotOrchestrator, OrchestratorCallbacks, OrchestratorOptions } from './bot/orchestrator';
|
||||
import { BotSession, BotState, TranscriptEntry } from './types';
|
||||
import { logger } from './utils/logger';
|
||||
import { config } from './config';
|
||||
|
||||
/**
|
||||
* Manages all active bot sessions.
|
||||
* Coordinates between the Gateway client and bot orchestrators.
|
||||
* Each session connects to the Gateway independently via WebSocket.
|
||||
*/
|
||||
export class SessionManager {
|
||||
private _sessions: Map<string, BotOrchestrator> = new Map();
|
||||
private _gatewayClient: GatewayClient | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Initialize the session manager and connect to the Gateway.
|
||||
* Initialize the session manager.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
logger.info('Initializing SessionManager...');
|
||||
|
||||
// Create Gateway client
|
||||
this._gatewayClient = new GatewayClient(config.gatewayWsUrl, {
|
||||
onJoinMeeting: (sessionId, meetingUrl, botName) => {
|
||||
this.createSession(sessionId, meetingUrl, botName);
|
||||
},
|
||||
onLeaveMeeting: (sessionId) => {
|
||||
this.endSession(sessionId);
|
||||
},
|
||||
onPlayAudio: (sessionId, audioData, format) => {
|
||||
this.playAudio(sessionId, audioData, format);
|
||||
},
|
||||
onDisconnect: () => {
|
||||
logger.warn('Gateway disconnected');
|
||||
},
|
||||
});
|
||||
|
||||
// Connect to Gateway
|
||||
try {
|
||||
await this._gatewayClient.connect();
|
||||
logger.info('Connected to Gateway');
|
||||
} catch (error) {
|
||||
logger.error('Failed to connect to Gateway:', error);
|
||||
// Continue without Gateway - can still use HTTP API
|
||||
}
|
||||
logger.info(`Gateway WebSocket URL: ${config.gatewayWsUrl}`);
|
||||
// Sessions connect to Gateway individually when created
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new bot session and join the meeting.
|
||||
*
|
||||
* @param sessionId - Unique session ID
|
||||
* @param meetingUrl - Teams meeting URL
|
||||
* @param botName - Display name for the bot
|
||||
* @param instanceId - Feature instance ID (for Gateway routing)
|
||||
*/
|
||||
async createSession(
|
||||
sessionId: string,
|
||||
meetingUrl: string,
|
||||
botName?: string
|
||||
botName?: string,
|
||||
instanceId?: string
|
||||
): Promise<void> {
|
||||
if (this._sessions.has(sessionId)) {
|
||||
logger.warn(`Session ${sessionId} already exists`);
|
||||
|
|
@ -74,11 +55,18 @@ export class SessionManager {
|
|||
},
|
||||
};
|
||||
|
||||
// Options for Gateway connection
|
||||
const options: OrchestratorOptions = {
|
||||
gatewayWsUrl: config.gatewayWsUrl,
|
||||
instanceId: instanceId || 'default',
|
||||
};
|
||||
|
||||
const orchestrator = new BotOrchestrator(
|
||||
sessionId,
|
||||
meetingUrl,
|
||||
botName || config.botName,
|
||||
callbacks
|
||||
callbacks,
|
||||
options
|
||||
);
|
||||
|
||||
this._sessions.set(sessionId, orchestrator);
|
||||
|
|
@ -147,7 +135,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Shutdown all sessions and disconnect from Gateway.
|
||||
* Shutdown all sessions.
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
logger.info('Shutting down SessionManager...');
|
||||
|
|
@ -156,11 +144,6 @@ export class SessionManager {
|
|||
const sessionIds = Array.from(this._sessions.keys());
|
||||
await Promise.all(sessionIds.map((id) => this.endSession(id)));
|
||||
|
||||
// Disconnect from Gateway
|
||||
if (this._gatewayClient) {
|
||||
this._gatewayClient.disconnect();
|
||||
}
|
||||
|
||||
logger.info('SessionManager shutdown complete');
|
||||
}
|
||||
|
||||
|
|
@ -170,11 +153,6 @@ export class SessionManager {
|
|||
private _handleStateChange(sessionId: string, state: BotState, message?: string): void {
|
||||
logger.info(`Session ${sessionId} state: ${state}${message ? ` - ${message}` : ''}`);
|
||||
|
||||
if (this._gatewayClient) {
|
||||
const status = this._gatewayClient.mapStateToStatus(state);
|
||||
this._gatewayClient.sendStatus(sessionId, status, message);
|
||||
}
|
||||
|
||||
// Clean up if disconnected or error
|
||||
if (state === 'disconnected' || state === 'error') {
|
||||
this._sessions.delete(sessionId);
|
||||
|
|
@ -186,15 +164,7 @@ export class SessionManager {
|
|||
*/
|
||||
private _handleTranscript(sessionId: string, entry: TranscriptEntry): void {
|
||||
logger.debug(`Session ${sessionId} transcript: [${entry.speaker}] ${entry.text}`);
|
||||
|
||||
if (this._gatewayClient) {
|
||||
this._gatewayClient.sendTranscript(
|
||||
sessionId,
|
||||
entry.speaker,
|
||||
entry.text,
|
||||
entry.isFinal
|
||||
);
|
||||
}
|
||||
// Transcripts are sent to Gateway by the orchestrator directly
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -202,9 +172,6 @@ export class SessionManager {
|
|||
*/
|
||||
private _handleError(sessionId: string, error: Error): void {
|
||||
logger.error(`Session ${sessionId} error:`, error);
|
||||
|
||||
if (this._gatewayClient) {
|
||||
this._gatewayClient.sendStatus(sessionId, 'error', error.message);
|
||||
}
|
||||
// Errors are sent to Gateway by the orchestrator directly
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue