mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
fix(export): prevent card stats poll overlap with frontend/backend singleflight
This commit is contained in:
@@ -276,6 +276,12 @@ class ChatService {
|
|||||||
private exportContentStatsRefreshPromise: Promise<void> | null = null
|
private exportContentStatsRefreshPromise: Promise<void> | null = null
|
||||||
private exportContentStatsRefreshQueued = false
|
private exportContentStatsRefreshQueued = false
|
||||||
private exportContentStatsRefreshForceQueued = false
|
private exportContentStatsRefreshForceQueued = false
|
||||||
|
private exportContentSessionCountsInFlight: {
|
||||||
|
promise: Promise<{ success: boolean; data?: ExportContentSessionCounts; error?: string }>
|
||||||
|
forceRefresh: boolean
|
||||||
|
traceId: string
|
||||||
|
startedAt: number
|
||||||
|
} | null = null
|
||||||
private exportContentStatsDirtySessionIds = new Set<string>()
|
private exportContentStatsDirtySessionIds = new Set<string>()
|
||||||
private exportContentScopeSessionIdsCache: { ids: string[]; updatedAt: number } | null = null
|
private exportContentScopeSessionIdsCache: { ids: string[]; updatedAt: number } | null = null
|
||||||
private readonly exportContentScopeSessionIdsCacheTtlMs = 60 * 1000
|
private readonly exportContentScopeSessionIdsCacheTtlMs = 60 * 1000
|
||||||
@@ -2124,170 +2130,214 @@ class ChatService {
|
|||||||
traceId?: string
|
traceId?: string
|
||||||
}): Promise<{ success: boolean; data?: ExportContentSessionCounts; error?: string }> {
|
}): Promise<{ success: boolean; data?: ExportContentSessionCounts; error?: string }> {
|
||||||
const traceId = this.normalizeExportDiagTraceId(options?.traceId)
|
const traceId = this.normalizeExportDiagTraceId(options?.traceId)
|
||||||
|
const forceRefresh = options?.forceRefresh === true
|
||||||
|
const triggerRefresh = options?.triggerRefresh !== false
|
||||||
const stepStartedAt = this.startExportDiagStep({
|
const stepStartedAt = this.startExportDiagStep({
|
||||||
traceId,
|
traceId,
|
||||||
stepId: 'backend-get-export-content-session-counts',
|
stepId: 'backend-get-export-content-session-counts',
|
||||||
stepName: '获取导出卡片统计',
|
stepName: '获取导出卡片统计',
|
||||||
message: '开始计算导出卡片统计',
|
message: '开始计算导出卡片统计',
|
||||||
data: {
|
data: {
|
||||||
triggerRefresh: options?.triggerRefresh !== false,
|
triggerRefresh,
|
||||||
forceRefresh: options?.forceRefresh === true
|
forceRefresh
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
let stepSuccess = false
|
let stepSuccess = false
|
||||||
let stepError = ''
|
let stepError = ''
|
||||||
let stepResult: ExportContentSessionCounts | undefined
|
let stepResult: ExportContentSessionCounts | undefined
|
||||||
|
let activePromise: Promise<{ success: boolean; data?: ExportContentSessionCounts; error?: string }> | null = null
|
||||||
|
let createdInFlight = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const connectResult = await this.ensureConnected()
|
if (this.exportContentSessionCountsInFlight) {
|
||||||
if (!connectResult.success) {
|
this.logExportDiag({
|
||||||
stepError = connectResult.error || '数据库未连接'
|
|
||||||
return { success: false, error: connectResult.error || '数据库未连接' }
|
|
||||||
}
|
|
||||||
this.refreshSessionMessageCountCacheScope()
|
|
||||||
|
|
||||||
const forceRefresh = options?.forceRefresh === true
|
|
||||||
const triggerRefresh = options?.triggerRefresh !== false
|
|
||||||
const sessionIds = await this.listExportContentScopeSessionIds(forceRefresh, traceId)
|
|
||||||
const sessionIdSet = new Set(sessionIds)
|
|
||||||
|
|
||||||
for (const sessionId of Array.from(this.exportContentStatsMemory.keys())) {
|
|
||||||
if (!sessionIdSet.has(sessionId)) {
|
|
||||||
this.exportContentStatsMemory.delete(sessionId)
|
|
||||||
this.exportContentStatsDirtySessionIds.delete(sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingTextCountSessionIds: string[] = []
|
|
||||||
let textSessions = 0
|
|
||||||
let voiceSessions = 0
|
|
||||||
let imageSessions = 0
|
|
||||||
let videoSessions = 0
|
|
||||||
let emojiSessions = 0
|
|
||||||
const pendingMediaSessionSet = new Set<string>()
|
|
||||||
|
|
||||||
for (const sessionId of sessionIds) {
|
|
||||||
const entry = this.exportContentStatsMemory.get(sessionId)
|
|
||||||
if (entry) {
|
|
||||||
if (entry.hasAny) {
|
|
||||||
textSessions += 1
|
|
||||||
} else if (forceRefresh || this.isExportContentEntryDirty(sessionId)) {
|
|
||||||
missingTextCountSessionIds.push(sessionId)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
missingTextCountSessionIds.push(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasMediaSnapshot = Boolean(entry && entry.mediaReady)
|
|
||||||
if (hasMediaSnapshot) {
|
|
||||||
if (entry!.hasVoice) voiceSessions += 1
|
|
||||||
if (entry!.hasImage) imageSessions += 1
|
|
||||||
if (entry!.hasVideo) videoSessions += 1
|
|
||||||
if (entry!.hasEmoji) emojiSessions += 1
|
|
||||||
} else {
|
|
||||||
pendingMediaSessionSet.add(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isExportContentEntryDirty(sessionId) && hasMediaSnapshot) {
|
|
||||||
pendingMediaSessionSet.add(sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingTextCountSessionIds.length > 0) {
|
|
||||||
const textCountStepStartedAt = this.startExportDiagStep({
|
|
||||||
traceId,
|
traceId,
|
||||||
stepId: 'backend-fill-text-counts',
|
source: 'backend',
|
||||||
stepName: '补全文本会话计数',
|
level: 'info',
|
||||||
message: '开始补全文本会话计数',
|
message: '复用进行中的导出卡片统计任务',
|
||||||
data: { missingSessions: missingTextCountSessionIds.length }
|
stepId: 'backend-get-export-content-session-counts',
|
||||||
|
stepName: '获取导出卡片统计',
|
||||||
|
status: 'running',
|
||||||
|
data: {
|
||||||
|
inFlightTraceId: this.exportContentSessionCountsInFlight.traceId,
|
||||||
|
inFlightForceRefresh: this.exportContentSessionCountsInFlight.forceRefresh,
|
||||||
|
requestedForceRefresh: forceRefresh,
|
||||||
|
inFlightElapsedMs: Date.now() - this.exportContentSessionCountsInFlight.startedAt
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const textCountStallTimer = setTimeout(() => {
|
activePromise = this.exportContentSessionCountsInFlight.promise
|
||||||
this.logExportDiag({
|
} else {
|
||||||
traceId,
|
const createdPromise = (async () => {
|
||||||
source: 'backend',
|
const connectResult = await this.ensureConnected()
|
||||||
level: 'warn',
|
if (!connectResult.success) {
|
||||||
message: '补全文本会话计数耗时较长',
|
return { success: false, error: connectResult.error || '数据库未连接' }
|
||||||
stepId: 'backend-fill-text-counts',
|
}
|
||||||
stepName: '补全文本会话计数',
|
this.refreshSessionMessageCountCacheScope()
|
||||||
status: 'running',
|
|
||||||
data: {
|
const sessionIds = await this.listExportContentScopeSessionIds(forceRefresh, traceId)
|
||||||
elapsedMs: Date.now() - textCountStepStartedAt,
|
const sessionIdSet = new Set(sessionIds)
|
||||||
missingSessions: missingTextCountSessionIds.length
|
|
||||||
}
|
for (const sessionId of Array.from(this.exportContentStatsMemory.keys())) {
|
||||||
})
|
if (!sessionIdSet.has(sessionId)) {
|
||||||
}, 3000)
|
this.exportContentStatsMemory.delete(sessionId)
|
||||||
const textCountResult = await this.getSessionMessageCounts(missingTextCountSessionIds, {
|
this.exportContentStatsDirtySessionIds.delete(sessionId)
|
||||||
preferHintCache: false,
|
|
||||||
bypassSessionCache: true,
|
|
||||||
traceId
|
|
||||||
})
|
|
||||||
clearTimeout(textCountStallTimer)
|
|
||||||
if (textCountResult.success && textCountResult.counts) {
|
|
||||||
const now = Date.now()
|
|
||||||
for (const sessionId of missingTextCountSessionIds) {
|
|
||||||
const count = textCountResult.counts[sessionId]
|
|
||||||
const hasAny = Number.isFinite(count) && Number(count) > 0
|
|
||||||
const prevEntry = this.exportContentStatsMemory.get(sessionId) || this.createDefaultExportContentEntry()
|
|
||||||
const nextEntry: ExportContentSessionStatsEntry = {
|
|
||||||
...prevEntry,
|
|
||||||
hasAny,
|
|
||||||
updatedAt: prevEntry.updatedAt || now
|
|
||||||
}
|
|
||||||
this.exportContentStatsMemory.set(sessionId, nextEntry)
|
|
||||||
if (hasAny) {
|
|
||||||
textSessions += 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.persistExportContentStatsScope(sessionIdSet)
|
|
||||||
this.endExportDiagStep({
|
const missingTextCountSessionIds: string[] = []
|
||||||
traceId,
|
let textSessions = 0
|
||||||
stepId: 'backend-fill-text-counts',
|
let voiceSessions = 0
|
||||||
stepName: '补全文本会话计数',
|
let imageSessions = 0
|
||||||
startedAt: textCountStepStartedAt,
|
let videoSessions = 0
|
||||||
|
let emojiSessions = 0
|
||||||
|
const pendingMediaSessionSet = new Set<string>()
|
||||||
|
|
||||||
|
for (const sessionId of sessionIds) {
|
||||||
|
const entry = this.exportContentStatsMemory.get(sessionId)
|
||||||
|
if (entry) {
|
||||||
|
if (entry.hasAny) {
|
||||||
|
textSessions += 1
|
||||||
|
} else if (forceRefresh || this.isExportContentEntryDirty(sessionId)) {
|
||||||
|
missingTextCountSessionIds.push(sessionId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
missingTextCountSessionIds.push(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMediaSnapshot = Boolean(entry && entry.mediaReady)
|
||||||
|
if (hasMediaSnapshot) {
|
||||||
|
if (entry.hasVoice) voiceSessions += 1
|
||||||
|
if (entry.hasImage) imageSessions += 1
|
||||||
|
if (entry.hasVideo) videoSessions += 1
|
||||||
|
if (entry.hasEmoji) emojiSessions += 1
|
||||||
|
} else {
|
||||||
|
pendingMediaSessionSet.add(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isExportContentEntryDirty(sessionId) && hasMediaSnapshot) {
|
||||||
|
pendingMediaSessionSet.add(sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingTextCountSessionIds.length > 0) {
|
||||||
|
const textCountStepStartedAt = this.startExportDiagStep({
|
||||||
|
traceId,
|
||||||
|
stepId: 'backend-fill-text-counts',
|
||||||
|
stepName: '补全文本会话计数',
|
||||||
|
message: '开始补全文本会话计数',
|
||||||
|
data: { missingSessions: missingTextCountSessionIds.length }
|
||||||
|
})
|
||||||
|
const textCountStallTimer = setTimeout(() => {
|
||||||
|
this.logExportDiag({
|
||||||
|
traceId,
|
||||||
|
source: 'backend',
|
||||||
|
level: 'warn',
|
||||||
|
message: '补全文本会话计数耗时较长',
|
||||||
|
stepId: 'backend-fill-text-counts',
|
||||||
|
stepName: '补全文本会话计数',
|
||||||
|
status: 'running',
|
||||||
|
data: {
|
||||||
|
elapsedMs: Date.now() - textCountStepStartedAt,
|
||||||
|
missingSessions: missingTextCountSessionIds.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
|
const textCountResult = await this.getSessionMessageCounts(missingTextCountSessionIds, {
|
||||||
|
preferHintCache: false,
|
||||||
|
bypassSessionCache: true,
|
||||||
|
traceId
|
||||||
|
})
|
||||||
|
clearTimeout(textCountStallTimer)
|
||||||
|
if (textCountResult.success && textCountResult.counts) {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const sessionId of missingTextCountSessionIds) {
|
||||||
|
const count = textCountResult.counts[sessionId]
|
||||||
|
const hasAny = Number.isFinite(count) && Number(count) > 0
|
||||||
|
const prevEntry = this.exportContentStatsMemory.get(sessionId) || this.createDefaultExportContentEntry()
|
||||||
|
const nextEntry: ExportContentSessionStatsEntry = {
|
||||||
|
...prevEntry,
|
||||||
|
hasAny,
|
||||||
|
updatedAt: prevEntry.updatedAt || now
|
||||||
|
}
|
||||||
|
this.exportContentStatsMemory.set(sessionId, nextEntry)
|
||||||
|
if (hasAny) {
|
||||||
|
textSessions += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.persistExportContentStatsScope(sessionIdSet)
|
||||||
|
this.endExportDiagStep({
|
||||||
|
traceId,
|
||||||
|
stepId: 'backend-fill-text-counts',
|
||||||
|
stepName: '补全文本会话计数',
|
||||||
|
startedAt: textCountStepStartedAt,
|
||||||
|
success: true,
|
||||||
|
message: '文本会话计数补全完成',
|
||||||
|
data: { updatedSessions: missingTextCountSessionIds.length }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.endExportDiagStep({
|
||||||
|
traceId,
|
||||||
|
stepId: 'backend-fill-text-counts',
|
||||||
|
stepName: '补全文本会话计数',
|
||||||
|
startedAt: textCountStepStartedAt,
|
||||||
|
success: false,
|
||||||
|
message: '文本会话计数补全失败',
|
||||||
|
data: { error: textCountResult.error || '未知错误' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceRefresh && triggerRefresh) {
|
||||||
|
void this.startExportContentStatsRefresh(true, traceId)
|
||||||
|
} else if (triggerRefresh && (pendingMediaSessionSet.size > 0 || this.exportContentStatsDirtySessionIds.size > 0)) {
|
||||||
|
void this.startExportContentStatsRefresh(false, traceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: '文本会话计数补全完成',
|
data: {
|
||||||
data: { updatedSessions: missingTextCountSessionIds.length }
|
totalSessions: sessionIds.length,
|
||||||
})
|
textSessions,
|
||||||
} else {
|
voiceSessions,
|
||||||
this.endExportDiagStep({
|
imageSessions,
|
||||||
traceId,
|
videoSessions,
|
||||||
stepId: 'backend-fill-text-counts',
|
emojiSessions,
|
||||||
stepName: '补全文本会话计数',
|
pendingMediaSessions: pendingMediaSessionSet.size,
|
||||||
startedAt: textCountStepStartedAt,
|
updatedAt: this.exportContentStatsScopeUpdatedAt,
|
||||||
success: false,
|
refreshing: this.exportContentStatsRefreshPromise !== null
|
||||||
message: '文本会话计数补全失败',
|
}
|
||||||
data: { error: textCountResult.error || '未知错误' }
|
}
|
||||||
})
|
})()
|
||||||
|
activePromise = createdPromise
|
||||||
|
this.exportContentSessionCountsInFlight = {
|
||||||
|
promise: createdPromise,
|
||||||
|
forceRefresh,
|
||||||
|
traceId,
|
||||||
|
startedAt: Date.now()
|
||||||
}
|
}
|
||||||
|
createdInFlight = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceRefresh && triggerRefresh) {
|
if (!activePromise) {
|
||||||
void this.startExportContentStatsRefresh(true, traceId)
|
stepError = '统计任务未初始化'
|
||||||
} else if (triggerRefresh && (pendingMediaSessionSet.size > 0 || this.exportContentStatsDirtySessionIds.size > 0)) {
|
return { success: false, error: stepError }
|
||||||
void this.startExportContentStatsRefresh(false, traceId)
|
|
||||||
}
|
}
|
||||||
|
const result = await activePromise
|
||||||
stepResult = {
|
stepSuccess = result.success
|
||||||
totalSessions: sessionIds.length,
|
if (result.success && result.data) {
|
||||||
textSessions,
|
stepResult = result.data
|
||||||
voiceSessions,
|
} else {
|
||||||
imageSessions,
|
stepError = result.error || '未知错误'
|
||||||
videoSessions,
|
|
||||||
emojiSessions,
|
|
||||||
pendingMediaSessions: pendingMediaSessionSet.size,
|
|
||||||
updatedAt: this.exportContentStatsScopeUpdatedAt,
|
|
||||||
refreshing: this.exportContentStatsRefreshPromise !== null
|
|
||||||
}
|
|
||||||
stepSuccess = true
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: stepResult
|
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ChatService: 获取导出内容会话统计失败:', e)
|
console.error('ChatService: 获取导出内容会话统计失败:', e)
|
||||||
stepError = String(e)
|
stepError = String(e)
|
||||||
return { success: false, error: String(e) }
|
return { success: false, error: String(e) }
|
||||||
} finally {
|
} finally {
|
||||||
|
if (createdInFlight && activePromise && this.exportContentSessionCountsInFlight?.promise === activePromise) {
|
||||||
|
this.exportContentSessionCountsInFlight = null
|
||||||
|
}
|
||||||
this.endExportDiagStep({
|
this.endExportDiagStep({
|
||||||
traceId,
|
traceId,
|
||||||
stepId: 'backend-get-export-content-session-counts',
|
stepId: 'backend-get-export-content-session-counts',
|
||||||
|
|||||||
@@ -971,6 +971,8 @@ function ExportPage() {
|
|||||||
const activeTaskCountRef = useRef(0)
|
const activeTaskCountRef = useRef(0)
|
||||||
const hasBaseConfigReadyRef = useRef(false)
|
const hasBaseConfigReadyRef = useRef(false)
|
||||||
const contentSessionCountsForceRetryRef = useRef(0)
|
const contentSessionCountsForceRetryRef = useRef(0)
|
||||||
|
const contentSessionCountsInFlightRef = useRef<Promise<void> | null>(null)
|
||||||
|
const contentSessionCountsInFlightTraceRef = useRef('')
|
||||||
|
|
||||||
const appendFrontendDiagLog = useCallback((entry: ExportCardDiagLogEntry) => {
|
const appendFrontendDiagLog = useCallback((entry: ExportCardDiagLogEntry) => {
|
||||||
setFrontendDiagLogs(prev => {
|
setFrontendDiagLogs(prev => {
|
||||||
@@ -1537,130 +1539,159 @@ function ExportPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadContentSessionCounts = useCallback(async (options?: { silent?: boolean; forceRefresh?: boolean }) => {
|
const loadContentSessionCounts = useCallback(async (options?: { silent?: boolean; forceRefresh?: boolean }) => {
|
||||||
|
if (contentSessionCountsInFlightRef.current) {
|
||||||
|
logFrontendDiag({
|
||||||
|
level: 'info',
|
||||||
|
stepId: 'frontend-load-content-session-counts',
|
||||||
|
stepName: '前端请求导出卡片统计',
|
||||||
|
status: 'running',
|
||||||
|
message: '统计请求仍在进行中,跳过本次轮询',
|
||||||
|
data: {
|
||||||
|
silent: options?.silent === true,
|
||||||
|
forceRefresh: options?.forceRefresh === true,
|
||||||
|
inFlightTraceId: contentSessionCountsInFlightTraceRef.current || undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const traceId = createExportDiagTraceId()
|
const traceId = createExportDiagTraceId()
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
logFrontendDiag({
|
const task = (async () => {
|
||||||
traceId,
|
logFrontendDiag({
|
||||||
stepId: 'frontend-load-content-session-counts',
|
traceId,
|
||||||
stepName: '前端请求导出卡片统计',
|
stepId: 'frontend-load-content-session-counts',
|
||||||
status: 'running',
|
stepName: '前端请求导出卡片统计',
|
||||||
message: '开始请求导出卡片统计',
|
status: 'running',
|
||||||
data: {
|
message: '开始请求导出卡片统计',
|
||||||
silent: options?.silent === true,
|
data: {
|
||||||
forceRefresh: options?.forceRefresh === true
|
silent: options?.silent === true,
|
||||||
}
|
forceRefresh: options?.forceRefresh === true
|
||||||
})
|
|
||||||
try {
|
|
||||||
const result = await withTimeout(
|
|
||||||
window.electronAPI.chat.getExportContentSessionCounts({
|
|
||||||
triggerRefresh: true,
|
|
||||||
forceRefresh: options?.forceRefresh === true,
|
|
||||||
traceId
|
|
||||||
}),
|
|
||||||
3200
|
|
||||||
)
|
|
||||||
if (!result) {
|
|
||||||
logFrontendDiag({
|
|
||||||
traceId,
|
|
||||||
level: 'warn',
|
|
||||||
stepId: 'frontend-load-content-session-counts',
|
|
||||||
stepName: '前端请求导出卡片统计',
|
|
||||||
status: 'timeout',
|
|
||||||
durationMs: Date.now() - startedAt,
|
|
||||||
message: '导出卡片统计请求超时(3200ms)'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (result?.success && result.data) {
|
|
||||||
const next: ExportContentSessionCountsSummary = {
|
|
||||||
totalSessions: Number.isFinite(result.data.totalSessions) ? Math.max(0, Math.floor(result.data.totalSessions)) : 0,
|
|
||||||
textSessions: Number.isFinite(result.data.textSessions) ? Math.max(0, Math.floor(result.data.textSessions)) : 0,
|
|
||||||
voiceSessions: Number.isFinite(result.data.voiceSessions) ? Math.max(0, Math.floor(result.data.voiceSessions)) : 0,
|
|
||||||
imageSessions: Number.isFinite(result.data.imageSessions) ? Math.max(0, Math.floor(result.data.imageSessions)) : 0,
|
|
||||||
videoSessions: Number.isFinite(result.data.videoSessions) ? Math.max(0, Math.floor(result.data.videoSessions)) : 0,
|
|
||||||
emojiSessions: Number.isFinite(result.data.emojiSessions) ? Math.max(0, Math.floor(result.data.emojiSessions)) : 0,
|
|
||||||
pendingMediaSessions: Number.isFinite(result.data.pendingMediaSessions) ? Math.max(0, Math.floor(result.data.pendingMediaSessions)) : 0,
|
|
||||||
updatedAt: Number.isFinite(result.data.updatedAt) ? Math.max(0, Math.floor(result.data.updatedAt)) : 0,
|
|
||||||
refreshing: result.data.refreshing === true
|
|
||||||
}
|
}
|
||||||
setContentSessionCounts(next)
|
})
|
||||||
const looksLikeAllZero = next.totalSessions > 0 &&
|
try {
|
||||||
next.textSessions === 0 &&
|
const result = await withTimeout(
|
||||||
next.voiceSessions === 0 &&
|
window.electronAPI.chat.getExportContentSessionCounts({
|
||||||
next.imageSessions === 0 &&
|
triggerRefresh: true,
|
||||||
next.videoSessions === 0 &&
|
forceRefresh: options?.forceRefresh === true,
|
||||||
next.emojiSessions === 0
|
traceId
|
||||||
|
}),
|
||||||
if (looksLikeAllZero && contentSessionCountsForceRetryRef.current < 3) {
|
3200
|
||||||
contentSessionCountsForceRetryRef.current += 1
|
)
|
||||||
const refreshTraceId = createExportDiagTraceId()
|
if (!result) {
|
||||||
logFrontendDiag({
|
logFrontendDiag({
|
||||||
traceId: refreshTraceId,
|
traceId,
|
||||||
stepId: 'frontend-force-refresh-content-session-counts',
|
level: 'warn',
|
||||||
stepName: '前端触发强制刷新导出卡片统计',
|
stepId: 'frontend-load-content-session-counts',
|
||||||
status: 'running',
|
stepName: '前端请求导出卡片统计',
|
||||||
message: '检测到统计全0,触发强制刷新'
|
status: 'timeout',
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
message: '导出卡片统计请求超时(3200ms,后台可能仍在处理)'
|
||||||
})
|
})
|
||||||
void window.electronAPI.chat.refreshExportContentSessionCounts({ forceRefresh: true, traceId: refreshTraceId }).then((refreshResult) => {
|
return
|
||||||
|
}
|
||||||
|
if (result?.success && result.data) {
|
||||||
|
const next: ExportContentSessionCountsSummary = {
|
||||||
|
totalSessions: Number.isFinite(result.data.totalSessions) ? Math.max(0, Math.floor(result.data.totalSessions)) : 0,
|
||||||
|
textSessions: Number.isFinite(result.data.textSessions) ? Math.max(0, Math.floor(result.data.textSessions)) : 0,
|
||||||
|
voiceSessions: Number.isFinite(result.data.voiceSessions) ? Math.max(0, Math.floor(result.data.voiceSessions)) : 0,
|
||||||
|
imageSessions: Number.isFinite(result.data.imageSessions) ? Math.max(0, Math.floor(result.data.imageSessions)) : 0,
|
||||||
|
videoSessions: Number.isFinite(result.data.videoSessions) ? Math.max(0, Math.floor(result.data.videoSessions)) : 0,
|
||||||
|
emojiSessions: Number.isFinite(result.data.emojiSessions) ? Math.max(0, Math.floor(result.data.emojiSessions)) : 0,
|
||||||
|
pendingMediaSessions: Number.isFinite(result.data.pendingMediaSessions) ? Math.max(0, Math.floor(result.data.pendingMediaSessions)) : 0,
|
||||||
|
updatedAt: Number.isFinite(result.data.updatedAt) ? Math.max(0, Math.floor(result.data.updatedAt)) : 0,
|
||||||
|
refreshing: result.data.refreshing === true
|
||||||
|
}
|
||||||
|
setContentSessionCounts(next)
|
||||||
|
const looksLikeAllZero = next.totalSessions > 0 &&
|
||||||
|
next.textSessions === 0 &&
|
||||||
|
next.voiceSessions === 0 &&
|
||||||
|
next.imageSessions === 0 &&
|
||||||
|
next.videoSessions === 0 &&
|
||||||
|
next.emojiSessions === 0
|
||||||
|
|
||||||
|
if (looksLikeAllZero && contentSessionCountsForceRetryRef.current < 3) {
|
||||||
|
contentSessionCountsForceRetryRef.current += 1
|
||||||
|
const refreshTraceId = createExportDiagTraceId()
|
||||||
logFrontendDiag({
|
logFrontendDiag({
|
||||||
traceId: refreshTraceId,
|
traceId: refreshTraceId,
|
||||||
stepId: 'frontend-force-refresh-content-session-counts',
|
stepId: 'frontend-force-refresh-content-session-counts',
|
||||||
stepName: '前端触发强制刷新导出卡片统计',
|
stepName: '前端触发强制刷新导出卡片统计',
|
||||||
status: refreshResult?.success ? 'done' : 'failed',
|
status: 'running',
|
||||||
level: refreshResult?.success ? 'info' : 'warn',
|
message: '检测到统计全0,触发强制刷新'
|
||||||
message: refreshResult?.success ? '强制刷新请求已提交' : `强制刷新失败:${refreshResult?.error || '未知错误'}`
|
|
||||||
})
|
})
|
||||||
}).catch((error) => {
|
void window.electronAPI.chat.refreshExportContentSessionCounts({ forceRefresh: true, traceId: refreshTraceId }).then((refreshResult) => {
|
||||||
logFrontendDiag({
|
logFrontendDiag({
|
||||||
traceId: refreshTraceId,
|
traceId: refreshTraceId,
|
||||||
stepId: 'frontend-force-refresh-content-session-counts',
|
stepId: 'frontend-force-refresh-content-session-counts',
|
||||||
stepName: '前端触发强制刷新导出卡片统计',
|
stepName: '前端触发强制刷新导出卡片统计',
|
||||||
status: 'failed',
|
status: refreshResult?.success ? 'done' : 'failed',
|
||||||
level: 'error',
|
level: refreshResult?.success ? 'info' : 'warn',
|
||||||
message: '强制刷新请求异常',
|
message: refreshResult?.success ? '强制刷新请求已提交' : `强制刷新失败:${refreshResult?.error || '未知错误'}`
|
||||||
data: { error: String(error) }
|
})
|
||||||
|
}).catch((error) => {
|
||||||
|
logFrontendDiag({
|
||||||
|
traceId: refreshTraceId,
|
||||||
|
stepId: 'frontend-force-refresh-content-session-counts',
|
||||||
|
stepName: '前端触发强制刷新导出卡片统计',
|
||||||
|
status: 'failed',
|
||||||
|
level: 'error',
|
||||||
|
message: '强制刷新请求异常',
|
||||||
|
data: { error: String(error) }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
contentSessionCountsForceRetryRef.current = 0
|
||||||
|
setHasSeededContentSessionCounts(true)
|
||||||
|
}
|
||||||
|
logFrontendDiag({
|
||||||
|
traceId,
|
||||||
|
stepId: 'frontend-load-content-session-counts',
|
||||||
|
stepName: '前端请求导出卡片统计',
|
||||||
|
status: 'done',
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
message: '导出卡片统计请求完成',
|
||||||
|
data: {
|
||||||
|
totalSessions: next.totalSessions,
|
||||||
|
pendingMediaSessions: next.pendingMediaSessions,
|
||||||
|
refreshing: next.refreshing
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
contentSessionCountsForceRetryRef.current = 0
|
logFrontendDiag({
|
||||||
setHasSeededContentSessionCounts(true)
|
traceId,
|
||||||
|
level: 'warn',
|
||||||
|
stepId: 'frontend-load-content-session-counts',
|
||||||
|
stepName: '前端请求导出卡片统计',
|
||||||
|
status: 'failed',
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
message: `导出卡片统计请求失败:${result?.error || '未知错误'}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载导出内容会话统计失败:', error)
|
||||||
logFrontendDiag({
|
logFrontendDiag({
|
||||||
traceId,
|
traceId,
|
||||||
stepId: 'frontend-load-content-session-counts',
|
level: 'error',
|
||||||
stepName: '前端请求导出卡片统计',
|
|
||||||
status: 'done',
|
|
||||||
durationMs: Date.now() - startedAt,
|
|
||||||
message: '导出卡片统计请求完成',
|
|
||||||
data: {
|
|
||||||
totalSessions: next.totalSessions,
|
|
||||||
pendingMediaSessions: next.pendingMediaSessions,
|
|
||||||
refreshing: next.refreshing
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
logFrontendDiag({
|
|
||||||
traceId,
|
|
||||||
level: 'warn',
|
|
||||||
stepId: 'frontend-load-content-session-counts',
|
stepId: 'frontend-load-content-session-counts',
|
||||||
stepName: '前端请求导出卡片统计',
|
stepName: '前端请求导出卡片统计',
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
message: `导出卡片统计请求失败:${result?.error || '未知错误'}`
|
message: '导出卡片统计请求异常',
|
||||||
|
data: { error: String(error) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
})()
|
||||||
console.error('加载导出内容会话统计失败:', error)
|
|
||||||
logFrontendDiag({
|
contentSessionCountsInFlightRef.current = task
|
||||||
traceId,
|
contentSessionCountsInFlightTraceRef.current = traceId
|
||||||
level: 'error',
|
try {
|
||||||
stepId: 'frontend-load-content-session-counts',
|
await task
|
||||||
stepName: '前端请求导出卡片统计',
|
} finally {
|
||||||
status: 'failed',
|
if (contentSessionCountsInFlightRef.current === task) {
|
||||||
durationMs: Date.now() - startedAt,
|
contentSessionCountsInFlightRef.current = null
|
||||||
message: '导出卡片统计请求异常',
|
contentSessionCountsInFlightTraceRef.current = ''
|
||||||
data: { error: String(error) }
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [logFrontendDiag])
|
}, [logFrontendDiag])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user