Merge branch 'development' into claude/agent-proactive-chat-UYHu9
This commit is contained in:
@@ -1,17 +1,25 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { File, Paths } from 'expo-file-system';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { AppState, type AppStateStatus } from 'react-native';
|
||||
|
||||
import type { TopicSuggestion, WsConnectionState } from '@/core/ws/types';
|
||||
|
||||
import { conversationApi } from './api';
|
||||
import {
|
||||
acquireBackgroundConversationWs,
|
||||
disposeAllBackgroundConversationWs,
|
||||
disposeBackgroundConversationWs,
|
||||
releaseConversationWsUi,
|
||||
} from './conversation-ws-background-pool';
|
||||
import { conversationMessagesRepository } from './conversation-messages-repository';
|
||||
import { conversationKeys } from './query-keys';
|
||||
import { takePreparedRealtimeSession } from './prepared-session-registry';
|
||||
import {
|
||||
RealtimeSession,
|
||||
type ErrorCallback,
|
||||
type StreamingTextCallback,
|
||||
type TtsSegmentPayload,
|
||||
type RealtimeSession,
|
||||
} from './realtime-session';
|
||||
import {
|
||||
type ConversationListItem,
|
||||
@@ -126,6 +134,7 @@ export function useDeleteConversation() {
|
||||
mutationFn: (conversationId: string) =>
|
||||
conversationApi.delete(conversationId),
|
||||
onSuccess: async (_, conversationId) => {
|
||||
disposeBackgroundConversationWs(conversationId);
|
||||
await voiceSegmentStore.clearConversation(conversationId);
|
||||
queryClient.setQueryData<ConversationListItem[]>(
|
||||
conversationKeys.lists(),
|
||||
@@ -165,6 +174,8 @@ interface UseRealtimeSessionOptions {
|
||||
onTtsSegment?: (payload: TtsSegmentPayload) => void;
|
||||
/** 用户发出下一条文本/语音成功后调用,用于恢复接受 TTS 片段(打断后丢弃迟到片段) */
|
||||
onTtsPlaybackResume?: () => void;
|
||||
/** 本条发送是否请求了「本轮助手朗读」,用于仅在该轮自动播放 WS TTS */
|
||||
onUserSendTtsPreference?: (requestedTts: boolean) => void;
|
||||
}
|
||||
|
||||
const MIN_RECORDING_DURATION_SEC = 1;
|
||||
@@ -190,6 +201,11 @@ interface RealtimeSessionState {
|
||||
sendVoiceMessage: (uri: string, durationMs: number) => Promise<boolean>;
|
||||
sendEndConversation: () => void;
|
||||
sendTtsCancel: () => void;
|
||||
requestAssistantSegmentTts: (body: {
|
||||
assistantMessageId: string;
|
||||
segmentIndex: number;
|
||||
segmentText?: string;
|
||||
}) => boolean;
|
||||
}
|
||||
|
||||
export function useRealtimeSession({
|
||||
@@ -197,9 +213,17 @@ export function useRealtimeSession({
|
||||
enabled = true,
|
||||
onTtsSegment,
|
||||
onTtsPlaybackResume,
|
||||
onUserSendTtsPreference,
|
||||
}: UseRealtimeSessionOptions): RealtimeSessionState {
|
||||
const queryClient = useQueryClient();
|
||||
const sessionRef = useRef<RealtimeSession | null>(null);
|
||||
const uiRef = useRef({
|
||||
handleStreamingText: (() => {}) as StreamingTextCallback,
|
||||
handleError: (() => {}) as ErrorCallback,
|
||||
onTtsSegment: undefined as
|
||||
| ((payload: TtsSegmentPayload) => void)
|
||||
| undefined,
|
||||
});
|
||||
|
||||
const [connectionState, setConnectionState] =
|
||||
useState<WsConnectionState>('disconnected');
|
||||
@@ -211,6 +235,10 @@ export function useRealtimeSession({
|
||||
[],
|
||||
);
|
||||
|
||||
const [foregroundResumeGeneration, setForegroundResumeGeneration] =
|
||||
useState(0);
|
||||
const needsResumeAfterBackgroundRef = useRef(false);
|
||||
|
||||
const handleStreamingText: StreamingTextCallback = useCallback(
|
||||
(text, isComplete) => {
|
||||
if (text.trim().length > 0) {
|
||||
@@ -245,7 +273,8 @@ export function useRealtimeSession({
|
||||
useEffect(() => {
|
||||
if (!enabled || !conversationId) return;
|
||||
|
||||
const session = new RealtimeSession({
|
||||
const prepared = takePreparedRealtimeSession(conversationId);
|
||||
const session = acquireBackgroundConversationWs(
|
||||
conversationId,
|
||||
queryClient,
|
||||
onStreamingText: handleStreamingText,
|
||||
@@ -256,10 +285,10 @@ export function useRealtimeSession({
|
||||
});
|
||||
|
||||
sessionRef.current = session;
|
||||
session.connect();
|
||||
setConnectionState(session.getConnectionState());
|
||||
|
||||
return () => {
|
||||
session.dispose();
|
||||
releaseConversationWsUi(session);
|
||||
sessionRef.current = null;
|
||||
setConnectionState('disconnected');
|
||||
setStreamingMessage(null);
|
||||
@@ -277,15 +306,17 @@ export function useRealtimeSession({
|
||||
]);
|
||||
|
||||
const sendText = useCallback(
|
||||
(text: string) => {
|
||||
(text: string, options?: { ttsThisTurn?: boolean }) => {
|
||||
if (!sessionRef.current) return;
|
||||
|
||||
const sent = sessionRef.current.sendText(text);
|
||||
const sent = sessionRef.current.sendText(text, options);
|
||||
if (!sent) {
|
||||
setError('消息发送失败,连接未就绪');
|
||||
return;
|
||||
}
|
||||
|
||||
onUserSendTtsPreference?.(options?.ttsThisTurn === true);
|
||||
|
||||
setAwaitingAssistantReply(true);
|
||||
setTopicSuggestions([]);
|
||||
onTtsPlaybackResume?.();
|
||||
@@ -319,11 +350,15 @@ export function useRealtimeSession({
|
||||
},
|
||||
);
|
||||
},
|
||||
[conversationId, queryClient, onTtsPlaybackResume],
|
||||
[conversationId, queryClient, onTtsPlaybackResume, onUserSendTtsPreference],
|
||||
);
|
||||
|
||||
const sendVoiceMessage = useCallback(
|
||||
async (uri: string, durationMs: number): Promise<boolean> => {
|
||||
async (
|
||||
uri: string,
|
||||
durationMs: number,
|
||||
options?: { ttsThisTurn?: boolean },
|
||||
): Promise<boolean> => {
|
||||
const session = sessionRef.current;
|
||||
if (!session) return false;
|
||||
|
||||
@@ -340,12 +375,15 @@ export function useRealtimeSession({
|
||||
clientSegmentId: `${voiceSessionId}-0`,
|
||||
isLast: true,
|
||||
duration: durationSec,
|
||||
ttsThisTurn: options?.ttsThisTurn,
|
||||
});
|
||||
if (!sent) {
|
||||
setError('语音发送失败,连接未就绪');
|
||||
return false;
|
||||
}
|
||||
|
||||
onUserSendTtsPreference?.(options?.ttsThisTurn === true);
|
||||
|
||||
setAwaitingAssistantReply(true);
|
||||
setTopicSuggestions([]);
|
||||
const localId = `pending_voice_${Date.now()}`;
|
||||
@@ -391,7 +429,7 @@ export function useRealtimeSession({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[conversationId, queryClient, onTtsPlaybackResume],
|
||||
[conversationId, queryClient, onTtsPlaybackResume, onUserSendTtsPreference],
|
||||
);
|
||||
|
||||
const sendEndConversation = useCallback(() => {
|
||||
@@ -402,6 +440,15 @@ export function useRealtimeSession({
|
||||
sessionRef.current?.sendTtsCancel();
|
||||
}, []);
|
||||
|
||||
const requestAssistantSegmentTts = useCallback(
|
||||
(body: {
|
||||
assistantMessageId: string;
|
||||
segmentIndex: number;
|
||||
segmentText?: string;
|
||||
}) => sessionRef.current?.requestAssistantSegmentTts(body) ?? false,
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
connectionState,
|
||||
streamingMessage,
|
||||
@@ -413,5 +460,6 @@ export function useRealtimeSession({
|
||||
sendVoiceMessage,
|
||||
sendEndConversation,
|
||||
sendTtsCancel,
|
||||
requestAssistantSegmentTts,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user