修复:CI 部署环境与 ref 错配、迁移碎片化、图片意图 source_span、章节物化脏版式、会话历史与本地语音不一致

新增:TTS 上传 COS 与分片、章节 reading_segments 物化与快照、markdown 清洗、会话消息 repository、语音 store 重构与相关测试
This commit is contained in:
Kevin
2026-03-20 16:36:42 +08:00
parent 7317bf10cd
commit 8af37e5e8e
65 changed files with 1704 additions and 504 deletions

View File

@@ -5,17 +5,20 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import type { WsConnectionState } from '@/core/ws/types';
import { conversationApi } from './api';
import { conversationMessagesRepository } from './conversation-messages-repository';
import { conversationKeys } from './query-keys';
import {
RealtimeSession,
type ErrorCallback,
type StreamingTextCallback,
type TtsSegmentPayload,
} from './realtime-session';
import type {
ConversationListItem,
MessageItem,
StreamingAgentMessage,
import {
type ConversationListItem,
type MessageItem,
type StreamingAgentMessage,
} from './types';
import { voiceSegmentStore } from '@/features/voice/voice-segment-store';
// ─── Query hooks ───
@@ -38,7 +41,7 @@ export function useConversationDetail(conversationId: string) {
export function useMessages(conversationId: string) {
return useQuery({
queryKey: conversationKeys.messages(conversationId),
queryFn: () => conversationApi.messages(conversationId),
queryFn: () => conversationMessagesRepository.loadMessages(conversationId),
enabled: !!conversationId,
});
}
@@ -76,7 +79,8 @@ export function useDeleteConversation() {
return useMutation({
mutationFn: (conversationId: string) =>
conversationApi.delete(conversationId),
onSuccess: (_, conversationId) => {
onSuccess: async (_, conversationId) => {
await voiceSegmentStore.clearConversation(conversationId);
queryClient.setQueryData<ConversationListItem[]>(
conversationKeys.lists(),
(old) => old?.filter((item) => item.id !== conversationId),
@@ -112,7 +116,7 @@ export function useEndConversation() {
interface UseRealtimeSessionOptions {
conversationId: string;
enabled?: boolean;
onTtsAudio?: (audioBase64: string) => void;
onTtsSegment?: (payload: TtsSegmentPayload) => void;
}
const MIN_RECORDING_DURATION_SEC = 1;
@@ -137,7 +141,7 @@ interface RealtimeSessionState {
export function useRealtimeSession({
conversationId,
enabled = true,
onTtsAudio,
onTtsSegment,
}: UseRealtimeSessionOptions): RealtimeSessionState {
const queryClient = useQueryClient();
const sessionRef = useRef<RealtimeSession | null>(null);
@@ -170,7 +174,7 @@ export function useRealtimeSession({
conversationId,
queryClient,
onStreamingText: handleStreamingText,
onTtsAudio,
onTtsSegment,
onError: handleError,
onStateChange: setConnectionState,
});
@@ -190,7 +194,7 @@ export function useRealtimeSession({
queryClient,
handleStreamingText,
handleError,
onTtsAudio,
onTtsSegment,
]);
const sendText = useCallback(
@@ -249,6 +253,12 @@ export function useRealtimeSession({
}
const localId = `pending_voice_${Date.now()}`;
await voiceSegmentStore.recordSentSegment({
voiceSessionId,
conversationId,
fileUri: uri,
durationMs,
});
queryClient.setQueryData<MessageItem[]>(
conversationKeys.messages(conversationId),
(old) => {
@@ -259,6 +269,7 @@ export function useRealtimeSession({
senderType: 'user',
timestamp: Date.now(),
messageType: 'voice',
voiceSessionId,
durationSeconds: durationSec,
audioUri: uri,
};