diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts index bffce03..e6bf31f 100644 --- a/packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts +++ b/packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts @@ -53,17 +53,33 @@ export class AgentController { : this.engineRegistry.getActiveEngine(); // Reuse existing session or create new one + const isVoice = body.voiceMode ?? false; let session: AgentSession; if (body.sessionId) { const existing = await this.sessionRepository.findById(body.sessionId); if (existing && existing.status === 'active' && existing.tenantId === tenantId) { session = existing; } else { - session = this.createNewSession(tenantId, engine.engineType, body.systemPrompt); + session = this.createNewSession(tenantId, engine.engineType, body.systemPrompt, isVoice); } } else { - session = this.createNewSession(tenantId, engine.engineType, body.systemPrompt); + session = this.createNewSession(tenantId, engine.engineType, body.systemPrompt, isVoice); } + + // Set a human-readable title on the FIRST task of this session. + // This title is stored in metadata so the session list can display it + // without ever exposing the internal systemPrompt string to the client. + // - Text sessions: truncate the first user prompt to 40 chars + // - Voice sessions: leave title empty; Flutter renders "语音对话 M/D HH:mm" + if (!(session.metadata as Record).title && !(session.metadata as Record).titleSet) { + session.metadata = { + ...session.metadata as Record, + voiceMode: isVoice, + title: isVoice ? '' : body.prompt.substring(0, 40).trim(), + titleSet: true, // prevent overwrite on subsequent turns + }; + } + // Keep session active for multi-turn session.status = 'active'; session.updatedAt = new Date(); @@ -619,14 +635,20 @@ export class AgentController { }); } - private createNewSession(tenantId: string, engineType: string, systemPrompt?: string): AgentSession { + private createNewSession( + tenantId: string, + engineType: string, + systemPrompt?: string, + voiceMode?: boolean, + ): AgentSession { const session = new AgentSession(); session.id = crypto.randomUUID(); session.tenantId = tenantId; session.engineType = engineType; session.status = 'active'; session.systemPrompt = systemPrompt; - session.metadata = {}; + // Pre-populate voiceMode so it's available even before the first task saves it + session.metadata = { voiceMode: voiceMode ?? false }; session.createdAt = new Date(); session.updatedAt = new Date(); return session; diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/session.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/session.controller.ts index c9388de..0805797 100644 --- a/packages/services/agent-service/src/interfaces/rest/controllers/session.controller.ts +++ b/packages/services/agent-service/src/interfaces/rest/controllers/session.controller.ts @@ -14,7 +14,18 @@ export class SessionController { @Get() async listSessions(@TenantId() tenantId: string) { - return this.sessionRepository.findByTenant(tenantId); + const sessions = await this.sessionRepository.findByTenant(tenantId); + // Return a safe DTO: systemPrompt is an internal engine instruction and must + // NOT be sent to the client (it would be displayed as the conversation title). + // The client derives the display title from metadata.title / metadata.voiceMode. + return sessions.map((s) => ({ + id: s.id, + status: s.status, + engineType: s.engineType, + metadata: s.metadata, // contains { title, voiceMode, titleSet, sdkSessionId? } + createdAt: s.createdAt, + updatedAt: s.updatedAt, + })); } @Get(':sessionId')