Merge branch 'development' into claude/agent-proactive-chat-UYHu9

This commit is contained in:
Sully
2026-05-11 13:07:18 +08:00
committed by GitHub
128 changed files with 4827 additions and 1144 deletions

View File

@@ -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,
};
}