fix(agent-service): store proper title in session metadata, exclude systemPrompt from list API

Two issues fixed:

1. agent.controller.ts — on the FIRST task of each session, write title+voiceMode
   into session.metadata so the client can display a meaningful conversation title:
     - Text sessions: metadata.title = first 40 chars of user prompt
     - Voice sessions: metadata.title = '' + metadata.voiceMode = true
       (Flutter renders these as '语音对话 M/D HH:mm')
   titleSet flag prevents overwriting the title on subsequent turns of the same session.

2. session.controller.ts — listSessions() now returns a DTO instead of the raw entity.
   systemPrompt is an internal engine instruction and is explicitly excluded from the
   response. The client receives { id, status, engineType, metadata, createdAt, updatedAt }.
This commit is contained in:
hailin 2026-03-04 02:39:47 -08:00
parent 9546dab93d
commit 6ca8aab243
2 changed files with 38 additions and 5 deletions

View File

@ -53,17 +53,33 @@ export class AgentController {
: this.engineRegistry.getActiveEngine(); : this.engineRegistry.getActiveEngine();
// Reuse existing session or create new one // Reuse existing session or create new one
const isVoice = body.voiceMode ?? false;
let session: AgentSession; let session: AgentSession;
if (body.sessionId) { if (body.sessionId) {
const existing = await this.sessionRepository.findById(body.sessionId); const existing = await this.sessionRepository.findById(body.sessionId);
if (existing && existing.status === 'active' && existing.tenantId === tenantId) { if (existing && existing.status === 'active' && existing.tenantId === tenantId) {
session = existing; session = existing;
} else { } else {
session = this.createNewSession(tenantId, engine.engineType, body.systemPrompt); session = this.createNewSession(tenantId, engine.engineType, body.systemPrompt, isVoice);
} }
} else { } 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<string, unknown>).title && !(session.metadata as Record<string, unknown>).titleSet) {
session.metadata = {
...session.metadata as Record<string, unknown>,
voiceMode: isVoice,
title: isVoice ? '' : body.prompt.substring(0, 40).trim(),
titleSet: true, // prevent overwrite on subsequent turns
};
}
// Keep session active for multi-turn // Keep session active for multi-turn
session.status = 'active'; session.status = 'active';
session.updatedAt = new Date(); 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(); const session = new AgentSession();
session.id = crypto.randomUUID(); session.id = crypto.randomUUID();
session.tenantId = tenantId; session.tenantId = tenantId;
session.engineType = engineType; session.engineType = engineType;
session.status = 'active'; session.status = 'active';
session.systemPrompt = systemPrompt; 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.createdAt = new Date();
session.updatedAt = new Date(); session.updatedAt = new Date();
return session; return session;

View File

@ -14,7 +14,18 @@ export class SessionController {
@Get() @Get()
async listSessions(@TenantId() tenantId: string) { 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') @Get(':sessionId')