import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { File } from 'expo-file-system'; import { useCallback, useEffect, useRef, useState } from 'react'; import type { WsConnectionState } from '@/core/ws/types'; import { conversationApi } from './api'; import { conversationKeys } from './query-keys'; import { RealtimeSession, type ErrorCallback, type StreamingTextCallback, } from './realtime-session'; import type { ConversationListItem, MessageItem, StreamingAgentMessage, } from './types'; // ─── Query hooks ─── // TODO: 连接不上后端时 isLoading 可能一直为 true,需加超时或展示错误态 export function useConversations() { return useQuery({ queryKey: conversationKeys.lists(), queryFn: () => conversationApi.list(), }); } export function useConversationDetail(conversationId: string) { return useQuery({ queryKey: conversationKeys.detail(conversationId), queryFn: () => conversationApi.detail(conversationId), enabled: !!conversationId, }); } export function useMessages(conversationId: string) { return useQuery({ queryKey: conversationKeys.messages(conversationId), queryFn: () => conversationApi.messages(conversationId), enabled: !!conversationId, }); } // ─── Mutation hooks ─── export function useCreateConversation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => conversationApi.create(), onSuccess: (newConversation) => { queryClient.setQueryData( conversationKeys.lists(), (old) => { const item: ConversationListItem = { id: newConversation.id, title: '岁月知己', avatarUrl: null, latestMessagePreview: '', latestMessageTime: Date.now(), unreadCount: 0, isDefaultAssistant: true, }; return [item, ...(old ?? [])]; }, ); }, }); } export function useDeleteConversation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (conversationId: string) => conversationApi.delete(conversationId), onSuccess: (_, conversationId) => { queryClient.setQueryData( conversationKeys.lists(), (old) => old?.filter((item) => item.id !== conversationId), ); queryClient.removeQueries({ queryKey: conversationKeys.detail(conversationId), }); queryClient.removeQueries({ queryKey: conversationKeys.messages(conversationId), }); }, }); } export function useEndConversation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (conversationId: string) => conversationApi.end(conversationId), onSuccess: (_, conversationId) => { queryClient.invalidateQueries({ queryKey: conversationKeys.detail(conversationId), }); queryClient.invalidateQueries({ queryKey: conversationKeys.lists(), }); }, }); } // ─── Realtime session hook ─── interface UseRealtimeSessionOptions { conversationId: string; enabled?: boolean; onTtsAudio?: (audioBase64: string) => void; } const MIN_RECORDING_DURATION_SEC = 1; function generateUUID(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } interface RealtimeSessionState { connectionState: WsConnectionState; streamingMessage: StreamingAgentMessage | null; error: string | null; sendText: (text: string) => void; sendVoiceMessage: (uri: string, durationMs: number) => Promise; sendEndConversation: () => void; } export function useRealtimeSession({ conversationId, enabled = true, onTtsAudio, }: UseRealtimeSessionOptions): RealtimeSessionState { const queryClient = useQueryClient(); const sessionRef = useRef(null); const [connectionState, setConnectionState] = useState('disconnected'); const [streamingMessage, setStreamingMessage] = useState(null); const [error, setError] = useState(null); const handleStreamingText: StreamingTextCallback = useCallback( (text, isComplete) => { if (isComplete) { setStreamingMessage(null); return; } setStreamingMessage({ text, isComplete }); }, [], ); const handleError: ErrorCallback = useCallback((message) => { setError(message); }, []); useEffect(() => { if (!enabled || !conversationId) return; const session = new RealtimeSession({ conversationId, queryClient, onStreamingText: handleStreamingText, onTtsAudio, onError: handleError, onStateChange: setConnectionState, }); sessionRef.current = session; session.connect(); return () => { session.dispose(); sessionRef.current = null; setConnectionState('disconnected'); setStreamingMessage(null); }; }, [conversationId, enabled, queryClient, handleStreamingText, handleError, onTtsAudio]); const sendText = useCallback( (text: string) => { if (!sessionRef.current) return; const sent = sessionRef.current.sendText(text); if (!sent) { setError('消息发送失败,连接未就绪'); return; } const localId = `pending_${Date.now()}`; queryClient.setQueryData( conversationKeys.messages(conversationId), (old) => { const msg: MessageItem = { id: localId, conversationId, content: text, senderType: 'user', timestamp: Date.now(), messageType: 'text', }; return [...(old ?? []), msg]; }, ); }, [conversationId, queryClient], ); const sendVoiceMessage = useCallback( async (uri: string, durationMs: number): Promise => { const session = sessionRef.current; if (!session) return false; const durationSec = Math.round(durationMs / 1000); if (durationSec < MIN_RECORDING_DURATION_SEC) return false; try { const file = new File(uri); const base64 = await file.base64(); if (!base64) return false; const voiceSessionId = generateUUID(); const sent = session.sendAudioSegment(base64, 0, { voiceSessionId, clientSegmentId: `${voiceSessionId}-0`, isLast: true, duration: durationSec, }); if (!sent) { setError('语音发送失败,连接未就绪'); return false; } const localId = `pending_voice_${Date.now()}`; queryClient.setQueryData( conversationKeys.messages(conversationId), (old) => { const msg: MessageItem = { id: localId, conversationId, content: '', senderType: 'user', timestamp: Date.now(), messageType: 'voice', durationSeconds: durationSec, audioUri: uri, }; return [...(old ?? []), msg]; }, ); return true; } catch { setError('语音文件读取失败'); return false; } }, [conversationId, queryClient], ); const sendEndConversation = useCallback(() => { sessionRef.current?.sendEndConversation(); }, []); return { connectionState, streamingMessage, error, sendText, sendVoiceMessage, sendEndConversation, }; }