feat: manage task process lifetimes and preserve turn history

This commit is contained in:
Ed1s0nZ
2026-09-16 17:52:58 +08:00
parent fd1c13a43d
commit f7882be546
54 changed files with 3650 additions and 330 deletions
+3
View File
@@ -1107,6 +1107,9 @@
"autoRefresh": "Auto refresh",
"historyHint": "Tip: Completed task history available. Check \"Show history\" to view.",
"statusRunning": "Running",
"statusCleanupUnconfirmed": "Remote cancellation unconfirmed",
"statusCleaning": "Cleaning up",
"statusCleanupFailed": "Cleanup failed (retrying)",
"statusCancelling": "Cancelling",
"statusFailed": "Failed",
"statusTimeout": "Timeout",
+3
View File
@@ -1095,6 +1095,9 @@
"autoRefresh": "自动刷新",
"historyHint": "提示:有已完成的任务历史,请勾选\"显示历史记录\"查看",
"statusRunning": "执行中",
"statusCleanupUnconfirmed": "远端停止状态待确认",
"statusCleaning": "清理中",
"statusCleanupFailed": "清理异常(自动重试)",
"statusCancelling": "取消中",
"statusFailed": "执行失败",
"statusTimeout": "执行超时",
+5 -2
View File
@@ -3,7 +3,7 @@ const progressTaskState = new Map();
let userInterruptModalPending = null;
let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 2000; // 运行态与审批态需要及时自刷新
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed', 'cleanup_unconfirmed']);
const hitlInterruptToolItemMap = new Map();
let activeTasksLoadPromise = null;
let activeTasksVisualSignature = '';
@@ -7153,13 +7153,16 @@ function renderActiveTasks(tasks) {
const statusMap = {
'running': _t('tasks.statusRunning'),
'cancelling': _t('tasks.statusCancelling'),
'cleaning': _t('tasks.statusCleaning'),
'cleanup_failed': _t('tasks.statusCleanupFailed'),
'cleanup_unconfirmed': _t('tasks.statusCleanupUnconfirmed'),
'failed': _t('tasks.statusFailed'),
'timeout': _t('tasks.statusTimeout'),
'cancelled': _t('tasks.statusCancelled'),
'completed': _t('tasks.statusCompleted')
};
const statusText = statusMap[task.status] || _t('tasks.statusRunning');
const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed'].includes(task.status);
const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed', 'cleanup_unconfirmed'].includes(task.status);
const taskDisplayName = getActiveTaskDisplayName(task);
const stopTaskBtnText = _t('tasks.stopTask');
+11 -7
View File
@@ -187,7 +187,7 @@ function updateCompletedTasksHistory(currentTasks) {
const exists = tasksState.completedTasksHistory.some(t => t.conversationId === task.conversationId);
if (!exists) {
// 如果任务状态不是最终状态,标记为completed
const finalStatus = ['completed', 'failed', 'timeout', 'cancelled'].includes(task.status)
const finalStatus = ['completed', 'failed', 'timeout', 'cancelled', 'cleanup_unconfirmed'].includes(task.status)
? task.status
: 'completed';
@@ -309,7 +309,7 @@ function updateTaskStats(tasks) {
tasks.forEach(task => {
if (task.status === 'running') {
stats.running++;
} else if (task.status === 'cancelling') {
} else if (['cancelling', 'cleaning', 'cleanup_failed'].includes(task.status)) {
stats.cancelling++;
} else if (task.status === 'completed') {
stats.completed++;
@@ -364,7 +364,7 @@ function filterAndSortTasks() {
if (statusFilter === 'active') {
// 仅运行中的任务(不包括历史)
filtered = tasksState.allTasks.filter(task =>
task.status === 'running' || task.status === 'cancelling'
['running', 'cancelling', 'cleaning', 'cleanup_failed'].includes(task.status)
);
} else if (statusFilter === 'history') {
// 仅历史记录
@@ -445,7 +445,7 @@ function updateTaskDurations() {
const status = item.dataset.status;
const durationEl = item.querySelector('.task-duration');
if (durationEl && startedAt && (status === 'running' || status === 'cancelling')) {
if (durationEl && startedAt && (['running', 'cancelling', 'cleaning', 'cleanup_failed'].includes(status))) {
durationEl.textContent = calculateDuration(startedAt);
}
});
@@ -470,6 +470,9 @@ function renderTasks(tasks) {
// 状态映射
const statusMap = {
'running': { text: _t('tasks.statusRunning'), class: 'task-status-running' },
'cleaning': { text: _t('tasks.statusCleaning'), class: 'task-status-cancelling' },
'cleanup_unconfirmed': { text: _t('tasks.statusCleanupUnconfirmed'), class: 'task-status-failed' },
'cleanup_failed': { text: _t('tasks.statusCleanupFailed'), class: 'task-status-failed' },
'cancelling': { text: _t('tasks.statusCancelling'), class: 'task-status-cancelling' },
'failed': { text: _t('tasks.statusFailed'), class: 'task-status-failed' },
'timeout': { text: _t('tasks.statusTimeout'), class: 'task-status-timeout' },
@@ -530,10 +533,10 @@ function renderTaskItem(task, statusMap, isHistory = false) {
: '';
const status = statusMap[task.status] || { text: task.status, class: 'task-status-unknown' };
const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed'].includes(task.status);
const canCancel = !isFinalStatus && task.status !== 'cancelling' && !isHistory;
const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed', 'cleanup_unconfirmed'].includes(task.status);
const canCancel = !isFinalStatus && !['cancelling', 'cleaning'].includes(task.status) && !isHistory;
const isSelected = tasksState.selectedTasks.has(task.conversationId);
const duration = (task.status === 'running' || task.status === 'cancelling')
const duration = (['running', 'cancelling', 'cleaning', 'cleanup_failed'].includes(task.status))
? calculateDuration(task.startedAt)
: '';
@@ -561,6 +564,7 @@ function renderTaskItem(task, statusMap, isHistory = false) {
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation(${escapeJsStringAttr(task.conversationId)})">` + _t('tasks.viewConversation') + `</button>` : ''}
</div>
</div>
${task.cleanupError ? `<div class="task-details">${escapeHtml(task.cleanupError)}</div>` : ''}
${task.conversationId ? `
<div class="task-details">
<span class="task-id-label">` + _t('tasks.conversationIdLabel') + `:</span>