From 0a5bb1eab4f3248b61a41cb866659fffc2986975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:11:02 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 809 ++++++++++++++++++++++++++++++++++++- web/static/i18n/en-US.json | 32 +- web/static/i18n/zh-CN.json | 32 +- web/static/js/settings.js | 4 +- web/static/js/tasks.js | 318 ++++++++++----- web/templates/index.html | 29 +- 6 files changed, 1109 insertions(+), 115 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 654abf5c..f7e7263d 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -8196,6 +8196,10 @@ header { overflow: hidden; } +.batch-queues-filters.tasks-filters { + margin-bottom: 12px; +} + .batch-queues-header { display: flex; justify-content: space-between; @@ -8214,26 +8218,370 @@ header { .batch-queues-list { display: flex; flex-direction: column; - gap: 16px; - margin-bottom: 16px; + gap: 12px; + margin-bottom: 28px; } .batch-queue-item { background: var(--bg-primary); border: 1px solid var(--border-color); - border-radius: 12px; - padding: 20px; + border-radius: 14px; + padding: 14px 16px; box-shadow: var(--shadow-sm); - transition: all 0.2s ease; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; cursor: pointer; } .batch-queue-item:hover { box-shadow: var(--shadow-md); - transform: translateY(-2px); + transform: translateY(-1px); border-color: var(--accent-color); } +.batch-queue-item--cron-wait { + border-left: 3px solid var(--accent-color); +} + +.batch-queue-item--cron-wait:hover { + border-color: var(--accent-color); +} + +.batch-queue-item__inner { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + +.batch-queue-item__top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px 14px; +} + +.batch-queue-item__top-actions { + flex-shrink: 0; + margin-top: 1px; +} + +.batch-queue-icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + padding: 0; + border-radius: 10px; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease; +} + +.batch-queue-icon-btn:hover { + color: var(--error-color, #dc3545); + border-color: rgba(220, 53, 69, 0.35); + background: rgba(220, 53, 69, 0.06); +} + +.batch-queue-icon-btn__svg { + display: block; +} + +.batch-queue-item__band { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px 16px; + padding: 12px 14px; + margin: 0; + background: var(--bg-secondary); + border-radius: 10px; + border: 1px solid var(--border-color); +} + +.batch-queue-item__band-left { + flex: 1 1 220px; + min-width: 0; +} + +.batch-queue-item__band-right { + flex: 1 1 200px; + min-width: 160px; + max-width: 100%; + margin-left: auto; + display: flex; + flex-direction: column; + gap: 6px; + align-items: stretch; +} + +.batch-queue-progress-meta--band { + align-items: stretch; + text-align: right; +} + +.batch-queue-progress-meta--band .batch-queue-progress-note { + text-align: right; + max-width: none; +} + +@media (max-width: 520px) { + .batch-queue-item__band-right { + margin-left: 0; + flex-basis: 100%; + } + + .batch-queue-progress-meta--band { + text-align: left; + } + + .batch-queue-progress-meta--band .batch-queue-progress-note { + text-align: left; + } +} + +.batch-queue-card-row { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 16px 20px; + margin-bottom: 12px; +} + +.batch-queue-primary { + flex: 1 1 280px; + min-width: 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +.batch-queue-card-title { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + line-height: 1.35; + letter-spacing: -0.015em; +} + +.batch-queue-card-title--muted { + color: var(--text-secondary); + font-weight: 500; +} + +.batch-queue-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} + +.batch-queue-chip { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 11px; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + max-width: 100%; +} + +.batch-queue-chip-emoji { + font-size: 0.875rem; + line-height: 1; + flex-shrink: 0; +} + +.batch-queue-chip-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 200px; +} + +.batch-queue-chip--role .batch-queue-chip-text { + max-width: 160px; +} + +.batch-queue-chip--schedule { + color: var(--accent-color); + border-color: var(--border-color); + background: var(--bg-primary); + box-shadow: inset 2px 0 0 0 var(--accent-color); +} + +.batch-queue-chip--warn { + color: #b45309; + border-color: rgba(180, 83, 9, 0.35); + background: rgba(251, 191, 36, 0.12); +} + +.batch-queue-status-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; +} + +.batch-queue-next-run { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 10px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-left: 3px solid var(--accent-color); + font-size: 0.8125rem; + font-weight: 500; + color: var(--text-primary); +} + +.batch-queue-next-run__icon { + font-size: 1rem; + line-height: 1; + opacity: 0.88; +} + +.batch-queue-next-run__text { + line-height: 1.35; +} + +.batch-queue-footnotes { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 8px; + margin-top: 2px; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.batch-queue-footnotes__id code { + font-size: 0.7rem; + padding: 2px 7px; + border-radius: 6px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + word-break: break-all; + color: var(--text-secondary); +} + +.batch-queue-footnotes__sep { + opacity: 0.45; + user-select: none; +} + +.batch-queue-side { + flex: 0 1 232px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; +} + +.batch-queue-delete-btn { + font-size: 0.75rem !important; + padding: 5px 12px !important; + color: var(--text-secondary) !important; + border-color: var(--border-color) !important; + background: var(--bg-primary) !important; +} + +.batch-queue-delete-btn:hover { + color: var(--error-color, #dc3545) !important; + border-color: rgba(220, 53, 69, 0.4) !important; + background: rgba(220, 53, 69, 0.06) !important; +} + +.batch-queue-progress-stack { + width: 100%; + min-width: 200px; +} + +.batch-queue-progress-bar.batch-queue-progress-bar--card { + height: 10px; + border-radius: 999px; + background: var(--bg-secondary); + overflow: hidden; +} + +.batch-queue-progress-fill--cron-wait { + background: linear-gradient(90deg, var(--accent-color), var(--accent-hover), var(--accent-color)); + background-size: 200% 100%; + animation: batch-queue-progress-shimmer 2.4s ease infinite; +} + +@keyframes batch-queue-progress-shimmer { + 0% { + background-position: 100% 0; + } + 100% { + background-position: -100% 0; + } +} + +.batch-queue-progress-stack .batch-queue-progress-meta { + width: 100%; + align-items: flex-end; +} + +.batch-queue-progress-fraction { + color: var(--text-secondary); + font-weight: 400; + font-size: 0.8125rem; +} + +.batch-queue-stats--pills { + margin: 0; + padding-top: 10px; + border-top: 1px solid var(--border-color); + gap: 8px; +} + +.batch-queue-stat-pill { + display: inline-flex; + align-items: baseline; + gap: 6px; + padding: 5px 11px; + border-radius: 8px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + font-size: 0.75rem; +} + +.batch-queue-stat-pill__k { + color: var(--text-secondary); + font-weight: 500; +} + +.batch-queue-stat-pill__v { + font-weight: 600; + color: var(--text-primary); + font-variant-numeric: tabular-nums; +} + +.batch-queue-stat-pill--ok .batch-queue-stat-pill__v { + color: var(--success-color); +} + +.batch-queue-stat-pill--err .batch-queue-stat-pill__v { + color: var(--error-color); +} + +.batch-queue-stat-pill--muted .batch-queue-stat-pill__v { + color: var(--text-secondary); +} + .batch-queue-header { display: flex; justify-content: space-between; @@ -8273,12 +8621,106 @@ header { border: 1px solid rgba(0, 123, 255, 0.3); } +.batch-queue-cron-active { + box-shadow: 0 0 0 1px rgba(0, 123, 255, 0.15); +} + +.batch-queue-status-paused { + background: rgba(255, 193, 7, 0.12); + color: #b38600; + border: 1px solid rgba(255, 193, 7, 0.45); +} + .batch-queue-status-completed { background: rgba(40, 167, 69, 0.1); color: var(--success-color); border: 1px solid rgba(40, 167, 69, 0.3); } +/* Cron 队列:本轮跑完、等待下次触发(区别于「一次性任务已完成」) */ +.batch-queue-status-cron-cycle { + background: var(--bg-primary); + color: var(--text-primary); + border: 1px dashed var(--accent-color); +} + +.batch-queue-status-wrap { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + max-width: 100%; +} + +.batch-queue-cron-sublabel { + font-size: 0.75rem; + color: var(--accent-color); + font-weight: 500; + line-height: 1.35; + max-width: 280px; +} + +.batch-queue-cron-sublabel.detail { + margin-top: 6px; + max-width: none; +} + +.batch-queue-detail-status-wrap { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} + +.batch-queue-progress-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + min-width: 0; +} + +.batch-queue-progress-note { + font-size: 0.72rem; + color: var(--text-secondary); + line-height: 1.35; + text-align: right; + max-width: 240px; +} + +.batch-queue-progress-note.detail { + text-align: left; + max-width: none; + margin-top: 2px; + opacity: 0.95; +} + +.batch-queue-cron-callout { + display: flex; + gap: 12px; + align-items: flex-start; + padding: 12px 14px; + margin-bottom: 8px; + border-radius: 10px; + border: 1px solid var(--border-color); + border-left: 3px solid var(--accent-color); + background: var(--bg-secondary); +} + +.batch-queue-cron-callout-icon { + font-size: 1.25rem; + line-height: 1.2; + flex-shrink: 0; + opacity: 0.9; +} + +.batch-queue-cron-callout p { + margin: 0; + font-size: 0.875rem; + line-height: 1.5; + color: var(--text-primary); +} + .batch-queue-status-cancelled { background: rgba(108, 117, 125, 0.1); color: var(--text-secondary); @@ -8337,6 +8779,303 @@ header { white-space: nowrap; } +/* 批量队列详情:信息分层(状态 → 摘要 → 告警 → 折叠技术信息) */ +.batch-queue-detail-layout { + margin-bottom: 20px; +} + +.batch-queue-detail-hero { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 14px 16px; + margin-bottom: 14px; + background: var(--bg-secondary); + border-radius: 10px; + border: 1px solid var(--border-color); +} + +.batch-queue-detail-hero__sub { + margin: 0; + font-size: 0.875rem; + color: var(--text-primary); + line-height: 1.45; +} + +.batch-queue-detail-hero__note { + margin: 0; + font-size: 0.8125rem; + color: var(--text-secondary); + line-height: 1.45; +} + +.batch-queue-detail-kv { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 10px 20px; + margin-bottom: 14px; +} + +.bq-kv { + display: grid; + grid-template-columns: minmax(100px, 34%) 1fr; + gap: 8px 12px; + align-items: start; + padding: 8px 0; + border-bottom: 1px solid var(--border-color); + font-size: 0.875rem; +} + +.bq-kv--block { + grid-column: 1 / -1; + grid-template-columns: minmax(100px, 28%) 1fr; +} + +.bq-kv:last-child { + border-bottom: none; +} + +.bq-kv__k { + color: var(--text-secondary); + font-weight: 500; + font-size: 0.8125rem; +} + +.bq-kv__v { + color: var(--text-primary); + word-break: break-word; +} + +.bq-kv__v code { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8125rem; + background: var(--bg-secondary); + padding: 2px 6px; + border-radius: 4px; +} + +.bq-kv__v--control { + min-width: 0; +} + +@media (max-width: 520px) { + .bq-kv, + .bq-kv--block { + grid-template-columns: 1fr; + gap: 4px; + } +} + +.bq-cron-toggle { + display: flex; + align-items: flex-start; + gap: 10px; + cursor: pointer; + margin: 0; +} + +.bq-cron-toggle input { + margin-top: 3px; + flex-shrink: 0; +} + +.bq-cron-toggle__hint { + font-size: 0.8125rem; + color: var(--text-secondary); + line-height: 1.45; +} + +.bq-alert { + padding: 10px 12px; + border-radius: 8px; + margin-bottom: 12px; + font-size: 0.8125rem; +} + +.bq-alert--err { + background: rgba(220, 53, 69, 0.07); + border: 1px solid rgba(220, 53, 69, 0.28); + color: var(--text-primary); +} + +.bq-alert--err strong { + display: block; + color: var(--error-color); + font-size: 0.8125rem; + margin-bottom: 4px; +} + +.bq-alert--err p { + margin: 0; + color: var(--text-primary); + word-break: break-word; +} + +.batch-queue-detail-tech { + margin-bottom: 8px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-primary); + padding: 0 12px; +} + +.batch-queue-detail-tech__sum { + cursor: pointer; + font-size: 0.8125rem; + font-weight: 600; + color: var(--text-secondary); + padding: 10px 0; + list-style: none; +} + +.batch-queue-detail-tech__sum::-webkit-details-marker { + display: none; +} + +.batch-queue-detail-tech__body { + padding: 4px 0 12px; + border-top: 1px solid var(--border-color); +} + +.batch-queue-detail-tech__body .bq-kv:first-child { + padding-top: 10px; +} + +.batch-queue-cron-callout--compact { + margin-bottom: 12px; + padding: 10px 12px; +} + +.batch-queue-cron-callout--compact p { + font-size: 0.8125rem; +} + +/* 列表卡片紧凑布局 */ +.batch-queue-item__config { + margin: 4px 0 0; + font-size: 0.8125rem; + color: var(--text-secondary); + line-height: 1.4; +} + +.batch-queue-item__idline { + margin: 6px 0 0; + font-size: 0.75rem; + color: var(--text-secondary); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 6px; +} + +.batch-queue-item__idline code { + font-size: 0.7rem; + padding: 1px 6px; + border-radius: 4px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + word-break: break-all; +} + +.batch-queue-item__idsep { + opacity: 0.45; + user-select: none; +} + +.batch-queue-inline-warn { + color: #b45309; + font-weight: 500; + font-size: 0.8125rem; +} + +.batch-queue-item__mid { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px 16px; + padding: 10px 0 6px; + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); +} + +.batch-queue-item__mid-left { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; + flex: 1 1 200px; + min-width: 0; +} + +.batch-queue-item__sublabel { + font-size: 0.8125rem; + color: var(--text-primary); + line-height: 1.35; +} + +.batch-queue-item__mid-right { + flex: 0 1 200px; + min-width: 140px; + display: flex; + flex-direction: column; + gap: 4px; + align-items: stretch; +} + +.batch-queue-progress-bar--list { + height: 8px; +} + +.batch-queue-item__pct { + font-size: 0.75rem; + color: var(--text-secondary); + text-align: right; +} + +.batch-queue-item__pct-frac { + font-variant-numeric: tabular-nums; +} + +.batch-queue-statsline { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 0; + font-size: 0.75rem; + color: var(--text-secondary); + padding-top: 8px; +} + +.batch-queue-statsline__sep { + margin: 0 8px; + opacity: 0.4; + user-select: none; +} + +.batch-queue-statsline__item--ok { + color: var(--success-color); +} + +.batch-queue-statsline__item--err { + color: var(--error-color); +} + +/* 任务列表与分页条分离,避免与最后一张卡片贴在一起 */ +#batch-queues-pagination.pagination-fixed { + margin-top: 8px; + padding-top: 10px; +} + +#batch-queues-pagination .pagination { + padding: 12px 16px 12px; + font-size: 0.875rem; +} + +#batch-queues-pagination .pagination-info { + color: var(--text-secondary); +} + .batch-queue-detail-info { margin-bottom: 24px; padding: 20px; @@ -8421,12 +9160,21 @@ header { color: var(--text-primary); } +.batch-queue-detail-layout + .batch-queue-tasks-list h4 { + margin-top: 4px; +} + +.batch-queue-item__title-col { + flex: 1; + min-width: 0; +} + .batch-task-item { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 8px; - padding: 16px; - margin-bottom: 12px; + padding: 12px 14px; + margin-bottom: 10px; transition: all 0.2s ease; } @@ -8556,15 +9304,56 @@ header { flex-direction: column; align-items: flex-start; } - + + .batch-queue-item__top { + flex-wrap: wrap; + gap: 8px 10px; + } + + .batch-queue-item__mid { + flex-direction: column; + align-items: stretch; + } + + .batch-queue-item__mid-right { + flex: 1 1 auto; + max-width: none; + } + + .batch-queue-item__pct { + text-align: left; + } + + .batch-queue-card-row { + flex-direction: column; + } + + .batch-queue-side { + flex-basis: auto; + width: 100%; + align-items: stretch; + } + + .batch-queue-delete-btn { + align-self: flex-end; + } + + .batch-queue-progress-stack { + min-width: 0; + } + .batch-queue-progress { width: 100%; } - + .batch-queue-stats { flex-direction: column; gap: 8px; } + + .batch-queue-stats--pills { + justify-content: flex-start; + } .batch-task-header { flex-direction: column; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index ab0bab61..7ceb3e28 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -254,6 +254,14 @@ "conversationIdLabel": "Conversation ID", "statusPending": "Pending", "statusPaused": "Paused", + "statusCronCycleIdle": "Round done · scheduled loop", + "statusCronRunning": "Running · cron queue", + "cronNextRunLine": "Next run: {{time}}", + "cronRoundDoneProgressHint": "Cron queue: subtasks finished; next round starts on schedule", + "cronRunningProgressHint": "This round is running; the next full cycle follows Cron / next run time", + "cronPendingScheduled": "Cron scheduled · next {{time}}", + "cronPendingProgressNote": "Will start on schedule, or click Start to run a round now", + "cronRecurringCallout": "Cron queues start a new round at each scheduled time. Turn off \"Allow Cron auto-run\" to stop looping.", "confirmCancelTasks": "Cancel {{n}} selected task(s)?", "batchCancelResultPartial": "Batch cancel: {{success}} succeeded, {{fail}} failed", "batchCancelResultSuccess": "Successfully cancelled {{n}} task(s)", @@ -276,6 +284,7 @@ "deleteQueueConfirm": "Delete this batch queue? This cannot be undone.", "deleteQueueFailed": "Failed to delete batch queue", "batchQueueTitle": "Batch task queue", + "batchQueueUntitled": "Untitled queue", "resumeExecute": "Resume", "taskIncomplete": "Task information incomplete", "cannotGetTaskMessageInput": "Cannot get task message input", @@ -1494,6 +1503,18 @@ "role": "Role", "defaultRole": "Default", "roleHint": "Select a role; all tasks will be executed using that role's configuration (prompt and tools).", + "agentMode": "Agent mode", + "agentModeSingle": "Single-agent (ReAct)", + "agentModeMulti": "Multi-agent (Eino)", + "agentModeHint": "Single-agent is recommended by default; use multi-agent for complex tasks (requires system multi-agent enabled).", + "scheduleMode": "Schedule mode", + "scheduleModeManual": "Manual", + "scheduleModeCron": "Cron expression", + "scheduleModeHint": "Manual is for one-time runs; Cron is for recurring runs. Validate tasks manually first.", + "cronExpr": "Cron expression", + "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", "tasksList": "Task list (one task per line)", "tasksListPlaceholder": "Enter task list, one per line", "tasksListPlaceholderExample": "Enter task list, one per line, for example:\nScan open ports of 192.168.1.1\nCheck if https://example.com has SQL injection\nEnumerate subdomains of example.com", @@ -1514,13 +1535,22 @@ "status": "Status", "createdAt": "Created at", "startedAt": "Started at", + "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.", + "lastScheduleTriggerAt": "Last scheduled trigger", + "lastScheduleError": "Last schedule error", + "lastRunError": "Last run failure summary", + "cronSchedulePausedBadge": "Schedule paused", + "scheduleToggleFailed": "Failed to update schedule toggle", "completedAt": "Completed at", "taskTotal": "Total tasks", "taskList": "Task list", "startLabel": "Start", "completeLabel": "Complete", "errorLabel": "Error", - "resultLabel": "Result" + "resultLabel": "Result", + "technicalDetails": "Technical details (ID, times, schedule)" }, "editBatchTaskModal": { "title": "Edit task", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index b485d5d6..c10193b5 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -254,6 +254,14 @@ "conversationIdLabel": "对话ID", "statusPending": "待执行", "statusPaused": "已暂停", + "statusCronCycleIdle": "本轮已完成 · 定时循环中", + "statusCronRunning": "执行中 · 定时队列", + "cronNextRunLine": "下次执行:{{time}}", + "cronRoundDoneProgressHint": "定时队列:子任务已跑完,到点将自动下一轮", + "cronRunningProgressHint": "本轮执行中;下一整轮仍按 Cron 与「下次执行时间」排程", + "cronPendingScheduled": "Cron 已排程 · 下次 {{time}}", + "cronPendingProgressNote": "到点将自动开始;也可手动点「开始执行」立即跑一轮", + "cronRecurringCallout": "Cron 队列会在「下次执行时间」自动开始新一轮;关闭「允许 Cron 自动调度」即停止循环。", "confirmCancelTasks": "确定要取消 {{n}} 个任务吗?", "batchCancelResultPartial": "批量取消完成:成功 {{success}} 个,失败 {{fail}} 个", "batchCancelResultSuccess": "成功取消 {{n}} 个任务", @@ -276,6 +284,7 @@ "deleteQueueConfirm": "确定要删除这个批量任务队列吗?此操作不可恢复。", "deleteQueueFailed": "删除批量任务队列失败", "batchQueueTitle": "批量任务队列", + "batchQueueUntitled": "未命名队列", "resumeExecute": "继续执行", "taskIncomplete": "任务信息不完整", "cannotGetTaskMessageInput": "无法获取任务消息输入框", @@ -1494,6 +1503,18 @@ "role": "角色", "defaultRole": "默认", "roleHint": "选择一个角色,所有任务将使用该角色的配置(提示词和工具)执行。", + "agentMode": "代理模式", + "agentModeSingle": "单代理(ReAct)", + "agentModeMulti": "多代理(Eino)", + "agentModeHint": "建议默认单代理;复杂任务可使用多代理(需系统已启用多代理)。", + "scheduleMode": "调度方式", + "scheduleModeManual": "手工执行", + "scheduleModeCron": "调度表达式(Cron)", + "scheduleModeHint": "手工执行用于一次性任务;Cron 用于周期任务,建议先手工验证任务正确性。", + "cronExpr": "Cron 表达式", + "cronExprPlaceholder": "例如:0 */2 * * *(每2小时执行一次)", + "cronExprHint": "采用标准 5 段 Cron:分 时 日 月 周,例如 `0 2 * * *` 表示每天 02:00 执行。", + "cronExprRequired": "请选择 Cron 调度后填写 Cron 表达式", "tasksList": "任务列表(每行一个任务)", "tasksListPlaceholder": "请输入任务列表,每行一个任务", "tasksListPlaceholderExample": "请输入任务列表,每行一个任务,例如:\n扫描 192.168.1.1 的开放端口\n检查 https://example.com 是否存在SQL注入\n枚举 example.com 的子域名", @@ -1514,13 +1535,22 @@ "status": "状态", "createdAt": "创建时间", "startedAt": "开始时间", + "nextRunAt": "下次执行时间", + "scheduleCronAuto": "允许 Cron 自动调度", + "scheduleCronAutoHint": "关闭后仅保留表达式配置,不会按时间自动跑;可随时手工点「开始执行」。", + "lastScheduleTriggerAt": "最近调度触发时间", + "lastScheduleError": "最近调度失败原因", + "lastRunError": "最近运行失败摘要", + "cronSchedulePausedBadge": "调度已暂停", + "scheduleToggleFailed": "更新调度开关失败", "completedAt": "完成时间", "taskTotal": "任务总数", "taskList": "任务列表", "startLabel": "开始", "completeLabel": "完成", "errorLabel": "错误", - "resultLabel": "结果" + "resultLabel": "结果", + "technicalDetails": "技术信息(ID / 时间 / 调度)" }, "editBatchTaskModal": { "title": "编辑任务", diff --git a/web/static/js/settings.js b/web/static/js/settings.js index c8873b03..e358b73c 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -125,8 +125,6 @@ async function loadConfig(loadTools = true) { if (maMode) maMode.value = (ma.default_mode === 'multi') ? 'multi' : 'single'; const maRobot = document.getElementById('multi-agent-robot-use'); if (maRobot) maRobot.checked = ma.robot_use_multi_agent === true; - const maBatch = document.getElementById('multi-agent-batch-use'); - if (maBatch) maBatch.checked = ma.batch_use_multi_agent === true; // 填充知识库配置 const knowledgeEnabledCheckbox = document.getElementById('knowledge-enabled'); @@ -820,7 +818,7 @@ async function applySettings() { enabled: document.getElementById('multi-agent-enabled')?.checked === true, default_mode: document.getElementById('multi-agent-default-mode')?.value === 'multi' ? 'multi' : 'single', robot_use_multi_agent: document.getElementById('multi-agent-robot-use')?.checked === true, - batch_use_multi_agent: document.getElementById('multi-agent-batch-use')?.checked === true + batch_use_multi_agent: false }, knowledge: knowledgeConfig, robots: { diff --git a/web/static/js/tasks.js b/web/static/js/tasks.js index ac2e33f0..1bfaf76c 100644 --- a/web/static/js/tasks.js +++ b/web/static/js/tasks.js @@ -3,6 +3,60 @@ function _t(key, opts) { return typeof window.t === 'function' ? window.t(key, opts) : key; } +/** 插值不转 HTML 实体(避免日期里的 / 变成 / 再被 escapeHtml 成乱码) */ +function _tPlain(key, opts) { + if (typeof window.t !== 'function') return key; + const base = opts && typeof opts === 'object' ? opts : {}; + const interp = base.interpolation && typeof base.interpolation === 'object' ? base.interpolation : {}; + return window.t(key, { + ...base, + interpolation: { escapeValue: false, ...interp } + }); +} + +/** Cron 队列在「本轮 completed」等状态下的展示文案(底层 status 不变,仅 UI 强调循环调度) */ +function getBatchQueueStatusPresentation(queue) { + const map = { + pending: { text: _t('tasks.statusPending'), class: 'batch-queue-status-pending' }, + running: { text: _t('tasks.statusRunning'), class: 'batch-queue-status-running' }, + paused: { text: _t('tasks.statusPaused'), class: 'batch-queue-status-paused' }, + completed: { text: _t('tasks.statusCompleted'), class: 'batch-queue-status-completed' }, + cancelled: { text: _t('tasks.statusCancelled'), class: 'batch-queue-status-cancelled' } + }; + const base = map[queue.status] || { text: queue.status, class: 'batch-queue-status-unknown' }; + const cronOn = queue.scheduleMode === 'cron' && queue.scheduleEnabled !== false; + const nextStr = queue.nextRunAt ? new Date(queue.nextRunAt).toLocaleString() : ''; + const empty = { sublabel: null, progressNote: null, callout: null }; + + if (cronOn && queue.status === 'completed') { + return { + text: _t('tasks.statusCronCycleIdle'), + class: 'batch-queue-status-cron-cycle', + sublabel: nextStr ? _tPlain('tasks.cronNextRunLine', { time: nextStr }) : null, + progressNote: _t('tasks.cronRoundDoneProgressHint'), + callout: _t('tasks.cronRecurringCallout') + }; + } + if (cronOn && queue.status === 'running') { + return { + text: _t('tasks.statusCronRunning'), + class: 'batch-queue-status-running batch-queue-cron-active', + sublabel: nextStr ? _tPlain('tasks.cronNextRunLine', { time: nextStr }) : null, + progressNote: _t('tasks.cronRunningProgressHint'), + callout: null + }; + } + if (cronOn && queue.status === 'pending' && nextStr) { + return { + ...base, + ...empty, + sublabel: _tPlain('tasks.cronPendingScheduled', { time: nextStr }), + progressNote: _t('tasks.cronPendingProgressNote') + }; + } + return { ...base, ...empty }; +} + // HTML转义函数(如果未定义) if (typeof escapeHtml === 'undefined') { function escapeHtml(text) { @@ -725,6 +779,9 @@ async function showBatchImportModal() { const input = document.getElementById('batch-tasks-input'); const titleInput = document.getElementById('batch-queue-title'); const roleSelect = document.getElementById('batch-queue-role'); + const agentModeSelect = document.getElementById('batch-queue-agent-mode'); + const scheduleModeSelect = document.getElementById('batch-queue-schedule-mode'); + const cronExprInput = document.getElementById('batch-queue-cron-expr'); if (modal && input) { input.value = ''; if (titleInput) { @@ -734,6 +791,16 @@ async function showBatchImportModal() { if (roleSelect) { roleSelect.value = ''; } + if (agentModeSelect) { + agentModeSelect.value = 'single'; + } + if (scheduleModeSelect) { + scheduleModeSelect.value = 'manual'; + } + if (cronExprInput) { + cronExprInput.value = ''; + } + handleBatchScheduleModeChange(); updateBatchImportStats(''); // 加载并填充角色列表 @@ -776,6 +843,24 @@ function closeBatchImportModal() { } } +function handleBatchScheduleModeChange() { + const scheduleModeSelect = document.getElementById('batch-queue-schedule-mode'); + const cronGroup = document.getElementById('batch-queue-cron-group'); + const cronExprInput = document.getElementById('batch-queue-cron-expr'); + const isCron = scheduleModeSelect && scheduleModeSelect.value === 'cron'; + if (cronGroup) { + cronGroup.style.display = isCron ? 'block' : 'none'; + } + if (cronExprInput) { + if (isCron) { + cronExprInput.setAttribute('required', 'required'); + } else { + cronExprInput.removeAttribute('required'); + cronExprInput.value = ''; + } + } +} + // 更新新建任务统计 function updateBatchImportStats(text) { const statsEl = document.getElementById('batch-import-stats'); @@ -807,6 +892,9 @@ async function createBatchQueue() { const input = document.getElementById('batch-tasks-input'); const titleInput = document.getElementById('batch-queue-title'); const roleSelect = document.getElementById('batch-queue-role'); + const agentModeSelect = document.getElementById('batch-queue-agent-mode'); + const scheduleModeSelect = document.getElementById('batch-queue-schedule-mode'); + const cronExprInput = document.getElementById('batch-queue-cron-expr'); if (!input) return; const text = input.value.trim(); @@ -827,6 +915,13 @@ async function createBatchQueue() { // 获取角色(可选,空字符串表示默认角色) const role = roleSelect ? roleSelect.value || '' : ''; + const agentMode = agentModeSelect ? (agentModeSelect.value === 'multi' ? 'multi' : 'single') : 'single'; + const scheduleMode = scheduleModeSelect ? (scheduleModeSelect.value === 'cron' ? 'cron' : 'manual') : 'manual'; + const cronExpr = cronExprInput ? cronExprInput.value.trim() : ''; + if (scheduleMode === 'cron' && !cronExpr) { + alert(_t('batchImportModal.cronExprRequired')); + return; + } try { const response = await apiFetch('/api/batch-tasks', { @@ -834,7 +929,7 @@ async function createBatchQueue() { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ title, tasks, role }), + body: JSON.stringify({ title, tasks, role, agentMode, scheduleMode, cronExpr }), }); if (!response.ok) { @@ -978,15 +1073,7 @@ function renderBatchQueues() { } list.innerHTML = queues.map(queue => { - const statusMap = { - 'pending': { text: _t('tasks.statusPending'), class: 'batch-queue-status-pending' }, - 'running': { text: _t('tasks.statusRunning'), class: 'batch-queue-status-running' }, - 'paused': { text: _t('tasks.statusPaused'), class: 'batch-queue-status-paused' }, - 'completed': { text: _t('tasks.statusCompleted'), class: 'batch-queue-status-completed' }, - 'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-queue-status-cancelled' } - }; - - const status = statusMap[queue.status] || { text: queue.status, class: 'batch-queue-status-unknown' }; + const pres = getBatchQueueStatusPresentation(queue); // 统计任务状态 const stats = { @@ -1010,44 +1097,59 @@ function renderBatchQueues() { // 允许删除待执行、已完成或已取消状态的队列 const canDelete = queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled'; - const titleDisplay = queue.title ? `${escapeHtml(queue.title)}` : ''; - - // 显示角色信息(使用正确的角色图标) const loadedRoles = batchQueuesState.loadedRoles || []; const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles); const roleName = queue.role && queue.role !== '' ? queue.role : _t('batchQueueDetailModal.defaultRole'); - const roleDisplay = `${roleIcon} ${escapeHtml(roleName)}`; - + const isCronCycleIdle = queue.scheduleMode === 'cron' && queue.scheduleEnabled !== false && queue.status === 'completed'; + const cardMod = isCronCycleIdle ? ' batch-queue-item--cron-wait' : ''; + const progressFillMod = isCronCycleIdle ? ' batch-queue-progress-fill--cron-wait' : ''; + + const agentLabel = queue.agentMode === 'multi' ? _t('batchImportModal.agentModeMulti') : _t('batchImportModal.agentModeSingle'); + let scheduleLabel = queue.scheduleMode === 'cron' ? _t('batchImportModal.scheduleModeCron') : _t('batchImportModal.scheduleModeManual'); + if (queue.scheduleMode === 'cron' && queue.cronExpr) { + scheduleLabel += ` (${queue.cronExpr})`; + } + const configLine = [roleName, agentLabel, scheduleLabel].map(s => escapeHtml(s)).join(' · '); + const cronPausedNote = queue.scheduleMode === 'cron' && queue.scheduleEnabled === false + ? ` (${escapeHtml(_t('batchQueueDetailModal.cronSchedulePausedBadge'))})` + : ''; + const shortId = queue.id.length > 14 ? escapeHtml(queue.id.slice(0, 12)) + '\u2026' : escapeHtml(queue.id); + const titleBlock = queue.title + ? `