fix(conversation): topic chips after warmup + English chip copy

- Buffer topic_suggestions until chat UI attaches (uiOwner + callback); replay on attach
- build_topic_chips respects user language for label/text; router passes user_language

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-12 11:10:21 +08:00
parent 7e64fc3faf
commit d155e45a44
5 changed files with 101 additions and 16 deletions

View File

@@ -32,6 +32,8 @@ export type TopicSuggestionsCallback = (payload: {
suggestions: TopicSuggestion[];
}) => void;
type TopicSuggestionsPayload = Parameters<TopicSuggestionsCallback>[0];
/** WebSocket `tts_audio`:服务端可能只带 base64、只带 COS URL或两者都有 */
export type TtsSegmentPayload = {
audioBase64?: string;
@@ -85,6 +87,8 @@ export class RealtimeSession {
private streamingBuffer = '';
/** 单段回复且服务端带 `assistant_message_id` 时用于落缓存 id */
private pendingAssistantMessageId: string | null = null;
/** WS 在聊天页挂载 `onTopicSuggestions` 之前到达(如列表预热连开场)时暂存,接棒后重放 */
private pendingTopicSuggestionsPayload: TopicSuggestionsPayload | null = null;
private destroyed = false;
/** 本条用户消息是否请求「先 TTS 再出字」的助手轮次 */
@@ -162,6 +166,15 @@ export class RealtimeSession {
if (!this.assistantTurnTtsSync && this.streamingBuffer.trim().length > 0) {
this.onStreamingText?.(this.streamingBuffer, false);
}
if (
this.uiOwner &&
this.pendingTopicSuggestionsPayload &&
this.onTopicSuggestions
) {
const p = this.pendingTopicSuggestionsPayload;
this.pendingTopicSuggestionsPayload = null;
this.onTopicSuggestions(p);
}
}
releaseUiCallbacks(
@@ -197,6 +210,7 @@ export class RealtimeSession {
this.resetAssistantTtsSyncState();
this.unsubEvent?.();
this.unsubState?.();
this.pendingTopicSuggestionsPayload = null;
this.client.dispose();
}
@@ -394,11 +408,17 @@ export class RealtimeSession {
}
if (event.kind === 'topic_suggestions') {
this.onTopicSuggestions?.({
const payload: TopicSuggestionsPayload = {
reason: event.reason,
stage: event.stage,
suggestions: event.suggestions,
});
};
if (this.uiOwner && this.onTopicSuggestions) {
this.pendingTopicSuggestionsPayload = null;
this.onTopicSuggestions(payload);
} else {
this.pendingTopicSuggestionsPayload = payload;
}
return;
}

View File

@@ -250,4 +250,52 @@ describe('RealtimeSession sync TTS / agent ordering', () => {
expect(staleScreenOnTts).not.toHaveBeenCalled();
session.dispose();
});
it('replays topic_suggestions after attachUiCallbacks with owner (warmup path)', async () => {
const onTopic = jest.fn();
const owner = Symbol('ui');
const session = new RealtimeSession({
conversationId: 'conv-x',
queryClient: qc,
});
await session.connect();
await new Promise((r) => setImmediate(r));
const ws = MockWebSocket.instances[0]!;
ws.simulateMessage({
type: 'topic_suggestions',
conversation_id: 'conv-x',
data: {
reason: 'opening',
stage: 'childhood',
suggestions: [
{
id: 'place',
label: 'where you grew up',
text: "I'd like to talk about where you grew up.",
},
],
},
timestamp: new Date().toISOString(),
});
expect(onTopic).not.toHaveBeenCalled();
session.attachUiCallbacks(
{
onTopicSuggestions: onTopic,
onStreamingText: () => {},
onTtsSegment: () => {},
onError: () => {},
onStateChange: () => {},
},
owner,
);
expect(onTopic).toHaveBeenCalledTimes(1);
expect(onTopic.mock.calls[0]![0].suggestions[0]!.id).toBe('place');
session.dispose();
});
});