Add files via upload

This commit is contained in:
公明
2026-04-13 23:11:02 +08:00
committed by GitHub
parent d4f2b0f93d
commit 0a5bb1eab4
6 changed files with 1109 additions and 115 deletions
+799 -10
View File
@@ -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;
+31 -1
View File
@@ -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",
+31 -1
View File
@@ -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": "编辑任务",
+1 -3
View File
@@ -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: {
+226 -92
View File
@@ -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 ? `<span class="batch-queue-title" style="font-weight: 600; color: var(--text-primary); margin-right: 8px;">${escapeHtml(queue.title)}</span>` : '';
// 显示角色信息(使用正确的角色图标)
const loadedRoles = batchQueuesState.loadedRoles || [];
const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles);
const roleName = queue.role && queue.role !== '' ? queue.role : _t('batchQueueDetailModal.defaultRole');
const roleDisplay = `<span class="batch-queue-role" style="margin-right: 8px;" title="${_t('batchQueueDetailModal.role')}: ${escapeHtml(roleName)}">${roleIcon} ${escapeHtml(roleName)}</span>`;
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
? ` <span class="batch-queue-inline-warn" title="${escapeHtml(_t('batchQueueDetailModal.scheduleCronAutoHint'))}">(${escapeHtml(_t('batchQueueDetailModal.cronSchedulePausedBadge'))})</span>`
: '';
const shortId = queue.id.length > 14 ? escapeHtml(queue.id.slice(0, 12)) + '\u2026' : escapeHtml(queue.id);
const titleBlock = queue.title
? `<h4 class="batch-queue-card-title">${escapeHtml(queue.title)}</h4>`
: `<h4 class="batch-queue-card-title batch-queue-card-title--muted">${escapeHtml(_t('tasks.batchQueueUntitled'))}</h4>`;
const doneCount = stats.completed + stats.failed + stats.cancelled;
const statsCompact = `<span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.totalLabel'))}\u00a0${stats.total}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.pendingLabel'))}\u00a0${stats.pending}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.runningLabel'))}\u00a0${stats.running}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item batch-queue-statsline__item--ok">${escapeHtml(_t('tasks.completedLabel'))}\u00a0${stats.completed}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item batch-queue-statsline__item--err">${escapeHtml(_t('tasks.failedLabel'))}\u00a0${stats.failed}</span>${stats.cancelled > 0 ? `<span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.cancelledLabel'))}\u00a0${stats.cancelled}</span>` : ''}`;
return `
<div class="batch-queue-item" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
<div class="batch-queue-header">
<div class="batch-queue-info" style="flex: 1;">
${titleDisplay}
${roleDisplay}
<span class="batch-queue-status ${status.class}">${status.text}</span>
<span class="batch-queue-id">${_t('tasks.queueIdLabel')}: ${escapeHtml(queue.id)}</span>
<span class="batch-queue-time">${_t('tasks.createdTimeLabel')}: ${new Date(queue.createdAt).toLocaleString()}</span>
</div>
<div class="batch-queue-progress">
<div class="batch-queue-progress-bar">
<div class="batch-queue-progress-fill" style="width: ${progress}%"></div>
<div class="batch-queue-item batch-queue-item--compact${cardMod}" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
<div class="batch-queue-item__inner">
<div class="batch-queue-item__top">
<div class="batch-queue-item__title-col">
${titleBlock}
<p class="batch-queue-item__config">${configLine}${cronPausedNote}</p>
<p class="batch-queue-item__idline"><code title="${escapeHtml(queue.id)}">${shortId}</code><span class="batch-queue-item__idsep">\u00b7</span><span>${escapeHtml(_t('tasks.createdTimeLabel'))}\u00a0${escapeHtml(new Date(queue.createdAt).toLocaleString())}</span></p>
</div>
<div class="batch-queue-item__top-actions" onclick="event.stopPropagation();">
${canDelete ? `<button type="button" class="batch-queue-icon-btn" onclick="deleteBatchQueueFromList('${queue.id}')" title="${escapeHtml(_t('tasks.deleteQueue'))}" aria-label="${escapeHtml(_t('tasks.deleteQueue'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>` : ''}
</div>
<span class="batch-queue-progress-text">${progress}% (${stats.completed + stats.failed + stats.cancelled}/${stats.total})</span>
</div>
<div class="batch-queue-actions" style="display: flex; align-items: center; gap: 8px; margin-left: 12px;" onclick="event.stopPropagation();">
${canDelete ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteBatchQueueFromList('${queue.id}')" title="${_t('tasks.deleteQueue')}">${_t('common.delete')}</button>` : ''}
<div class="batch-queue-item__mid">
<div class="batch-queue-item__mid-left">
<span class="batch-queue-status ${pres.class}">${escapeHtml(pres.text)}</span>
${pres.sublabel ? `<span class="batch-queue-item__sublabel">${escapeHtml(pres.sublabel)}</span>` : ''}
</div>
<div class="batch-queue-item__mid-right">
<div class="batch-queue-progress-bar batch-queue-progress-bar--card batch-queue-progress-bar--list">
<div class="batch-queue-progress-fill${progressFillMod}" style="width: ${progress}%"></div>
</div>
<span class="batch-queue-item__pct">${progress}%\u00a0<span class="batch-queue-item__pct-frac">(${doneCount}/${stats.total})</span></span>
</div>
</div>
</div>
<div class="batch-queue-stats">
<span>${_t('tasks.totalLabel')}: ${stats.total}</span>
<span>${_t('tasks.pendingLabel')}: ${stats.pending}</span>
<span>${_t('tasks.runningLabel')}: ${stats.running}</span>
<span style="color: var(--success-color);">${_t('tasks.completedLabel')}: ${stats.completed}</span>
<span style="color: var(--error-color);">${_t('tasks.failedLabel')}: ${stats.failed}</span>
${stats.cancelled > 0 ? `<span style="color: var(--text-secondary);">${_t('tasks.cancelledLabel')}: ${stats.cancelled}</span>` : ''}
<div class="batch-queue-statsline" aria-label="${escapeHtml(_t('tasks.batchQueueTitle'))}">${statsCompact}</div>
</div>
</div>
`;
}).join('');
// 渲染分页控件
@@ -1198,7 +1300,8 @@ async function showBatchQueueDetail(queueId) {
const result = await response.json();
const queue = result.queue;
batchQueuesState.currentQueueId = queueId;
const pres = getBatchQueueStatusPresentation(queue);
if (title) {
// textContent 本身会做转义;这里不要再 escapeHtml,否则会把 && 显示成 &amp;...(看起来像“变形/乱码”)
title.textContent = queue.title ? _t('tasks.batchQueueTitle') + ' - ' + String(queue.title) : _t('tasks.batchQueueTitle');
@@ -1227,15 +1330,6 @@ async function showBatchQueueDetail(queueId) {
deleteBtn.style.display = (queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled' || queue.status === 'paused') ? 'inline-block' : 'none';
}
// 队列状态映射
const queueStatusMap = {
'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 taskStatusMap = {
'pending': { text: _t('tasks.statusPending'), class: 'batch-task-status-pending' },
@@ -1245,13 +1339,10 @@ async function showBatchQueueDetail(queueId) {
'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-task-status-cancelled' }
};
// 获取角色信息(如果队列有角色配置)
let roleDisplay = '';
let roleLineVal = '';
if (queue.role && queue.role !== '') {
// 如果有角色配置,尝试获取角色详细信息
let roleName = queue.role;
let roleIcon = '👤';
// 从已加载的角色列表中查找角色图标
let roleIcon = '\uD83D\uDC64';
if (Array.isArray(loadedRoles) && loadedRoles.length > 0) {
const role = loadedRoles.find(r => r.name === roleName);
if (role && role.icon) {
@@ -1262,23 +1353,21 @@ async function showBatchQueueDetail(queueId) {
const codePoint = parseInt(unicodeMatch[1], 16);
icon = String.fromCodePoint(codePoint);
} catch (e) {
// 转换失败,使用默认图标
// ignore
}
}
roleIcon = icon;
}
}
roleDisplay = `<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.role') + `</span>
<span class="detail-value">${roleIcon} ${escapeHtml(roleName)}</span>
</div>`;
roleLineVal = roleIcon + ' ' + escapeHtml(roleName);
} else {
// 默认角色
roleDisplay = `<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.role') + `</span>
<span class="detail-value">🔵 ` + _t('batchQueueDetailModal.defaultRole') + `</span>
</div>`;
roleLineVal = '\uD83D\uDD35 ' + escapeHtml(_t('batchQueueDetailModal.defaultRole'));
}
const agentModeText = queue.agentMode === 'multi' ? _t('batchImportModal.agentModeMulti') : _t('batchImportModal.agentModeSingle');
const scheduleModeText = queue.scheduleMode === 'cron' ? _t('batchImportModal.scheduleModeCron') : _t('batchImportModal.scheduleModeManual');
const scheduleDetail = escapeHtml(scheduleModeText) + (queue.scheduleMode === 'cron' && queue.cronExpr ? `${escapeHtml(queue.cronExpr)}` : '');
const showProgressNoteInModal = !!(pres.progressNote && !pres.callout);
// 保存滚动位置,防止刷新时滚动条弹回顶部
const modalBody = content.closest('.modal-body');
@@ -1287,36 +1376,34 @@ async function showBatchQueueDetail(queueId) {
const savedTasksListScrollTop = tasksList ? tasksList.scrollTop : 0;
content.innerHTML = `
<div class="batch-queue-detail-info">
${queue.title ? `<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.queueTitle') + `</span>
<span class="detail-value">${escapeHtml(queue.title)}</span>
</div>` : ''}
${roleDisplay}
<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.queueId') + `</span>
<span class="detail-value"><code>${escapeHtml(queue.id)}</code></span>
</div>
<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.status') + `</span>
<span class="detail-value"><span class="batch-queue-status ${queueStatusMap[queue.status]?.class || ''}">${queueStatusMap[queue.status]?.text || queue.status}</span></span>
</div>
<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.createdAt') + `</span>
<span class="detail-value">${new Date(queue.createdAt).toLocaleString()}</span>
</div>
${queue.startedAt ? `<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.startedAt') + `</span>
<span class="detail-value">${new Date(queue.startedAt).toLocaleString()}</span>
</div>` : ''}
${queue.completedAt ? `<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.completedAt') + `</span>
<span class="detail-value">${new Date(queue.completedAt).toLocaleString()}</span>
</div>` : ''}
<div class="detail-item">
<span class="detail-label">` + _t('batchQueueDetailModal.taskTotal') + `</span>
<span class="detail-value">${queue.tasks.length}</span>
<div class="batch-queue-detail-layout">
<section class="batch-queue-detail-hero">
<span class="batch-queue-status ${pres.class}">${escapeHtml(pres.text)}</span>
${pres.sublabel ? `<p class="batch-queue-detail-hero__sub">${escapeHtml(pres.sublabel)}</p>` : ''}
${showProgressNoteInModal ? `<p class="batch-queue-detail-hero__note">${escapeHtml(pres.progressNote)}</p>` : ''}
</section>
<section class="batch-queue-detail-kv">
${queue.title ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.queueTitle'))}</span><span class="bq-kv__v">${escapeHtml(queue.title)}</span></div>` : ''}
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.role'))}</span><span class="bq-kv__v">${roleLineVal}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchImportModal.agentMode'))}</span><span class="bq-kv__v">${escapeHtml(agentModeText)}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchImportModal.scheduleMode'))}</span><span class="bq-kv__v">${scheduleDetail}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.taskTotal'))}</span><span class="bq-kv__v">${queue.tasks.length}</span></div>
${queue.scheduleMode === 'cron' ? `<div class="bq-kv bq-kv--block"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.scheduleCronAuto'))}</span><span class="bq-kv__v bq-kv__v--control"><label class="bq-cron-toggle"><input type="checkbox" ${queue.scheduleEnabled !== false ? 'checked' : ''} onchange="updateBatchQueueScheduleEnabled(this.checked)" /><span class="bq-cron-toggle__hint">${escapeHtml(_t('batchQueueDetailModal.scheduleCronAutoHint'))}</span></label></span></div>` : ''}
</section>
${queue.lastScheduleError ? `<div class="bq-alert bq-alert--err"><strong>${escapeHtml(_t('batchQueueDetailModal.lastScheduleError'))}</strong><p>${escapeHtml(queue.lastScheduleError)}</p></div>` : ''}
${queue.lastRunError ? `<div class="bq-alert bq-alert--err"><strong>${escapeHtml(_t('batchQueueDetailModal.lastRunError'))}</strong><p>${escapeHtml(queue.lastRunError)}</p></div>` : ''}
${pres.callout ? `<div class="batch-queue-cron-callout batch-queue-cron-callout--compact"><span class="batch-queue-cron-callout-icon" aria-hidden="true">\u21BB</span><p>${escapeHtml(pres.callout)}</p></div>` : ''}
<details class="batch-queue-detail-tech">
<summary class="batch-queue-detail-tech__sum">${escapeHtml(_t('batchQueueDetailModal.technicalDetails'))}</summary>
<div class="batch-queue-detail-tech__body">
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.queueId'))}</span><span class="bq-kv__v"><code>${escapeHtml(queue.id)}</code></span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.createdAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.createdAt).toLocaleString())}</span></div>
${queue.startedAt ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.startedAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.startedAt).toLocaleString())}</span></div>` : ''}
${queue.completedAt ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.completedAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.completedAt).toLocaleString())}</span></div>` : ''}
${queue.scheduleMode === 'cron' && queue.nextRunAt && !pres.sublabel ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.nextRunAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.nextRunAt).toLocaleString())}</span></div>` : ''}
${queue.lastScheduleTriggerAt ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.lastScheduleTriggerAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.lastScheduleTriggerAt).toLocaleString())}</span></div>` : ''}
</div>
</details>
</div>
<div class="batch-queue-tasks-list">
<h4>` + _t('batchQueueDetailModal.taskList') + `</h4>
@@ -1834,6 +1921,28 @@ async function deleteBatchTask(queueId, taskId) {
}
}
async function updateBatchQueueScheduleEnabled(enabled) {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) return;
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}/schedule-enabled`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scheduleEnabled: enabled }),
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('batchQueueDetailModal.scheduleToggleFailed'));
}
showBatchQueueDetail(queueId);
refreshBatchQueues();
} catch (e) {
console.error(e);
alert(_t('batchQueueDetailModal.scheduleToggleFailed') + ': ' + e.message);
showBatchQueueDetail(queueId);
}
}
// 导出函数
window.showBatchImportModal = showBatchImportModal;
window.closeBatchImportModal = closeBatchImportModal;
@@ -1857,3 +1966,28 @@ window.closeAddBatchTaskModal = closeAddBatchTaskModal;
window.saveAddBatchTask = saveAddBatchTask;
window.deleteBatchTaskFromElement = deleteBatchTaskFromElement;
window.deleteBatchQueueFromList = deleteBatchQueueFromList;
window.handleBatchScheduleModeChange = handleBatchScheduleModeChange;
window.updateBatchQueueScheduleEnabled = updateBatchQueueScheduleEnabled;
// 语言切换后,列表/分页/详情弹窗由 JS 渲染的文案需用当前语言重绘(applyTranslations 不会处理 innerHTML 内容)
document.addEventListener('languagechange', function () {
try {
const tasksPage = document.getElementById('page-tasks');
if (!tasksPage || !tasksPage.classList.contains('active')) {
return;
}
if (document.getElementById('batch-queues-list')) {
renderBatchQueues();
}
const detailModal = document.getElementById('batch-queue-detail-modal');
if (
detailModal &&
detailModal.style.display === 'block' &&
batchQueuesState.currentQueueId
) {
showBatchQueueDetail(batchQueuesState.currentQueueId);
}
} catch (e) {
console.warn('languagechange tasks refresh failed', e);
}
});
+21 -8
View File
@@ -1430,14 +1430,6 @@
</label>
<small class="form-hint" data-i18n="settingsBasic.multiAgentRobotUseHint">需同时勾选「启用多代理」;调用量与成本更高。</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="multi-agent-batch-use" class="modern-checkbox" />
<span class="checkbox-custom"></span>
<span class="checkbox-text" data-i18n="settingsBasic.multiAgentBatchUse">批量任务队列也使用多代理</span>
</label>
<small class="form-hint" data-i18n="settingsBasic.multiAgentBatchUseHint">开启后,任务管理中按队列执行的每个子任务将走 Eino DeepAgent(需启用多代理)。</small>
</div>
</div>
</div>
@@ -2322,6 +2314,27 @@ version: 1.0.0<br>
</select>
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.roleHint">选择一个角色,所有任务将使用该角色的配置(提示词和工具)执行。</div>
</div>
<div class="form-group">
<label for="batch-queue-agent-mode" data-i18n="batchImportModal.agentMode">代理模式</label>
<select id="batch-queue-agent-mode" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
<option value="single" data-i18n="batchImportModal.agentModeSingle">单代理(ReAct</option>
<option value="multi" data-i18n="batchImportModal.agentModeMulti">多代理(Eino</option>
</select>
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.agentModeHint">建议默认单代理;复杂任务可使用多代理(需系统已启用多代理)。</div>
</div>
<div class="form-group">
<label for="batch-queue-schedule-mode" data-i18n="batchImportModal.scheduleMode">调度方式</label>
<select id="batch-queue-schedule-mode" onchange="handleBatchScheduleModeChange()" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
<option value="manual" data-i18n="batchImportModal.scheduleModeManual">手工执行</option>
<option value="cron" data-i18n="batchImportModal.scheduleModeCron">调度表达式(Cron</option>
</select>
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.scheduleModeHint">手工执行用于一次性任务;Cron 用于周期任务,建议先手工验证任务正确性。</div>
</div>
<div class="form-group" id="batch-queue-cron-group" style="display: none;">
<label for="batch-queue-cron-expr"><span data-i18n="batchImportModal.cronExpr">Cron 表达式</span><span style="color: red;">*</span></label>
<input type="text" id="batch-queue-cron-expr" data-i18n="batchImportModal.cronExprPlaceholder" data-i18n-attr="placeholder" placeholder="例如:0 */2 * * *(每2小时执行一次)" />
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.cronExprHint">采用标准 5 段 Cron:分 时 日 月 周,例如 `0 2 * * *` 表示每天 02:00 执行。</div>
</div>
<div class="form-group">
<label for="batch-tasks-input"><span data-i18n="batchImportModal.tasksList">任务列表(每行一个任务)</span><span style="color: red;">*</span></label>
<textarea id="batch-tasks-input" rows="15" data-i18n="batchImportModal.tasksListPlaceholderExample" data-i18n-attr="placeholder" placeholder="请输入任务列表,每行一个任务,例如:&#10;扫描 192.168.1.1 的开放端口&#10;检查 https://example.com 是否存在SQL注入&#10;枚举 example.com 的子域名" style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea>