fix(voice): suppress session terminate during DingTalk OAuth flow
When the voice agent triggers DingTalk OAuth, the user leaves the app to authorize in DingTalk/browser, causing the LiveKit participant to disconnect. The voice-agent then calls DELETE /voice to terminate the session — but the user intends to return after completing OAuth. Fix: mark the session as "oauth_pending" in VoiceSessionController when oauth-trigger fires. If terminateVoiceSession is called while the flag is active (10-min grace), suppress the terminate and return 200 OK so the voice-agent exits cleanly. The session stays alive; when the user returns to the voice screen, voice/start + inject auto-resume it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5a66f85235
commit
5874907300
|
|
@ -27,10 +27,23 @@ import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type
|
|||
import { AgentSession } from '../../../domain/entities/agent-session.entity';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
/** OAuth flow timeout — give the user this long to complete authorization before
|
||||
* allowing the voice session to be terminated (voice-agent disconnect during OAuth). */
|
||||
const OAUTH_GRACE_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
@Controller('api/v1/agent/sessions')
|
||||
export class VoiceSessionController {
|
||||
private readonly logger = new Logger(VoiceSessionController.name);
|
||||
|
||||
/**
|
||||
* Tracks sessions where a DingTalk OAuth flow was triggered.
|
||||
* While an entry is present (and not expired), the voice session will NOT
|
||||
* be terminated if the voice-agent calls DELETE /voice (which happens when
|
||||
* the LiveKit participant disconnects — a side-effect of the user leaving
|
||||
* the app to complete OAuth in DingTalk/browser).
|
||||
*/
|
||||
private readonly oauthPendingSessions = new Map<string, number>(); // sessionId → expiresAt
|
||||
|
||||
constructor(
|
||||
private readonly voiceSessionManager: VoiceSessionManager,
|
||||
private readonly sessionRepository: SessionRepository,
|
||||
|
|
@ -160,6 +173,20 @@ export class VoiceSessionController {
|
|||
@TenantId() tenantId: string,
|
||||
@Param('sessionId') sessionId: string,
|
||||
) {
|
||||
// If a DingTalk OAuth flow was triggered for this session, do NOT terminate yet.
|
||||
// The user left the app to authorize in DingTalk/browser, causing the LiveKit
|
||||
// participant to disconnect, which triggers this call from the voice-agent.
|
||||
// We keep the session alive so the user can resume after OAuth completes.
|
||||
const oauthExpiry = this.oauthPendingSessions.get(sessionId);
|
||||
if (oauthExpiry && Date.now() < oauthExpiry) {
|
||||
this.logger.log(
|
||||
`Voice session ${sessionId} terminate suppressed — DingTalk OAuth in progress ` +
|
||||
`(grace expires in ${Math.round((oauthExpiry - Date.now()) / 1000)}s)`,
|
||||
);
|
||||
return { sessionId, terminated: false, reason: 'oauth_pending' };
|
||||
}
|
||||
this.oauthPendingSessions.delete(sessionId);
|
||||
|
||||
const session = await this.sessionRepository.findById(sessionId);
|
||||
|
||||
if (session && session.tenantId === tenantId) {
|
||||
|
|
@ -211,6 +238,12 @@ export class VoiceSessionController {
|
|||
const instanceName = body.instanceName || inst.name || body.instanceId;
|
||||
const { oauthUrl } = this.dingTalkRouter.generateOAuthUrl(body.instanceId);
|
||||
|
||||
// Mark this session as having an in-flight OAuth flow.
|
||||
// terminateVoiceSession will suppress the terminate call while this is set,
|
||||
// so the LiveKit disconnect caused by the user leaving to authorize in
|
||||
// DingTalk/browser does not destroy the voice session prematurely.
|
||||
this.oauthPendingSessions.set(sessionId, Date.now() + OAUTH_GRACE_MS);
|
||||
|
||||
// Emit the oauth_prompt event to the session's WebSocket stream.
|
||||
// voice-agent subscribes to this stream and forwards it to Flutter via
|
||||
// LiveKit publish_data (topic="oauth_prompt"), which triggers the bottom sheet.
|
||||
|
|
|
|||
Loading…
Reference in New Issue