继续重构会话消息处理逻辑

This commit is contained in:
xintaofei
2026-03-12 18:34:34 +08:00
parent 4e49e2f16a
commit bd5456423f
9 changed files with 452 additions and 779 deletions

View File

@@ -0,0 +1,39 @@
"use client"
import { useEffect } from "react"
import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
import type { DbConversationDetail } from "@/lib/types"
function isVirtualConversationId(conversationId: number): boolean {
return !Number.isFinite(conversationId) || conversationId <= 0
}
export function useConversationDetail(conversationId: number): {
detail: DbConversationDetail | null
loading: boolean
error: string | null
} {
const { getSession, fetchDetail } = useConversationRuntime()
const session = getSession(conversationId)
const isVirtual = isVirtualConversationId(conversationId)
useEffect(() => {
if (isVirtual) return
if (session?.detail || session?.detailLoading) return
fetchDetail(conversationId)
}, [
conversationId,
isVirtual,
session?.detail,
session?.detailLoading,
fetchDetail,
])
return {
detail: session?.detail ?? null,
loading: session
? session.detailLoading
: !isVirtual,
error: session?.detailError ?? null,
}
}

View File

@@ -1,208 +0,0 @@
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { getFolderConversation } from "@/lib/tauri"
import type { DbConversationDetail } from "@/lib/types"
// Module-level cache: survives component unmount/remount
const detailCache = new Map<number, DbConversationDetail>()
const detailInFlight = new Map<number, Promise<DbConversationDetail>>()
const detailListeners = new Map<
number,
Set<(detail: DbConversationDetail) => void>
>()
function publishDetail(conversationId: number, detail: DbConversationDetail) {
const listeners = detailListeners.get(conversationId)
if (!listeners || listeners.size === 0) return
for (const listener of listeners) {
listener(detail)
}
}
function setCachedDetail(conversationId: number, detail: DbConversationDetail) {
detailCache.set(conversationId, detail)
publishDetail(conversationId, detail)
}
function subscribeDetail(
conversationId: number,
listener: (detail: DbConversationDetail) => void
) {
let listeners = detailListeners.get(conversationId)
if (!listeners) {
listeners = new Set()
detailListeners.set(conversationId, listeners)
}
listeners.add(listener)
return () => {
const current = detailListeners.get(conversationId)
if (!current) return
current.delete(listener)
if (current.size === 0) {
detailListeners.delete(conversationId)
}
}
}
/** Invalidate cached detail so the next mount re-fetches from disk. */
export function invalidateDetailCache(conversationId: number) {
detailCache.delete(conversationId)
}
async function loadAndCacheDetail(
conversationId: number
): Promise<DbConversationDetail> {
const existing = detailInFlight.get(conversationId)
if (existing) return existing
const promise = getFolderConversation(conversationId)
.then((detail) => {
setCachedDetail(conversationId, detail)
return detail
})
.finally(() => {
detailInFlight.delete(conversationId)
})
detailInFlight.set(conversationId, promise)
return promise
}
export async function refreshDetailCache(
conversationId: number
): Promise<DbConversationDetail> {
detailCache.delete(conversationId)
return loadAndCacheDetail(conversationId)
}
interface State {
key: number
detail: DbConversationDetail | null
loading: boolean
error: string | null
fetchSeq: number
}
function isVirtualConversationId(conversationId: number): boolean {
return !Number.isFinite(conversationId) || conversationId <= 0
}
export function useDbMessageDetail(conversationId: number) {
const isVirtualId = isVirtualConversationId(conversationId)
const getCachedState = useCallback((id: number): State => {
if (isVirtualConversationId(id)) {
return {
key: id,
detail: null,
loading: false,
error: null,
fetchSeq: 0,
}
}
const cached = detailCache.get(id)
return {
key: id,
detail: cached ?? null,
loading: !cached,
error: null,
fetchSeq: 0,
}
}, [])
const [state, setState] = useState<State>(() => {
return getCachedState(conversationId)
})
const derivedState =
state.key === conversationId ? state : getCachedState(conversationId)
useEffect(() => {
if (isVirtualId) return
return subscribeDetail(conversationId, (detail) => {
setState((prev) => ({
key: conversationId,
detail,
loading: false,
error: null,
fetchSeq: prev.key === conversationId ? prev.fetchSeq : 0,
}))
})
}, [conversationId, isVirtualId])
const refetch = useCallback(() => {
if (isVirtualConversationId(conversationId)) {
setState(getCachedState(conversationId))
return
}
detailCache.delete(conversationId)
setState((prev) => {
const base =
prev.key === conversationId ? prev : getCachedState(conversationId)
return {
...base,
key: conversationId,
loading: true,
error: null,
fetchSeq: base.fetchSeq + 1,
}
})
}, [conversationId, getCachedState])
useEffect(() => {
if (isVirtualId) return
// Skip fetch if cache already has data
if (detailCache.has(conversationId)) return
let cancelled = false
loadAndCacheDetail(conversationId)
.then((d) => {
if (!cancelled) {
setState((prev) =>
prev.key === conversationId
? { ...prev, detail: d, loading: false, error: null }
: {
key: conversationId,
detail: d,
loading: false,
error: null,
fetchSeq: 0,
}
)
}
})
.catch((e) => {
if (!cancelled) {
setState((prev) =>
prev.key === conversationId
? {
...prev,
error: e instanceof Error ? e.message : String(e),
loading: false,
}
: {
key: conversationId,
detail: null,
loading: false,
error: e instanceof Error ? e.message : String(e),
fetchSeq: 0,
}
)
}
})
return () => {
cancelled = true
}
}, [conversationId, derivedState.fetchSeq, isVirtualId])
return useMemo(
() => ({
detail: derivedState.detail,
loading: derivedState.loading,
error: derivedState.error,
refetch,
}),
[derivedState.detail, derivedState.loading, derivedState.error, refetch]
)
}

View File

@@ -1,60 +0,0 @@
"use client"
import { useEffect, useState } from "react"
import { getConversation } from "@/lib/tauri"
import type { AgentType, ConversationDetail } from "@/lib/types"
interface MessageDetailState {
key: string
detail: ConversationDetail | null
loading: boolean
error: string | null
}
function makeKey(agentType: AgentType, conversationId: string): string {
return `${agentType}:${conversationId}`
}
export function useMessageDetail(agentType: AgentType, conversationId: string) {
const key = makeKey(agentType, conversationId)
const [state, setState] = useState<MessageDetailState>({
key,
detail: null,
loading: true,
error: null,
})
// Reset when key changes (single setState instead of 4)
if (state.key !== key) {
setState({ key, detail: null, loading: true, error: null })
}
useEffect(() => {
let cancelled = false
getConversation(agentType, conversationId)
.then((d) => {
if (!cancelled) {
setState((prev) => ({ ...prev, detail: d, loading: false }))
}
})
.catch((e) => {
if (!cancelled) {
setState((prev) => ({
...prev,
error: e instanceof Error ? e.message : String(e),
loading: false,
}))
}
})
return () => {
cancelled = true
}
}, [agentType, conversationId])
return {
detail: state.detail,
loading: state.loading,
error: state.error,
}
}