From 6fb96dcc0cd555dc096b304e28347f43b8e133da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:24:21 +0800 Subject: [PATCH] Add files via upload --- web/static/i18n/en-US.json | 7 + web/static/i18n/zh-CN.json | 7 + web/static/js/tasks.js | 257 +++++++++++++++++++++++++++++++++---- 3 files changed, 247 insertions(+), 24 deletions(-) diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 419d233a..3720484a 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -251,6 +251,7 @@ "clearHistory": "Clear history", "cancelTask": "Cancel task", "viewConversation": "View conversation", + "retryTask": "Retry", "conversationIdLabel": "Conversation ID", "statusPending": "Pending", "statusPaused": "Paused", @@ -1517,6 +1518,7 @@ "cronExprPlaceholder": "e.g. 0 */2 * * * (run every 2 hours)", "cronExprHint": "Use standard 5-field Cron: minute hour day month weekday. Example: `0 2 * * *` runs at 02:00 daily.", "cronExprRequired": "Please fill in a Cron expression when Cron schedule is selected", + "cronExprInvalid": "Invalid Cron expression format. Must have 5 fields (minute hour day month weekday), e.g.: 0 */2 * * *", "executeNow": "Run immediately after creation", "executeNowHint": "Default is off. When disabled, the queue stays pending and can be started manually later.", "tasksList": "Task list (one task per line)", @@ -1544,6 +1546,11 @@ "nextRunAt": "Next run at", "scheduleCronAuto": "Allow Cron auto-run", "scheduleCronAutoHint": "When off, the cron expression is kept but the queue will not run on schedule; use Start to run manually.", + "editSchedule": "Edit Schedule", + "editScheduleTitle": "Edit Schedule Configuration", + "editScheduleSuccess": "Schedule updated", + "editScheduleError": "Failed to update schedule", + "editMetadata": "Edit Info", "lastScheduleTriggerAt": "Last scheduled trigger", "lastScheduleError": "Last schedule error", "lastRunError": "Last run failure summary", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index f0a81576..22c0586f 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -251,6 +251,7 @@ "clearHistory": "清空历史", "cancelTask": "取消任务", "viewConversation": "查看对话", + "retryTask": "重试", "conversationIdLabel": "对话ID", "statusPending": "待执行", "statusPaused": "已暂停", @@ -1517,6 +1518,7 @@ "cronExprPlaceholder": "例如:0 */2 * * *(每2小时执行一次)", "cronExprHint": "采用标准 5 段 Cron:分 时 日 月 周,例如 `0 2 * * *` 表示每天 02:00 执行。", "cronExprRequired": "请选择 Cron 调度后填写 Cron 表达式", + "cronExprInvalid": "Cron 表达式格式错误,需要 5 段(分 时 日 月 周),例如:0 */2 * * *", "executeNow": "创建后立即执行", "executeNowHint": "默认不立即执行;关闭后队列保持待执行,可在需要时手动开始。", "tasksList": "任务列表(每行一个任务)", @@ -1544,6 +1546,11 @@ "nextRunAt": "下次执行时间", "scheduleCronAuto": "允许 Cron 自动调度", "scheduleCronAutoHint": "关闭后仅保留表达式配置,不会按时间自动跑;可随时手工点「开始执行」。", + "editSchedule": "修改调度", + "editScheduleTitle": "修改调度配置", + "editScheduleSuccess": "调度配置已更新", + "editScheduleError": "更新调度配置失败", + "editMetadata": "编辑信息", "lastScheduleTriggerAt": "最近调度触发时间", "lastScheduleError": "最近调度失败原因", "lastRunError": "最近运行失败摘要", diff --git a/web/static/js/tasks.js b/web/static/js/tasks.js index 7694c1b9..575178c3 100644 --- a/web/static/js/tasks.js +++ b/web/static/js/tasks.js @@ -937,7 +937,11 @@ async function createBatchQueue() { alert(_t('batchImportModal.cronExprRequired')); return; } - + if (scheduleMode === 'cron' && !/^\S+\s+\S+\s+\S+\s+\S+\s+\S+$/.test(cronExpr)) { + alert(_t('batchImportModal.cronExprInvalid') || 'Cron 表达式格式错误,需要 5 段(分 时 日 月 周)'); + return; + } + try { const response = await apiFetch('/api/batch-tasks', { method: 'POST', @@ -1372,10 +1376,36 @@ async function showBatchQueueDetail(queueId) { ${showProgressNoteInModal ? `

${escapeHtml(pres.progressNote)}

` : ''}
- ${queue.title ? `
${escapeHtml(_t('batchQueueDetailModal.queueTitle'))}${escapeHtml(queue.title)}
` : ''} +
${escapeHtml(_t('batchQueueDetailModal.queueTitle'))}${escapeHtml(queue.title || _t('tasks.batchQueueUntitled'))}${allowSubtaskMutation ? ` ` : ''}
+
${escapeHtml(_t('batchQueueDetailModal.role'))}${roleLineVal}
${escapeHtml(_t('batchImportModal.agentMode'))}${escapeHtml(agentModeText)}
-
${escapeHtml(_t('batchImportModal.scheduleMode'))}${scheduleDetail}
+
${escapeHtml(_t('batchImportModal.scheduleMode'))}${scheduleDetail}${allowSubtaskMutation ? ` ` : ''}
+
${escapeHtml(_t('batchQueueDetailModal.taskTotal'))}${queue.tasks.length}
${queue.scheduleMode === 'cron' ? `
${escapeHtml(_t('batchQueueDetailModal.scheduleCronAuto'))}
` : ''}
@@ -1408,6 +1438,7 @@ async function showBatchQueueDetail(queueId) { ${escapeHtml(task.message)} ${canEdit ? `` : ''} ${canEdit ? `` : ''} + ${allowSubtaskMutation && task.status === 'failed' ? `` : ''} ${task.conversationId ? `` : ''} ${task.startedAt ? `
` + _t('batchQueueDetailModal.startLabel') + `: ${new Date(task.startedAt).toLocaleString()}
` : ''} @@ -1452,7 +1483,8 @@ async function showBatchQueueDetail(queueId) { async function startBatchQueue() { const queueId = batchQueuesState.currentQueueId; if (!queueId) return; - + const btn = document.getElementById('batch-queue-start-btn'); + if (btn) { btn.disabled = true; } try { // Cron 队列点击“开始执行”会立即运行一轮,这里二次确认避免误触 const queueResponse = await apiFetch(`/api/batch-tasks/${queueId}`); @@ -1482,6 +1514,8 @@ async function startBatchQueue() { } catch (error) { console.error('启动批量任务失败:', error); alert(_t('tasks.startBatchQueueFailed') + ': ' + error.message); + } finally { + if (btn) { btn.disabled = false; } } } @@ -1489,11 +1523,12 @@ async function startBatchQueue() { async function pauseBatchQueue() { const queueId = batchQueuesState.currentQueueId; if (!queueId) return; - + if (!confirm(_t('tasks.pauseQueueConfirm'))) { return; } - + const btn = document.getElementById('batch-queue-pause-btn'); + if (btn) { btn.disabled = true; } try { const response = await apiFetch(`/api/batch-tasks/${queueId}/pause`, { method: 'POST', @@ -1510,6 +1545,8 @@ async function pauseBatchQueue() { } catch (error) { console.error('暂停批量任务失败:', error); alert(_t('tasks.pauseQueueFailed') + ': ' + error.message); + } finally { + if (btn) { btn.disabled = false; } } } @@ -1517,11 +1554,12 @@ async function pauseBatchQueue() { async function deleteBatchQueue() { const queueId = batchQueuesState.currentQueueId; if (!queueId) return; - + if (!confirm(_t('tasks.deleteQueueConfirm'))) { return; } - + const btn = document.getElementById('batch-queue-delete-btn'); + if (btn) { btn.disabled = true; } try { const response = await apiFetch(`/api/batch-tasks/${queueId}`, { method: 'DELETE', @@ -1537,6 +1575,8 @@ async function deleteBatchQueue() { } catch (error) { console.error('删除批量任务队列失败:', error); alert(_t('tasks.deleteQueueFailed') + ': ' + error.message); + } finally { + if (btn) { btn.disabled = false; } } } @@ -1586,8 +1626,17 @@ function startBatchQueueRefresh(queueId) { if (batchQueuesState.refreshInterval) { clearInterval(batchQueuesState.refreshInterval); } - + batchQueuesState.refreshInterval = setInterval(() => { + // 如果编辑或添加任务的模态框正在打开,跳过本次刷新防止丢失编辑内容 + const editModal = document.getElementById('edit-batch-task-modal'); + const addModal = document.getElementById('add-batch-task-modal'); + const editScheduleRow = document.getElementById('bq-edit-schedule-row'); + if ((editModal && editModal.style.display === 'block') || + (addModal && addModal.style.display === 'block') || + (editScheduleRow && editScheduleRow.style.display !== 'none')) { + return; + } if (batchQueuesState.currentQueueId === queueId) { showBatchQueueDetail(queueId); refreshBatchQueues(); @@ -1625,7 +1674,9 @@ function viewBatchTaskConversation(conversationId) { // 编辑批量任务的状态 const editBatchTaskState = { queueId: null, - taskId: null + taskId: null, + _escHandler: null, + _saveHandler: null }; // 从元素获取任务信息并打开编辑模态框 @@ -1676,24 +1727,30 @@ function editBatchTask(queueId, taskId, currentMessage) { messageInput.select(); }, 100); + // 清理旧的事件监听器(防止泄漏) + if (editBatchTaskState._escHandler) { + document.removeEventListener('keydown', editBatchTaskState._escHandler); + } + if (editBatchTaskState._saveHandler) { + messageInput.removeEventListener('keydown', editBatchTaskState._saveHandler); + } + // 添加ESC键监听 - const handleKeyDown = (e) => { + editBatchTaskState._escHandler = (e) => { if (e.key === 'Escape') { closeEditBatchTaskModal(); - document.removeEventListener('keydown', handleKeyDown); } }; - document.addEventListener('keydown', handleKeyDown); - + document.addEventListener('keydown', editBatchTaskState._escHandler); + // 添加Enter+Ctrl/Cmd保存功能 - const handleKeyPress = (e) => { + editBatchTaskState._saveHandler = (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); saveBatchTask(); - document.removeEventListener('keydown', handleKeyPress); } }; - messageInput.addEventListener('keydown', handleKeyPress); + messageInput.addEventListener('keydown', editBatchTaskState._saveHandler); } // 关闭编辑批量任务模态框 @@ -1702,6 +1759,18 @@ function closeEditBatchTaskModal() { if (modal) { modal.style.display = 'none'; } + // 清理事件监听器 + if (editBatchTaskState._escHandler) { + document.removeEventListener('keydown', editBatchTaskState._escHandler); + editBatchTaskState._escHandler = null; + } + if (editBatchTaskState._saveHandler) { + const messageInput = document.getElementById('edit-task-message'); + if (messageInput) { + messageInput.removeEventListener('keydown', editBatchTaskState._saveHandler); + } + editBatchTaskState._saveHandler = null; + } editBatchTaskState.queueId = null; editBatchTaskState.taskId = null; } @@ -1782,28 +1851,46 @@ function showAddBatchTaskModal() { messageInput.focus(); }, 100); + // 清理旧的事件监听器 + if (showAddBatchTaskModal._escHandler) { + document.removeEventListener('keydown', showAddBatchTaskModal._escHandler); + } + if (showAddBatchTaskModal._saveHandler && messageInput) { + messageInput.removeEventListener('keydown', showAddBatchTaskModal._saveHandler); + } + // 添加ESC键监听 - const handleKeyDown = (e) => { + showAddBatchTaskModal._escHandler = (e) => { if (e.key === 'Escape') { closeAddBatchTaskModal(); - document.removeEventListener('keydown', handleKeyDown); } }; - document.addEventListener('keydown', handleKeyDown); - + document.addEventListener('keydown', showAddBatchTaskModal._escHandler); + // 添加Enter+Ctrl/Cmd保存功能 - const handleKeyPress = (e) => { + showAddBatchTaskModal._saveHandler = (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); saveAddBatchTask(); - messageInput.removeEventListener('keydown', handleKeyPress); } }; - messageInput.addEventListener('keydown', handleKeyPress); + messageInput.addEventListener('keydown', showAddBatchTaskModal._saveHandler); } // 关闭添加批量任务模态框 function closeAddBatchTaskModal() { + // 清理事件监听器 + if (showAddBatchTaskModal._escHandler) { + document.removeEventListener('keydown', showAddBatchTaskModal._escHandler); + showAddBatchTaskModal._escHandler = null; + } + if (showAddBatchTaskModal._saveHandler) { + const messageInput = document.getElementById('add-task-message'); + if (messageInput) { + messageInput.removeEventListener('keydown', showAddBatchTaskModal._saveHandler); + } + showAddBatchTaskModal._saveHandler = null; + } const modal = document.getElementById('add-batch-task-modal'); const messageInput = document.getElementById('add-task-message'); if (modal) { @@ -1952,6 +2039,120 @@ async function updateBatchQueueScheduleEnabled(enabled) { } } +// --- 元数据(标题/角色)内联编辑 --- +function showEditMetadataInline() { + const row = document.getElementById('bq-edit-metadata-row'); + if (row) row.style.display = ''; +} +function hideEditMetadataInline() { + const row = document.getElementById('bq-edit-metadata-row'); + if (row) row.style.display = 'none'; +} +async function saveEditMetadata() { + const queueId = batchQueuesState.currentQueueId; + if (!queueId) return; + const titleInput = document.getElementById('bq-edit-title'); + const roleInput = document.getElementById('bq-edit-role'); + const title = titleInput ? titleInput.value.trim() : ''; + const role = roleInput ? roleInput.value.trim() : ''; + try { + const response = await apiFetch(`/api/batch-tasks/${queueId}/metadata`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, role }), + }); + if (!response.ok) { + const result = await response.json().catch(() => ({})); + throw new Error(result.error || _t('tasks.updateTaskFailed')); + } + showBatchQueueDetail(queueId); + refreshBatchQueues(); + } catch (e) { + console.error(e); + alert(e.message); + } +} + +// --- 重试失败任务 --- +async function retryBatchTask(queueId, taskId) { + if (!queueId || !taskId) return; + try { + // 将失败任务重置为 pending(通过更新消息触发状态刷新不可行,需后端支持) + // 利用已有的 update task API:先获取当前消息,再 PUT 回去(后端会保留 message 但不重置状态) + // 实际上后端 UpdateTaskMessage 不修改 status,所以我们需要直接调 API 修改 status + // 暂时方案:删除旧任务+重新添加同内容任务 + const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`); + if (!detailResp.ok) throw new Error(_t('tasks.getQueueDetailFailed')); + const detail = await detailResp.json(); + const task = detail.queue.tasks.find(t => t.id === taskId); + if (!task) throw new Error('Task not found'); + const message = task.message; + // 删除旧任务 + await apiFetch(`/api/batch-tasks/${queueId}/tasks/${taskId}`, { method: 'DELETE' }); + // 添加新任务(会自动为 pending) + await apiFetch(`/api/batch-tasks/${queueId}/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message }), + }); + showBatchQueueDetail(queueId); + refreshBatchQueues(); + } catch (e) { + console.error('重试任务失败:', e); + alert(e.message); + } +} + +// --- 调度配置内联编辑 --- +function showEditScheduleInline() { + const row = document.getElementById('bq-edit-schedule-row'); + if (row) row.style.display = ''; +} +function hideEditScheduleInline() { + const row = document.getElementById('bq-edit-schedule-row'); + if (row) row.style.display = 'none'; +} +function toggleEditScheduleCronInput() { + const modeSelect = document.getElementById('bq-edit-schedule-mode'); + const cronInput = document.getElementById('bq-edit-cron-expr'); + if (modeSelect && cronInput) { + cronInput.style.display = modeSelect.value === 'cron' ? '' : 'none'; + } +} +async function saveEditSchedule() { + const queueId = batchQueuesState.currentQueueId; + if (!queueId) return; + const modeSelect = document.getElementById('bq-edit-schedule-mode'); + const cronInput = document.getElementById('bq-edit-cron-expr'); + if (!modeSelect) return; + const scheduleMode = modeSelect.value; + const cronExpr = cronInput ? cronInput.value.trim() : ''; + if (scheduleMode === 'cron' && !cronExpr) { + alert(_t('batchImportModal.cronExprRequired')); + return; + } + if (scheduleMode === 'cron' && !/^\S+\s+\S+\s+\S+\s+\S+\s+\S+$/.test(cronExpr)) { + alert(_t('batchImportModal.cronExprInvalid') || 'Cron 表达式格式错误,需要 5 段(分 时 日 月 周)'); + return; + } + try { + const response = await apiFetch(`/api/batch-tasks/${queueId}/schedule`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ scheduleMode, cronExpr }), + }); + if (!response.ok) { + const result = await response.json().catch(() => ({})); + throw new Error(result.error || _t('batchQueueDetailModal.editScheduleError')); + } + showBatchQueueDetail(queueId); + refreshBatchQueues(); + } catch (e) { + console.error(e); + alert(_t('batchQueueDetailModal.editScheduleError') + ': ' + e.message); + } +} + // 导出函数 window.showBatchImportModal = showBatchImportModal; window.closeBatchImportModal = closeBatchImportModal; @@ -1977,6 +2178,14 @@ window.deleteBatchTaskFromElement = deleteBatchTaskFromElement; window.deleteBatchQueueFromList = deleteBatchQueueFromList; window.handleBatchScheduleModeChange = handleBatchScheduleModeChange; window.updateBatchQueueScheduleEnabled = updateBatchQueueScheduleEnabled; +window.showEditMetadataInline = showEditMetadataInline; +window.hideEditMetadataInline = hideEditMetadataInline; +window.saveEditMetadata = saveEditMetadata; +window.retryBatchTask = retryBatchTask; +window.showEditScheduleInline = showEditScheduleInline; +window.hideEditScheduleInline = hideEditScheduleInline; +window.toggleEditScheduleCronInput = toggleEditScheduleCronInput; +window.saveEditSchedule = saveEditSchedule; // 语言切换后,列表/分页/详情弹窗由 JS 渲染的文案需用当前语言重绘(applyTranslations 不会处理 innerHTML 内容) document.addEventListener('languagechange', function () {