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();
// 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<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
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;

View File

@ -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')