图片解密再次优化

This commit is contained in:
cc
2026-04-15 23:57:33 +08:00
parent 419a53d6ec
commit ab1d64e0c9
20 changed files with 1504 additions and 422 deletions

View File

@@ -22,6 +22,8 @@ import {
MessageSquare,
MessageSquareText,
Mic,
Pause,
Play,
RefreshCw,
Search,
Square,
@@ -48,6 +50,8 @@ import {
import {
requestCancelBackgroundTask,
requestCancelBackgroundTasks,
requestPauseBackgroundTask,
requestResumeBackgroundTask,
subscribeBackgroundTasks
} from '../services/backgroundTaskMonitor'
import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
@@ -208,6 +212,8 @@ interface AutomationTaskDraft {
dateRangeConfig: ExportAutomationDateRangeConfig | string | null
intervalDays: number
intervalHours: number
firstTriggerAtEnabled: boolean
firstTriggerAtValue: string
stopAtEnabled: boolean
stopAtValue: string
maxRunsEnabled: boolean
@@ -217,6 +223,7 @@ interface AutomationTaskDraft {
const defaultTxtColumns = ['index', 'time', 'senderRole', 'messageType', 'content']
const DETAIL_PRECISE_REFRESH_COOLDOWN_MS = 10 * 60 * 1000
const TASK_PERFORMANCE_UPDATE_MIN_INTERVAL_MS = 900
const EXPORT_PROGRESS_UI_FLUSH_INTERVAL_MS = 320
const SESSION_MEDIA_METRIC_PREFETCH_ROWS = 10
const SESSION_MEDIA_METRIC_BATCH_SIZE = 8
const SESSION_MEDIA_METRIC_BACKGROUND_FEED_SIZE = 48
@@ -248,6 +255,8 @@ const backgroundTaskSourceLabels: Record<string, string> = {
const backgroundTaskStatusLabels: Record<BackgroundTaskRecord['status'], string> = {
running: '运行中',
pause_requested: '中断中',
paused: '已中断',
cancel_requested: '停止中',
completed: '已完成',
failed: '失败',
@@ -321,6 +330,69 @@ const createEmptyProgress = (): TaskProgress => ({
mediaBytesWritten: 0
})
const areStringArraysEqual = (left: string[], right: string[]): boolean => {
if (left === right) return true
if (left.length !== right.length) return false
for (let index = 0; index < left.length; index += 1) {
if (left[index] !== right[index]) return false
}
return true
}
const areTaskProgressEqual = (left: TaskProgress, right: TaskProgress): boolean => (
left.current === right.current &&
left.total === right.total &&
left.currentName === right.currentName &&
left.phase === right.phase &&
left.phaseLabel === right.phaseLabel &&
left.phaseProgress === right.phaseProgress &&
left.phaseTotal === right.phaseTotal &&
left.exportedMessages === right.exportedMessages &&
left.estimatedTotalMessages === right.estimatedTotalMessages &&
left.collectedMessages === right.collectedMessages &&
left.writtenFiles === right.writtenFiles &&
left.mediaDoneFiles === right.mediaDoneFiles &&
left.mediaCacheHitFiles === right.mediaCacheHitFiles &&
left.mediaCacheMissFiles === right.mediaCacheMissFiles &&
left.mediaCacheFillFiles === right.mediaCacheFillFiles &&
left.mediaDedupReuseFiles === right.mediaDedupReuseFiles &&
left.mediaBytesWritten === right.mediaBytesWritten
)
const normalizeProgressFloat = (value: unknown, digits = 3): number => {
const parsed = Number(value)
if (!Number.isFinite(parsed)) return 0
const factor = 10 ** digits
return Math.round(parsed * factor) / factor
}
const normalizeProgressInt = (value: unknown): number => {
const parsed = Number(value)
if (!Number.isFinite(parsed)) return 0
return Math.max(0, Math.floor(parsed))
}
const buildProgressPayloadSignature = (payload: ExportProgress): string => ([
String(payload.phase || ''),
String(payload.currentSessionId || ''),
String(payload.currentSession || ''),
String(payload.phaseLabel || ''),
normalizeProgressFloat(payload.current, 4),
normalizeProgressFloat(payload.total, 4),
normalizeProgressFloat(payload.phaseProgress, 2),
normalizeProgressFloat(payload.phaseTotal, 2),
normalizeProgressInt(payload.collectedMessages),
normalizeProgressInt(payload.exportedMessages),
normalizeProgressInt(payload.estimatedTotalMessages),
normalizeProgressInt(payload.writtenFiles),
normalizeProgressInt(payload.mediaDoneFiles),
normalizeProgressInt(payload.mediaCacheHitFiles),
normalizeProgressInt(payload.mediaCacheMissFiles),
normalizeProgressInt(payload.mediaCacheFillFiles),
normalizeProgressInt(payload.mediaDedupReuseFiles),
normalizeProgressInt(payload.mediaBytesWritten)
].join('|'))
const createEmptyTaskPerformance = (): TaskPerformance => ({
stages: {
collect: 0,
@@ -508,6 +580,35 @@ const getTaskStatusLabel = (task: ExportTask): string => {
return '失败'
}
const resolveBackgroundTaskCardClass = (status: BackgroundTaskRecord['status']): 'running' | 'paused' | 'stopped' | 'success' | 'error' => {
if (status === 'running') return 'running'
if (status === 'pause_requested' || status === 'paused') return 'paused'
if (status === 'cancel_requested' || status === 'canceled') return 'stopped'
if (status === 'completed') return 'success'
return 'error'
}
const parseBackgroundTaskProgress = (progressText?: string): { current: number; total: number; ratio: number | null } => {
const normalized = String(progressText || '').trim()
if (!normalized) {
return { current: 0, total: 0, ratio: null }
}
const match = normalized.match(/(\d+)\s*\/\s*(\d+)/)
if (!match) {
return { current: 0, total: 0, ratio: null }
}
const current = Math.max(0, Math.floor(Number(match[1]) || 0))
const total = Math.max(0, Math.floor(Number(match[2]) || 0))
if (total <= 0) {
return { current, total, ratio: null }
}
return {
current,
total,
ratio: Math.max(0, Math.min(1, current / total))
}
}
const formatAbsoluteDate = (timestamp: number): string => {
const d = new Date(timestamp)
const y = d.getFullYear()
@@ -643,6 +744,11 @@ type ContactsDataSource = 'cache' | 'network' | null
const normalizeAutomationIntervalDays = (value: unknown): number => Math.max(0, Math.floor(Number(value) || 0))
const normalizeAutomationIntervalHours = (value: unknown): number => Math.max(0, Math.min(23, Math.floor(Number(value) || 0)))
const normalizeAutomationFirstTriggerAt = (value: unknown): number => {
const numeric = Math.floor(Number(value) || 0)
if (!Number.isFinite(numeric) || numeric <= 0) return 0
return numeric
}
const resolveAutomationIntervalMs = (schedule: ExportAutomationSchedule): number => {
const days = normalizeAutomationIntervalDays(schedule.intervalDays)
@@ -652,6 +758,16 @@ const resolveAutomationIntervalMs = (schedule: ExportAutomationSchedule): number
return totalHours * 60 * 60 * 1000
}
const resolveAutomationInitialTriggerAt = (task: ExportAutomationTask): number | null => {
const intervalMs = resolveAutomationIntervalMs(task.schedule)
if (intervalMs <= 0) return null
const firstTriggerAt = normalizeAutomationFirstTriggerAt(task.schedule.firstTriggerAt)
if (firstTriggerAt > 0) return firstTriggerAt
const createdAt = Math.max(0, Math.floor(Number(task.createdAt || 0)))
if (!createdAt) return null
return createdAt + intervalMs
}
const formatAutomationScheduleLabel = (schedule: ExportAutomationSchedule): string => {
const days = normalizeAutomationIntervalDays(schedule.intervalDays)
const hours = normalizeAutomationIntervalHours(schedule.intervalHours)
@@ -665,12 +781,60 @@ const resolveAutomationDueScheduleKey = (task: ExportAutomationTask, now: Date):
const intervalMs = resolveAutomationIntervalMs(task.schedule)
if (intervalMs <= 0) return null
const nowMs = now.getTime()
const anchorAt = Math.max(
0,
Number(task.runState?.lastTriggeredAt || 0) || Number(task.createdAt || 0)
)
if (nowMs < anchorAt + intervalMs) return null
return `interval:${anchorAt}:${Math.floor((nowMs - anchorAt) / intervalMs)}`
const lastTriggeredAt = Math.max(0, Math.floor(Number(task.runState?.lastTriggeredAt || 0)))
if (lastTriggeredAt > 0) {
if (nowMs < lastTriggeredAt + intervalMs) return null
return `interval:${lastTriggeredAt}:${Math.floor((nowMs - lastTriggeredAt) / intervalMs)}`
}
const initialTriggerAt = resolveAutomationInitialTriggerAt(task)
if (!initialTriggerAt) return null
if (nowMs < initialTriggerAt) return null
return `first:${initialTriggerAt}`
}
const resolveAutomationFirstTriggerSummary = (task: ExportAutomationTask): string => {
const firstTriggerAt = normalizeAutomationFirstTriggerAt(task.schedule.firstTriggerAt)
if (firstTriggerAt <= 0) return '未指定(默认按创建时间+间隔)'
return new Date(firstTriggerAt).toLocaleString('zh-CN')
}
const buildAutomationSchedule = (
intervalDays: number,
intervalHours: number,
firstTriggerAt: number
): ExportAutomationSchedule => ({
type: 'interval',
intervalDays,
intervalHours,
firstTriggerAt: firstTriggerAt > 0 ? firstTriggerAt : undefined
})
const buildAutomationDatePart = (timestamp: number): string => {
const date = new Date(timestamp)
if (Number.isNaN(date.getTime())) return ''
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}-${month}-${day}`
}
const buildAutomationTodayDatePart = (): string => buildAutomationDatePart(Date.now())
const normalizeAutomationDatePart = (value: string): string => {
const text = String(value || '').trim()
return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : ''
}
const normalizeAutomationTimePart = (value: string): string => {
const text = String(value || '').trim()
if (!/^\d{2}:\d{2}$/.test(text)) return '00:00'
const [hoursText, minutesText] = text.split(':')
const hours = Math.floor(Number(hoursText))
const minutes = Math.floor(Number(minutesText))
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return '00:00'
const safeHours = Math.min(23, Math.max(0, hours))
const safeMinutes = Math.min(59, Math.max(0, minutes))
return `${`${safeHours}`.padStart(2, '0')}:${`${safeMinutes}`.padStart(2, '0')}`
}
const toDateTimeLocalValue = (timestamp: number): string => {
@@ -811,9 +975,9 @@ const formatAutomationStopCondition = (task: ExportAutomationTask): string => {
const resolveAutomationNextTriggerAt = (task: ExportAutomationTask): number | null => {
const intervalMs = resolveAutomationIntervalMs(task.schedule)
if (intervalMs <= 0) return null
const anchorAt = Math.max(0, Number(task.runState?.lastTriggeredAt || 0) || Number(task.createdAt || 0))
if (!anchorAt) return null
return anchorAt + intervalMs
const lastTriggeredAt = Math.max(0, Math.floor(Number(task.runState?.lastTriggeredAt || 0)))
if (lastTriggeredAt > 0) return lastTriggeredAt + intervalMs
return resolveAutomationInitialTriggerAt(task)
}
const formatAutomationCurrentState = (
@@ -1597,25 +1761,40 @@ const SectionInfoTooltip = memo(function SectionInfoTooltip({
interface TaskCenterModalProps {
isOpen: boolean
tasks: ExportTask[]
chatBackgroundTasks: BackgroundTaskRecord[]
taskRunningCount: number
taskQueuedCount: number
expandedPerfTaskId: string | null
nowTick: number
onClose: () => void
onTogglePerfTask: (taskId: string) => void
onPauseBackgroundTask: (taskId: string) => void
onResumeBackgroundTask: (taskId: string) => void
onCancelBackgroundTask: (taskId: string) => void
}
const TaskCenterModal = memo(function TaskCenterModal({
isOpen,
tasks,
chatBackgroundTasks,
taskRunningCount,
taskQueuedCount,
expandedPerfTaskId,
nowTick,
onClose,
onTogglePerfTask
onTogglePerfTask,
onPauseBackgroundTask,
onResumeBackgroundTask,
onCancelBackgroundTask
}: TaskCenterModalProps) {
if (!isOpen) return null
const chatActiveTaskCount = chatBackgroundTasks.filter(task => (
task.status === 'running' ||
task.status === 'pause_requested' ||
task.status === 'paused' ||
task.status === 'cancel_requested'
)).length
const totalTaskCount = tasks.length + chatBackgroundTasks.length
return createPortal(
<div
@@ -1632,7 +1811,7 @@ const TaskCenterModal = memo(function TaskCenterModal({
<div className="task-center-modal-header">
<div className="task-center-modal-title">
<h3></h3>
<span> {taskRunningCount} · {taskQueuedCount} · {tasks.length}</span>
<span> {taskRunningCount} · {taskQueuedCount} · {chatActiveTaskCount} · {totalTaskCount}</span>
</div>
<button
className="close-icon-btn"
@@ -1644,8 +1823,8 @@ const TaskCenterModal = memo(function TaskCenterModal({
</button>
</div>
<div className="task-center-modal-body">
{tasks.length === 0 ? (
<div className="task-empty"></div>
{totalTaskCount === 0 ? (
<div className="task-empty">/</div>
) : (
<div className="task-list">
{tasks.map(task => {
@@ -1833,6 +2012,70 @@ const TaskCenterModal = memo(function TaskCenterModal({
</div>
)
})}
{chatBackgroundTasks.map(task => {
const taskCardClass = resolveBackgroundTaskCardClass(task.status)
const progress = parseBackgroundTaskProgress(task.progressText)
const canPause = task.resumable && task.status === 'running'
const canResume = task.resumable && (task.status === 'paused' || task.status === 'pause_requested')
const canCancel = task.cancelable && (
task.status === 'running' ||
task.status === 'pause_requested' ||
task.status === 'paused' ||
task.status === 'cancel_requested'
)
return (
<div key={task.id} className={`task-card ${taskCardClass}`}>
<div className="task-main">
<div className="task-title">{task.title}</div>
<div className="task-meta">
<span className={`task-status ${taskCardClass}`}>{backgroundTaskStatusLabels[task.status]}</span>
<span>{backgroundTaskSourceLabels[task.sourcePage] || backgroundTaskSourceLabels.other}</span>
<span>{new Date(task.startedAt).toLocaleString('zh-CN')}</span>
</div>
{progress.ratio !== null && (
<div className="task-progress-bar">
<div
className="task-progress-fill"
style={{ width: `${progress.ratio * 100}%` }}
/>
</div>
)}
<div className="task-progress-text">
{task.detail || '任务进行中'}
{task.progressText ? ` · ${task.progressText}` : ''}
</div>
</div>
<div className="task-actions">
{canPause && (
<button
className="task-action-btn"
type="button"
onClick={() => onPauseBackgroundTask(task.id)}
>
<Pause size={14} />
</button>
)}
{canResume && (
<button
className="task-action-btn primary"
type="button"
onClick={() => onResumeBackgroundTask(task.id)}
>
<Play size={14} />
</button>
)}
<button
className="task-action-btn danger"
type="button"
onClick={() => onCancelBackgroundTask(task.id)}
disabled={!canCancel || task.status === 'cancel_requested'}
>
{task.status === 'cancel_requested' ? '停止中' : '停止'}
</button>
</div>
</div>
)
})}
</div>
)}
</div>
@@ -4857,6 +5100,7 @@ function ExportPage() {
const openEditAutomationTaskDraft = useCallback((task: ExportAutomationTask) => {
const schedule = task.schedule
const firstTriggerAt = normalizeAutomationFirstTriggerAt(schedule.firstTriggerAt)
const stopAt = Number(task.stopCondition?.endAt || 0)
const maxRuns = Number(task.stopCondition?.maxRuns || 0)
const resolvedRange = resolveAutomationDateRangeSelection(task.template.dateRangeConfig as any, new Date())
@@ -4877,6 +5121,8 @@ function ExportPage() {
dateRangeConfig: task.template.dateRangeConfig,
intervalDays: normalizeAutomationIntervalDays(schedule.intervalDays),
intervalHours: normalizeAutomationIntervalHours(schedule.intervalHours),
firstTriggerAtEnabled: firstTriggerAt > 0,
firstTriggerAtValue: firstTriggerAt > 0 ? toDateTimeLocalValue(firstTriggerAt) : '',
stopAtEnabled: stopAt > 0,
stopAtValue: stopAt > 0 ? toDateTimeLocalValue(stopAt) : '',
maxRunsEnabled: maxRuns > 0,
@@ -4982,7 +5228,18 @@ function ExportPage() {
window.alert('执行间隔不能为 0请至少设置天数或小时')
return
}
const schedule: ExportAutomationSchedule = { type: 'interval', intervalDays, intervalHours }
const firstTriggerAtTimestamp = automationTaskDraft.firstTriggerAtEnabled
? parseDateTimeLocalValue(automationTaskDraft.firstTriggerAtValue)
: null
if (automationTaskDraft.firstTriggerAtEnabled && !firstTriggerAtTimestamp) {
window.alert('请填写有效的首次触发时间')
return
}
const schedule = buildAutomationSchedule(
intervalDays,
intervalHours,
firstTriggerAtTimestamp && firstTriggerAtTimestamp > 0 ? firstTriggerAtTimestamp : 0
)
const stopAtTimestamp = automationTaskDraft.stopAtEnabled
? parseDateTimeLocalValue(automationTaskDraft.stopAtValue)
: null
@@ -5169,14 +5426,10 @@ function ExportPage() {
const settledSessionIdsFromProgress = new Set<string>()
const sessionMessageProgress = new Map<string, { exported: number; total: number; knownTotal: boolean }>()
let queuedProgressPayload: ExportProgress | null = null
let queuedProgressRaf: number | null = null
let queuedProgressSignature = ''
let queuedProgressTimer: number | null = null
const clearQueuedProgress = () => {
if (queuedProgressRaf !== null) {
window.cancelAnimationFrame(queuedProgressRaf)
queuedProgressRaf = null
}
if (queuedProgressTimer !== null) {
window.clearTimeout(queuedProgressTimer)
queuedProgressTimer = null
@@ -5228,6 +5481,7 @@ function ExportPage() {
if (!queuedProgressPayload) return
const payload = queuedProgressPayload
queuedProgressPayload = null
queuedProgressSignature = ''
const now = Date.now()
const currentSessionId = String(payload.currentSessionId || '').trim()
updateTask(next.id, task => {
@@ -5284,77 +5538,71 @@ function ExportPage() {
const mediaBytesWritten = Number.isFinite(payload.mediaBytesWritten)
? Math.max(prevMediaBytesWritten, Math.max(0, Math.floor(Number(payload.mediaBytesWritten || 0))))
: prevMediaBytesWritten
const nextProgress: TaskProgress = {
current: payload.current,
total: payload.total,
currentName: payload.currentSession || '',
phase: payload.phase,
phaseLabel: payload.phaseLabel || '',
phaseProgress: payload.phaseProgress || 0,
phaseTotal: payload.phaseTotal || 0,
exportedMessages: Math.max(task.progress.exportedMessages, aggregatedMessageProgress.exported),
estimatedTotalMessages: aggregatedMessageProgress.estimated > 0
? Math.max(task.progress.estimatedTotalMessages, aggregatedMessageProgress.estimated)
: (task.progress.estimatedTotalMessages > 0 ? task.progress.estimatedTotalMessages : 0),
collectedMessages: Math.max(task.progress.collectedMessages, collectedMessages),
writtenFiles,
mediaDoneFiles,
mediaCacheHitFiles,
mediaCacheMissFiles,
mediaCacheFillFiles,
mediaDedupReuseFiles,
mediaBytesWritten
}
const hasSettledListChanged = !areStringArraysEqual(settledSessionIds, nextSettledSessionIds)
const hasProgressChanged = !areTaskProgressEqual(task.progress, nextProgress)
const hasPerformanceChanged = performance !== task.performance
if (!hasSettledListChanged && !hasProgressChanged && !hasPerformanceChanged) {
return task
}
return {
...task,
progress: {
current: payload.current,
total: payload.total,
currentName: payload.currentSession,
phase: payload.phase,
phaseLabel: payload.phaseLabel || '',
phaseProgress: payload.phaseProgress || 0,
phaseTotal: payload.phaseTotal || 0,
exportedMessages: Math.max(task.progress.exportedMessages, aggregatedMessageProgress.exported),
estimatedTotalMessages: aggregatedMessageProgress.estimated > 0
? Math.max(task.progress.estimatedTotalMessages, aggregatedMessageProgress.estimated)
: (task.progress.estimatedTotalMessages > 0 ? task.progress.estimatedTotalMessages : 0),
collectedMessages: Math.max(task.progress.collectedMessages, collectedMessages),
writtenFiles,
mediaDoneFiles,
mediaCacheHitFiles,
mediaCacheMissFiles,
mediaCacheFillFiles,
mediaDedupReuseFiles,
mediaBytesWritten
},
settledSessionIds: nextSettledSessionIds,
performance
progress: hasProgressChanged ? nextProgress : task.progress,
settledSessionIds: hasSettledListChanged ? nextSettledSessionIds : settledSessionIds,
performance: hasPerformanceChanged ? performance : task.performance
}
})
}
const queueProgressUpdate = (payload: ExportProgress) => {
const signature = buildProgressPayloadSignature(payload)
if (queuedProgressPayload && signature === queuedProgressSignature) {
return
}
queuedProgressPayload = payload
queuedProgressSignature = signature
if (payload.phase === 'complete') {
clearQueuedProgress()
flushQueuedProgress()
return
}
if (queuedProgressRaf !== null || queuedProgressTimer !== null) return
queuedProgressRaf = window.requestAnimationFrame(() => {
queuedProgressRaf = null
queuedProgressTimer = window.setTimeout(() => {
queuedProgressTimer = null
flushQueuedProgress()
}, 180)
})
if (queuedProgressTimer !== null) return
queuedProgressTimer = window.setTimeout(() => {
queuedProgressTimer = null
flushQueuedProgress()
}, EXPORT_PROGRESS_UI_FLUSH_INTERVAL_MS)
}
if (next.payload.scope === 'sns') {
progressUnsubscribeRef.current = window.electronAPI.sns.onExportProgress((payload) => {
updateTask(next.id, task => {
if (task.status !== 'running') return task
return {
...task,
progress: {
current: payload.current || 0,
total: payload.total || 0,
currentName: '',
phase: 'exporting',
phaseLabel: payload.status || '',
phaseProgress: payload.total > 0 ? payload.current : 0,
phaseTotal: payload.total || 0,
exportedMessages: payload.total > 0 ? Math.max(0, Math.floor(payload.current || 0)) : task.progress.exportedMessages,
estimatedTotalMessages: payload.total > 0 ? Math.max(0, Math.floor(payload.total || 0)) : task.progress.estimatedTotalMessages,
collectedMessages: task.progress.collectedMessages,
writtenFiles: task.progress.writtenFiles,
mediaDoneFiles: task.progress.mediaDoneFiles,
mediaCacheHitFiles: task.progress.mediaCacheHitFiles,
mediaCacheMissFiles: task.progress.mediaCacheMissFiles,
mediaCacheFillFiles: task.progress.mediaCacheFillFiles,
mediaDedupReuseFiles: task.progress.mediaDedupReuseFiles,
mediaBytesWritten: task.progress.mediaBytesWritten
}
}
queueProgressUpdate({
current: Number(payload.current || 0),
total: Number(payload.total || 0),
currentSession: '',
currentSessionId: '',
phase: 'exporting',
phaseLabel: String(payload.status || ''),
phaseProgress: payload.total > 0 ? Number(payload.current || 0) : 0,
phaseTotal: Number(payload.total || 0)
})
})
} else {
@@ -5679,6 +5927,8 @@ function ExportPage() {
dateRangeConfig: serializeExportDateRangeConfig(normalizedRangeSelection),
intervalDays: 1,
intervalHours: 0,
firstTriggerAtEnabled: false,
firstTriggerAtValue: '',
stopAtEnabled: false,
stopAtValue: '',
maxRunsEnabled: false,
@@ -7357,11 +7607,23 @@ function ExportPage() {
const handleCancelBackgroundTask = useCallback((taskId: string) => {
requestCancelBackgroundTask(taskId)
}, [])
const handlePauseBackgroundTask = useCallback((taskId: string) => {
requestPauseBackgroundTask(taskId)
}, [])
const handleResumeBackgroundTask = useCallback((taskId: string) => {
requestResumeBackgroundTask(taskId)
}, [])
const handleCancelAllNonExportTasks = useCallback(() => {
requestCancelBackgroundTasks(task => (
task.sourcePage !== 'export' &&
task.sourcePage !== 'chat' &&
task.cancelable &&
(task.status === 'running' || task.status === 'cancel_requested')
(
task.status === 'running' ||
task.status === 'pause_requested' ||
task.status === 'paused' ||
task.status === 'cancel_requested'
)
))
}, [])
@@ -7509,7 +7771,18 @@ function ExportPage() {
const isSnsCardStatsLoading = !hasSeededSnsStats
const taskRunningCount = tasks.filter(task => task.status === 'running').length
const taskQueuedCount = tasks.filter(task => task.status === 'queued').length
const taskCenterAlertCount = taskRunningCount + taskQueuedCount
const chatBackgroundTasks = useMemo(() => (
backgroundTasks.filter(task => task.sourcePage === 'chat')
), [backgroundTasks])
const chatBackgroundActiveTaskCount = useMemo(() => (
chatBackgroundTasks.filter(task => (
task.status === 'running' ||
task.status === 'pause_requested' ||
task.status === 'paused' ||
task.status === 'cancel_requested'
)).length
), [chatBackgroundTasks])
const taskCenterAlertCount = taskRunningCount + taskQueuedCount + chatBackgroundActiveTaskCount
const hasFilteredContacts = filteredContacts.length > 0
const optionalMetricColumnCount = (shouldShowSnsColumn ? 1 : 0) + (shouldShowMutualFriendsColumn ? 1 : 0)
const contactsMetricColumnCount = 4 + optionalMetricColumnCount
@@ -7524,15 +7797,25 @@ function ExportPage() {
width: `${Math.max(contactsHorizontalScrollMetrics.contentWidth, contactsHorizontalScrollMetrics.viewportWidth)}px`
}), [contactsHorizontalScrollMetrics.contentWidth, contactsHorizontalScrollMetrics.viewportWidth])
const nonExportBackgroundTasks = useMemo(() => (
backgroundTasks.filter(task => task.sourcePage !== 'export')
backgroundTasks.filter(task => task.sourcePage !== 'export' && task.sourcePage !== 'chat')
), [backgroundTasks])
const runningNonExportTaskCount = useMemo(() => (
nonExportBackgroundTasks.filter(task => task.status === 'running' || task.status === 'cancel_requested').length
nonExportBackgroundTasks.filter(task => (
task.status === 'running' ||
task.status === 'pause_requested' ||
task.status === 'paused' ||
task.status === 'cancel_requested'
)).length
), [nonExportBackgroundTasks])
const cancelableNonExportTaskCount = useMemo(() => (
nonExportBackgroundTasks.filter(task => (
task.cancelable &&
(task.status === 'running' || task.status === 'cancel_requested')
(
task.status === 'running' ||
task.status === 'pause_requested' ||
task.status === 'paused' ||
task.status === 'cancel_requested'
)
)).length
), [nonExportBackgroundTasks])
const nonExportBackgroundTasksUpdatedAt = useMemo(() => (
@@ -8152,12 +8435,16 @@ function ExportPage() {
<TaskCenterModal
isOpen={isTaskCenterOpen}
tasks={tasks}
chatBackgroundTasks={chatBackgroundTasks}
taskRunningCount={taskRunningCount}
taskQueuedCount={taskQueuedCount}
expandedPerfTaskId={expandedPerfTaskId}
nowTick={nowTick}
onClose={closeTaskCenter}
onTogglePerfTask={toggleTaskPerfDetail}
onPauseBackgroundTask={handlePauseBackgroundTask}
onResumeBackgroundTask={handleResumeBackgroundTask}
onCancelBackgroundTask={handleCancelBackgroundTask}
/>
{isAutomationModalOpen && createPortal(
@@ -8233,6 +8520,7 @@ function ExportPage() {
{queueState === 'queued' && <span className="automation-task-status queued"></span>}
</div>
<p>{formatAutomationScheduleLabel(task.schedule)}</p>
<p>{resolveAutomationFirstTriggerSummary(task)}</p>
<p>{formatAutomationRangeLabel(task.template.dateRangeConfig as any)}</p>
<p>{task.sessionIds.length} </p>
<p>{task.outputDir || `${exportFolder || '未设置'}(全局)`}</p>
@@ -8346,6 +8634,52 @@ function ExportPage() {
</label>
</div>
<div className="automation-form-field">
<span></span>
<label className="automation-inline-check">
<input
type="checkbox"
checked={automationTaskDraft.firstTriggerAtEnabled}
onChange={(event) => setAutomationTaskDraft((prev) => prev ? {
...prev,
firstTriggerAtEnabled: event.target.checked
} : prev)}
/>
</label>
{automationTaskDraft.firstTriggerAtEnabled && (
<div className="automation-first-trigger-picker">
<input
type="date"
className="automation-stopat-date"
value={automationTaskDraft.firstTriggerAtValue ? automationTaskDraft.firstTriggerAtValue.slice(0, 10) : ''}
onChange={(event) => {
const datePart = normalizeAutomationDatePart(event.target.value)
const timePart = normalizeAutomationTimePart(automationTaskDraft.firstTriggerAtValue?.slice(11) || '00:00')
setAutomationTaskDraft((prev) => prev ? {
...prev,
firstTriggerAtValue: datePart ? `${datePart}T${timePart}` : ''
} : prev)
}}
/>
<input
type="time"
className="automation-stopat-time"
value={automationTaskDraft.firstTriggerAtValue ? normalizeAutomationTimePart(automationTaskDraft.firstTriggerAtValue.slice(11)) : '00:00'}
onChange={(event) => {
const timePart = normalizeAutomationTimePart(event.target.value)
const datePart = normalizeAutomationDatePart(automationTaskDraft.firstTriggerAtValue?.slice(0, 10))
|| buildAutomationTodayDatePart()
setAutomationTaskDraft((prev) => prev ? {
...prev,
firstTriggerAtValue: `${datePart}T${timePart}`
} : prev)
}}
/>
</div>
)}
</div>
<div className="automation-form-field">
<span></span>
<div className="automation-segment-row">
@@ -8486,7 +8820,11 @@ function ExportPage() {
</label>
<div className="automation-draft-summary">
{automationTaskDraft.sessionIds.length} · {automationTaskDraft.intervalDays} {automationTaskDraft.intervalHours} · {formatAutomationRangeLabel(automationTaskDraft.dateRangeConfig as any, automationRangeSelection)} ·
{automationTaskDraft.sessionIds.length} · {automationTaskDraft.intervalDays} {automationTaskDraft.intervalHours} · {
automationTaskDraft.firstTriggerAtEnabled
? (automationTaskDraft.firstTriggerAtValue ? automationTaskDraft.firstTriggerAtValue.replace('T', ' ') : '未设置')
: '默认按创建时间+间隔'
} · {formatAutomationRangeLabel(automationTaskDraft.dateRangeConfig as any, automationRangeSelection)} ·
</div>
</div>
<div className="automation-editor-actions">
@@ -8959,7 +9297,12 @@ function ExportPage() {
type="button"
className="session-load-detail-task-stop-btn"
onClick={() => handleCancelBackgroundTask(task.id)}
disabled={!task.cancelable || (task.status !== 'running' && task.status !== 'cancel_requested')}
disabled={!task.cancelable || (
task.status !== 'running' &&
task.status !== 'pause_requested' &&
task.status !== 'paused' &&
task.status !== 'cancel_requested'
)}
>
</button>