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 { AgentSession } from '../../../domain/entities/agent-session.entity';
|
||||||
import * as crypto from 'crypto';
|
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')
|
@Controller('api/v1/agent/sessions')
|
||||||
export class VoiceSessionController {
|
export class VoiceSessionController {
|
||||||
private readonly logger = new Logger(VoiceSessionController.name);
|
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(
|
constructor(
|
||||||
private readonly voiceSessionManager: VoiceSessionManager,
|
private readonly voiceSessionManager: VoiceSessionManager,
|
||||||
private readonly sessionRepository: SessionRepository,
|
private readonly sessionRepository: SessionRepository,
|
||||||
|
|
@ -160,6 +173,20 @@ export class VoiceSessionController {
|
||||||
@TenantId() tenantId: string,
|
@TenantId() tenantId: string,
|
||||||
@Param('sessionId') sessionId: 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);
|
const session = await this.sessionRepository.findById(sessionId);
|
||||||
|
|
||||||
if (session && session.tenantId === tenantId) {
|
if (session && session.tenantId === tenantId) {
|
||||||
|
|
@ -211,6 +238,12 @@ export class VoiceSessionController {
|
||||||
const instanceName = body.instanceName || inst.name || body.instanceId;
|
const instanceName = body.instanceName || inst.name || body.instanceId;
|
||||||
const { oauthUrl } = this.dingTalkRouter.generateOAuthUrl(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.
|
// Emit the oauth_prompt event to the session's WebSocket stream.
|
||||||
// voice-agent subscribes to this stream and forwards it to Flutter via
|
// voice-agent subscribes to this stream and forwards it to Flutter via
|
||||||
// LiveKit publish_data (topic="oauth_prompt"), which triggers the bottom sheet.
|
// LiveKit publish_data (topic="oauth_prompt"), which triggers the bottom sheet.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue