diff --git a/docs/bug-sheet-review.md b/docs/bug-sheet-review.md index 98b6395d..06d5d08b 100644 --- a/docs/bug-sheet-review.md +++ b/docs/bug-sheet-review.md @@ -108,3 +108,8 @@ - Excel 第 47 行:截图显示 NSE 将 `-sT` 当作脚本名,原因是 scan_type 根据“最后一个非标志参数”找插入位置,遇到 additional_args 的 --script 值时插入到了标志与值之间。nmap 的 scan_type 改为置于参数列表开头,保留各 flag/value 相邻关系;其他工具的既有插入规则保留。 - `-sC` 与 --script 不是互斥错误;官方说明明确有 --script 时忽略 -sC,故不新增错误拒绝规则。来源:https://nmap.org/book/man-nse.html 。 - 读取真实 tools/nmap.yaml 验证截图参数、nse_scripts 字段和尾部 --max-retries 值的完整顺序;既有空可选参数测试通过。测试不向目标发送扫描;本机没有 nmap,因此不声称完成实际扫描验证。 + +## 追加:批量队列结果标签 + +- Excel 第 58 行:截图的子任务为 failed,队列 completed 表示所有子任务已经结束;绿色“已完成”和 100% 被误认为全部成功。列表和详情共用展示函数,现在全部失败显示失败色及“执行失败”,部分失败显示“已结束(N 项失败)”,详情解释进度统计结束任务,包含失败/取消。后端生命周期和重跑规则不变。 +- 3 项实际展示函数回归覆盖全失败、混合结果、成功、运行中及已有周期队列的后续运行信息;不扩展调度能力。 diff --git a/docs/bug-sheet-status.md b/docs/bug-sheet-status.md index d75ee7fe..91561279 100644 --- a/docs/bug-sheet-status.md +++ b/docs/bug-sheet-status.md @@ -60,7 +60,7 @@ | 55 | — | - 选择其专用测试会话,打开「终端」。 | 已修复,Unix Shell 回归通过 | | 56 | — | AI看代码给提的问题 | 已修复并有针对性验证 | | 57 | — | 审计日志搜索无结果后,点击“清空”不会清除关键词或恢复列表,无反应 | 当前重置会清空关键词并请求首个无筛选页面,回归未复现 | -| 58 | — | 任务失败,也显示已完成 | 待核对 | +| 58 | — | 任务失败,也显示已完成 | 已修复队列结果标签:全部失败显示失败,混合结果显示失败数;3 项回归通过 | | 59 | — | 批量任务在“用户并发配额已满”时进入等待调度后,即使后来停止容器、腾出配额,队列仍停在“执行中 | 当前仓库未实现,不扩展能力 | | 60 | — | 资产扫描任务在后续工具调用的 HITL 审批阶段已被系统中断,审批卡片明确显示“任务已中断、审批 | 待核对 | | 61 | — | 问题:事实正文里的 discovered_on: target/xxx 只会当普通文本保存,不会 | 已按建议明确关系字段说明 | diff --git a/web/static/css/style.css b/web/static/css/style.css index 775cc77d..ad1c5f15 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -46688,3 +46688,9 @@ html[data-theme="dark"] { --tool-blocked-color: #f4bc62; } font-size: .75rem; line-height: 1.6; } + +.batch-queue-status-failed { + color: var(--error-color, #dc2626); + background: rgba(220, 38, 38, 0.08); + border-color: rgba(220, 38, 38, 0.25); +} diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index a3e8dc42..f83913fb 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1099,6 +1099,8 @@ "toolFailed": "Failed" }, "tasks": { + "statusEndedWithFailures": "Finished ({{count}} failed)", + "finishedProgressHint": "Progress counts finished tasks, including failures and cancellations; it is not the success rate.", "title": "Task Management", "stopTask": "Stop task", "interruptModalTitle": "Interrupt current step", diff --git a/web/static/i18n/ru-RU.json b/web/static/i18n/ru-RU.json index 7b9d82a9..5f438a30 100644 --- a/web/static/i18n/ru-RU.json +++ b/web/static/i18n/ru-RU.json @@ -1111,6 +1111,8 @@ "toolFailed": "Ошибка" }, "tasks": { + "statusEndedWithFailures": "Завершено (ошибок: {{count}})", + "finishedProgressHint": "Прогресс учитывает завершённые задачи, включая ошибки и отмены, а не долю успешных задач.", "title": "Управление задачами", "stopTask": "Остановить задачу", "interruptModalTitle": "Прервать текущий шаг", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index fc334b0e..56e588d3 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1087,6 +1087,8 @@ "toolFailed": "失败" }, "tasks": { + "statusEndedWithFailures": "已结束({{count}} 项失败)", + "finishedProgressHint": "进度表示已结束的任务比例,包含失败和取消,不表示成功率。", "title": "任务管理", "stopTask": "停止任务", "interruptModalTitle": "中断当前步骤", diff --git a/web/static/js/batch-outcome-display.test.cjs b/web/static/js/batch-outcome-display.test.cjs new file mode 100644 index 00000000..d1f190dc --- /dev/null +++ b/web/static/js/batch-outcome-display.test.cjs @@ -0,0 +1,18 @@ +const fs=require('node:fs'),vm=require('node:vm'),test=require('node:test'),assert=require('node:assert/strict'); +const source=fs.readFileSync('web/static/js/tasks.js','utf8'); +const start=source.indexOf('function getBatchQueueStatusPresentation('),end=source.indexOf('\nfunction ',start+10); +const c=vm.createContext({_t:key=>key,_tPlain:(key,args)=>key+':'+JSON.stringify(args)});vm.runInContext(source.slice(start,end),c); +test('all failed batch tasks show failure even when the queue lifecycle is completed',()=>{ + const q={status:'completed',tasks:[{status:'failed'},{status:'failed'}]};const p=c.getBatchQueueStatusPresentation(q); + assert.equal(p.text,'tasks.statusFailed');assert.equal(p.class,'batch-queue-status-failed');assert.equal(q.status,'completed'); +}); +test('mixed outcome names the failure count while successful queues remain completed',()=>{ + const mixed=c.getBatchQueueStatusPresentation({status:'completed',tasks:[{status:'completed'},{status:'failed'}]}); + assert.match(mixed.text,/statusEndedWithFailures.*"count":1/);assert.equal(mixed.progressNote,'tasks.finishedProgressHint'); + assert.equal(c.getBatchQueueStatusPresentation({status:'completed',tasks:[{status:'completed'}]}).text,'tasks.statusCompleted'); +}); +test('running queues retain running status and existing recurring queues retain their next-run information',()=>{ + assert.equal(c.getBatchQueueStatusPresentation({status:'running',tasks:[{status:'failed'}]}).text,'tasks.statusRunning'); + const p=c.getBatchQueueStatusPresentation({status:'completed',scheduleMode:'cron',scheduleEnabled:true,nextRunAt:'2026-10-07T00:00:00Z',tasks:[{status:'failed'}]}); + assert.equal(p.text,'tasks.statusFailed');assert.match(p.sublabel,/cronNextRunLine/);assert.equal(p.callout,'tasks.cronRecurringCallout'); +}); diff --git a/web/static/js/tasks.js b/web/static/js/tasks.js index 3d545b48..84a7a60e 100644 --- a/web/static/js/tasks.js +++ b/web/static/js/tasks.js @@ -45,6 +45,18 @@ function getBatchQueueStatusPresentation(queue) { const nextStr = queue.nextRunAt ? new Date(queue.nextRunAt).toLocaleString() : ''; const empty = { sublabel: null, progressNote: null, callout: null }; + const failedCount = (queue.tasks || []).filter(task => task.status === 'failed').length; + if (queue.status === 'completed' && failedCount > 0) { + const allFailed = failedCount === (queue.tasks || []).length; + return { + text: allFailed ? _t('tasks.statusFailed') : _tPlain('tasks.statusEndedWithFailures', { count: failedCount }), + class: 'batch-queue-status-failed', + sublabel: cronOn && nextStr ? _tPlain('tasks.cronNextRunLine', { time: nextStr }) : null, + progressNote: _t('tasks.finishedProgressHint'), + callout: cronOn ? _t('tasks.cronRecurringCallout') : null + }; + } + if (cronOn && queue.status === 'completed') { return { text: _t('tasks.statusCronCycleIdle'),