This commit is contained in:
2026-10-24 16:34:02 +08:00
parent 99e5687ce0
commit 7119e04a65
24 changed files with 553 additions and 39 deletions
+65 -10
View File
@@ -698,6 +698,9 @@ export const useChatStore = defineStore('chat', () => {
const isLoadingSessions = ref(false)
const sessionsLoaded = ref(false)
const isLoadingMessages = ref(false)
const messageLoadFailed = ref(false)
let sessionLoadGeneration = 0
let sessionResumeAbort: AbortController | null = null
const isRunActive = computed(() => isStreaming.value)
async function fetchRuntimeSessions(profile?: string | null): Promise<SessionSummary[]> {
@@ -805,6 +808,10 @@ export const useChatStore = defineStore('chat', () => {
}
function clearActiveSession() {
sessionLoadGeneration++
sessionResumeAbort?.abort()
isLoadingMessages.value = false
messageLoadFailed.value = false
const sid = activeSessionId.value
activeSessionId.value = null
activeSession.value = null
@@ -1082,8 +1089,9 @@ export const useChatStore = defineStore('chat', () => {
}
// Re-pull active session from server. Used on tab-visible events.
async function refreshActiveSession(): Promise<boolean> {
async function refreshActiveSession(isCurrent: () => boolean = () => true): Promise<boolean> {
const sid = activeSessionId.value
const generation = sessionLoadGeneration
if (!sid) return false
try {
const target = sessions.value.find(s => s.id === sid)
@@ -1093,7 +1101,7 @@ export const useChatStore = defineStore('chat', () => {
LIVE_CHAT_MAX_LOADED_MESSAGES,
)
const detail = await fetchSessionMessagesPage(sid, 0, limit, activeSession.value?.profile)
if (!detail) return false
if (!detail || !isCurrent() || generation !== sessionLoadGeneration || activeSessionId.value !== sid) return false
const mapped = mapHermesMessages(detail.messages || [])
target.messages = mapped
target.loadedMessageCount = detail.messages.length
@@ -1176,6 +1184,12 @@ export const useChatStore = defineStore('chat', () => {
}
async function switchSession(sessionId: string, focusId?: string | null) {
const generation = ++sessionLoadGeneration
sessionResumeAbort?.abort()
const resumeAbort = new AbortController()
sessionResumeAbort = resumeAbort
messageLoadFailed.value = false
isLoadingMessages.value = false
clearThinkingObservationFor(sessionId)
activeSessionId.value = sessionId
focusMessageId.value = focusId ?? null
@@ -1188,17 +1202,43 @@ export const useChatStore = defineStore('chat', () => {
if (!activeSession.value) return
isLoadingMessages.value = true
let awaitingRuntime = false
let runtimeReceived = false
let resumeTimeout: ReturnType<typeof setTimeout> | undefined
let httpHistoryLoaded = false
let runtimeExpiry: ReturnType<typeof setTimeout> | undefined
resumeAbort.signal.addEventListener('abort', () => clearTimeout(runtimeExpiry), { once: true })
try {
// Load messages via Socket.IO resume (server loads from DB if not in memory)
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('resume timeout')), 15_000)
resumeSession(sessionId, (data) => {
let settled = false
const cancel = () => {
if (settled) return
settled = true
clearTimeout(timeout)
if (data.session_id !== sessionId || activeSessionId.value !== sessionId) {
resolve()
}
const timeout = setTimeout(() => {
settled = true
// Recover history now, while allowing a late resume to restore live run state.
awaitingRuntime = true
runtimeExpiry = setTimeout(() => resumeAbort.abort(), 12_000)
reject(new Error('resume timeout'))
}, 3000)
resumeTimeout = timeout
resumeAbort.signal.addEventListener('abort', cancel, { once: true })
resumeSession(sessionId, (data) => {
if (settled && !awaitingRuntime) return
awaitingRuntime = false
settled = true
clearTimeout(timeout)
clearTimeout(runtimeExpiry)
if (generation !== sessionLoadGeneration || data.session_id !== sessionId || activeSessionId.value !== sessionId) {
resolve()
return
}
runtimeReceived = true
const target = sessions.value.find(s => s.id === sessionId)
if (!target) {
resolve()
@@ -1237,7 +1277,7 @@ export const useChatStore = defineStore('chat', () => {
target.parentTitle = (data as any).parentTitle || target.parentTitle || null
target.parentLastMessage = (data as any).parentLastMessage || target.parentLastMessage || null
target.parentLastMessageRole = (data as any).parentLastMessageRole || target.parentLastMessageRole || null
if (data.messages?.length) {
if (!httpHistoryLoaded && data.messages?.length) {
target.messages = mapHermesMessages(data.messages as any[])
restoreWorkspaceRunChangeMessages(sessionId)
target.loadedMessageCount = data.messageLoadedCount ?? data.messages.length
@@ -1253,6 +1293,7 @@ export const useChatStore = defineStore('chat', () => {
}
}
activeSession.value = target
messageLoadFailed.value = false
// Process replayed events (compression state etc.)
if (data.events?.length) {
for (const evt of data.events) {
@@ -1346,20 +1387,31 @@ export const useChatStore = defineStore('chat', () => {
}
}
}
resumeServerWorkingRun(sessionId)
resumeAbort.abort()
resolve()
}, activeSession.value?.profile, runtimeTransport())
}, activeSession.value?.profile, runtimeTransport(), resumeAbort.signal)
})
if (activeSessionId.value === sessionId) {
if (generation === sessionLoadGeneration && activeSessionId.value === sessionId) {
await loadWorkspaceRunChangesForSession(sessionId)
}
} catch (err) {
clearTimeout(resumeTimeout)
console.error('Failed to load session messages via resume:', err)
if (generation === sessionLoadGeneration && activeSessionId.value === sessionId) {
const loaded = await refreshActiveSession(() => !runtimeReceived)
if (generation === sessionLoadGeneration && !runtimeReceived) {
httpHistoryLoaded = loaded
messageLoadFailed.value = !loaded
}
}
} finally {
isLoadingMessages.value = false
if (!awaitingRuntime) resumeAbort.abort()
if (generation === sessionLoadGeneration) isLoadingMessages.value = false
}
// Resume in-flight run event listeners if needed
if (activeSessionId.value === sessionId) {
if (generation === sessionLoadGeneration && activeSessionId.value === sessionId) {
resumeServerWorkingRun(sessionId)
}
}
@@ -3732,7 +3784,9 @@ export const useChatStore = defineStore('chat', () => {
const sid = activeSessionId.value
if (sid && !streamStates.value.has(sid)) {
// Re-load messages via resume (server loads from DB)
const generation = sessionLoadGeneration
resumeSession(sid, (data) => {
if (generation !== sessionLoadGeneration || activeSessionId.value !== sid) return
if (data.isWorking) {
serverWorking.value.add(sid)
} else {
@@ -3904,6 +3958,7 @@ export const useChatStore = defineStore('chat', () => {
isLoadingSessions,
sessionsLoaded,
isLoadingMessages,
messageLoadFailed,
newChat,
newCliSession,
@@ -15,7 +15,15 @@ export const useProfilesStore = defineStore('profiles', () => {
const loading = ref(false)
const switching = ref(false)
async function fetchProfiles() {
let profilesLoadPromise: Promise<void> | null = null
function fetchProfiles(): Promise<void> {
if (profilesLoadPromise) return profilesLoadPromise
profilesLoadPromise = loadProfiles().finally(() => { profilesLoadPromise = null })
return profilesLoadPromise
}
async function loadProfiles() {
loading.value = true
try {
profiles.value = await profilesApi.fetchProfiles()
@@ -66,7 +66,14 @@ export const useSettingsStore = defineStore('settings', () => {
const weixin = ref<Record<string, any>>({})
const platforms = ref<Record<string, any>>({})
async function fetchSettings() {
let settingsLoadPromise: Promise<void> | null = null
function fetchSettings(): Promise<void> {
if (settingsLoadPromise) return settingsLoadPromise
settingsLoadPromise = loadSettings().finally(() => { settingsLoadPromise = null })
return settingsLoadPromise
}
async function loadSettings() {
loading.value = true
try {
const data = await configApi.fetchConfig()