From 68e3ead4d702a7983584f4aa0b1f195756cb81f0 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 Jul 2026 18:56:52 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 228 +++++++++++++++++++++++++++++++++ web/static/i18n/en-US.json | 10 ++ web/static/i18n/zh-CN.json | 10 ++ web/static/js/auth.js | 6 + web/static/js/settings.js | 3 + web/static/js/vulnerability.js | 75 +++++++++++ web/templates/index.html | 33 +++++ 7 files changed, 365 insertions(+) diff --git a/web/static/css/style.css b/web/static/css/style.css index 9bd69023..6967d430 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -490,6 +490,234 @@ html[data-theme="dark"] .main-sidebar { align-items: center; } +.vulnerability-alert-settings { + margin: 24px 0 4px; + border: 1px solid var(--border-color); + border-radius: 10px; + background: var(--bg-primary); + box-shadow: 0 3px 12px rgba(15, 23, 42, 0.035); +} + +.vulnerability-alert-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 17px 20px; + border-bottom: 1px solid var(--border-color); +} + +.vulnerability-alert-copy { + display: flex; + min-width: 0; + align-items: center; + gap: 12px; +} + +.vulnerability-alert-copy > div { + display: flex; + min-width: 0; + flex-direction: column; + gap: 2px; +} + +.vulnerability-alert-copy strong { + display: block; + color: var(--text-primary); + font-size: 0.9375rem; + font-weight: 650; + line-height: 1.4; +} + +.vulnerability-alert-copy > div > span { + color: var(--text-secondary); + font-size: 0.75rem; + line-height: 1.45; +} + +.vulnerability-alert-icon { + display: flex; + width: 34px; + height: 34px; + flex: 0 0 34px; + align-items: center; + justify-content: center; + border-radius: 9px; + background: rgba(0, 102, 255, 0.09); + color: var(--accent-color); +} + +.vulnerability-alert-controls { + display: flex; + min-height: 58px; + align-items: center; + gap: 22px; + padding: 10px 20px; + border-radius: 0 0 10px 10px; + background: var(--bg-secondary); +} + +.vulnerability-alert-binding-row { + display: flex; + min-width: 0; + flex: 1; + align-items: center; + flex-wrap: wrap; + gap: 7px 10px; + padding-left: 22px; + border-left: 1px solid var(--border-color); +} + +.vulnerability-alert-binding-row > span { + color: var(--text-secondary); + font-size: 0.75rem; + line-height: 1.45; +} + +.vulnerability-alert-binding-row > span.is-warning { + color: #b45309; +} + +.vulnerability-alert-bind-button { + padding: 2px 0; + border: 0; + background: transparent; + color: var(--accent-color); + font: inherit; + font-size: 0.75rem; + font-weight: 650; + line-height: 1.45; + cursor: pointer; +} + +.vulnerability-alert-bind-button:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.vulnerability-alert-field { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 10px; + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.vulnerability-alert-field .settings-custom-select { + width: 148px !important; + min-width: 148px !important; +} + +.vulnerability-alert-field .settings-custom-select-trigger { + min-height: 36px; + padding: 7px 11px; + border-radius: 7px; + font-size: 0.875rem; + font-weight: 600; +} + +.vulnerability-alert-field .settings-custom-select-menu { + min-width: 148px; +} + +.vulnerability-alert-switch { + position: relative; + display: inline-flex; + min-height: 36px; + align-items: center; + gap: 9px; + cursor: pointer; + color: var(--text-primary); + font-size: 0.875rem; + font-weight: 600; +} + +.vulnerability-alert-switch input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.vulnerability-alert-switch-track { + position: relative; + width: 38px; + height: 22px; + flex: 0 0 38px; + border-radius: 999px; + background: #cbd5e1; + transition: background-color 0.16s ease, box-shadow 0.16s ease; +} + +.vulnerability-alert-switch-track::after { + content: ''; + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.22); + transition: transform 0.16s ease; +} + +.vulnerability-alert-switch input:checked + .vulnerability-alert-switch-track { + background: var(--accent-color); +} + +.vulnerability-alert-switch input:checked + .vulnerability-alert-switch-track::after { + transform: translateX(16px); +} + +.vulnerability-alert-switch input:focus-visible + .vulnerability-alert-switch-track { + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.16); +} + +.vulnerability-alert-switch input:disabled ~ span { + cursor: not-allowed; + opacity: 0.55; +} + +@media (max-width: 760px) { + .vulnerability-alert-header { + align-items: flex-start; + } + + .vulnerability-alert-controls { + align-items: flex-start; + flex-direction: column; + gap: 12px; + padding: 14px 16px; + } + + .vulnerability-alert-binding-row { + width: 100%; + padding-top: 12px; + padding-left: 0; + border-top: 1px solid var(--border-color); + border-left: 0; + } +} + +@media (max-width: 520px) { + .vulnerability-alert-settings { + margin-top: 20px; + } + + .vulnerability-alert-header { + padding: 15px 16px; + } + + .vulnerability-alert-switch-label { + display: none; + } +} + .page-header-actions .btn-danger, .page-header-actions .btn-secondary, .page-header-actions .btn-primary { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 45108c9a..f154d39d 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2105,6 +2105,16 @@ "errorGeneric": "Something went wrong. Please try again." }, "vulnerabilityPage": { + "alertTitle": "Robot vulnerability alerts", + "alertDescription": "Push newly discovered vulnerabilities to robots bound to this account, filtered by severity.", + "alertHint": "Send newly discovered vulnerabilities at or above this severity to your bound robot accounts.", + "alertConfiguredNotBound": "{{platforms}} is enabled, but this Web account is not bound to a recipient identity.", + "alertNoBinding": "No proactive robot is both enabled and bound. Your settings are still saved.", + "alertBindAction": "Bind recipient account", + "alertMinimum": "Minimum severity", + "alertEnabled": "Enable alerts", + "alertSaved": "Vulnerability alert settings saved", + "alertSaveFailed": "Failed to save vulnerability alert settings", "statTotal": "Total", "statClickAll": "View all (clear severity filter)", "statClickFilter": "Click to filter by this severity; click again to clear", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index c6b4d444..5de2dcb2 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2093,6 +2093,16 @@ "errorGeneric": "操作失败,请稍后重试。" }, "vulnerabilityPage": { + "alertTitle": "机器人漏洞提醒", + "alertDescription": "按严重级别将新发现的漏洞推送至当前账号绑定的机器人。", + "alertHint": "发现符合级别的新漏洞后,通过已绑定机器人账号推送。", + "alertConfiguredNotBound": "{{platforms}}已启用,但当前 Web 账号尚未绑定对应的接收身份。", + "alertNoBinding": "尚未启用并绑定支持主动推送的机器人。设置仍会保存。", + "alertBindAction": "绑定接收账号", + "alertMinimum": "最低级别", + "alertEnabled": "启用提醒", + "alertSaved": "漏洞提醒设置已保存", + "alertSaveFailed": "保存漏洞提醒设置失败", "statTotal": "总漏洞数", "statClickAll": "查看全部(清除严重度筛选)", "statClickFilter": "点击按此严重度筛选;再次点击清除", diff --git a/web/static/js/auth.js b/web/static/js/auth.js index 05057b4c..67b43323 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -711,6 +711,9 @@ async function openRobotAccountBinding() { function closeRobotAccountBinding() { closeAppModal('robot-account-binding-modal'); + if (typeof window.loadVulnerabilityAlertSubscription === 'function') { + window.loadVulnerabilityAlertSubscription(); + } } async function generateRobotBindingCode() { @@ -859,6 +862,9 @@ async function loadRobotAccountBindings() { `).join(''); + if (typeof window.loadVulnerabilityAlertSubscription === 'function') { + window.loadVulnerabilityAlertSubscription(); + } } function formatRobotBindingTime(value) { diff --git a/web/static/js/settings.js b/web/static/js/settings.js index c81d60dc..78b36fcf 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -585,6 +585,9 @@ function switchSettingsSection(section) { activeContent.classList.add('active'); initSettingsCustomSelects(activeContent); } + if (section === 'robots' && typeof window.loadVulnerabilityAlertSubscription === 'function') { + window.loadVulnerabilityAlertSubscription(); + } if (section === 'terminal' && typeof initTerminal === 'function') { setTimeout(initTerminal, 0); } diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js index ca90dec2..5fd58453 100644 --- a/web/static/js/vulnerability.js +++ b/web/static/js/vulnerability.js @@ -1856,6 +1856,79 @@ function refreshVulnerabilities() { loadVulnerabilities(); } +async function loadVulnerabilityAlertSubscription() { + const enabledEl = document.getElementById('vulnerability-alert-enabled'); + const severityEl = document.getElementById('vulnerability-alert-min-severity'); + if (!enabledEl || !severityEl || typeof apiFetch === 'undefined') return; + try { + const response = await apiFetch('/api/vulnerability-alerts/subscription'); + if (!response.ok) throw new Error(await response.text()); + const data = await response.json(); + const sub = data.subscription || {}; + enabledEl.checked = !!sub.enabled; + severityEl.value = sub.min_severity || 'high'; + severityEl.disabled = false; + if (typeof window.refreshSettingsCustomSelects === 'function') { + window.refreshSettingsCustomSelects(); + } + const hint = document.getElementById('vulnerability-alert-binding-hint'); + const bindButton = document.getElementById('vulnerability-alert-bind-button'); + if (hint && !data.delivery_ready) { + const supportedTypes = [ + { type: 'wecom', label: vulnT('settings.robots.wecom.title') }, + { type: 'lark', label: vulnT('settings.robots.lark.title') }, + { type: 'telegram', label: 'Telegram' }, + { type: 'slack', label: 'Slack' }, + { type: 'discord', label: 'Discord' } + ]; + const enabledPlatforms = supportedTypes.filter(function (item) { + if (typeof getRobotStatus !== 'function') return false; + const status = getRobotStatus(item.type); + return status && status.state === 'enabled'; + }).map(function (item) { return item.label; }); + hint.textContent = enabledPlatforms.length + ? vulnT('vulnerabilityPage.alertConfiguredNotBound', { platforms: enabledPlatforms.join('、') }) + : vulnT('vulnerabilityPage.alertNoBinding'); + hint.classList.add('is-warning'); + if (bindButton) bindButton.hidden = false; + } else if (hint) { + hint.textContent = vulnT('vulnerabilityPage.alertHint'); + hint.classList.remove('is-warning'); + if (bindButton) bindButton.hidden = true; + } + } catch (error) { + console.warn('加载漏洞提醒设置失败', error); + } +} + +async function saveVulnerabilityAlertSubscription() { + const enabledEl = document.getElementById('vulnerability-alert-enabled'); + const severityEl = document.getElementById('vulnerability-alert-min-severity'); + if (!enabledEl || !severityEl) return; + enabledEl.disabled = true; + severityEl.disabled = true; + if (typeof window.refreshSettingsCustomSelects === 'function') { + window.refreshSettingsCustomSelects(); + } + try { + const response = await apiFetch('/api/vulnerability-alerts/subscription', { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: enabledEl.checked, min_severity: severityEl.value }) + }); + if (!response.ok) throw new Error(await response.text()); + if (typeof showNotification === 'function') showNotification(vulnT('vulnerabilityPage.alertSaved'), 'success'); + } catch (error) { + if (typeof showNotification === 'function') showNotification(vulnT('vulnerabilityPage.alertSaveFailed'), 'error'); + await loadVulnerabilityAlertSubscription(); + } finally { + enabledEl.disabled = false; + severityEl.disabled = false; + if (typeof window.refreshSettingsCustomSelects === 'function') { + window.refreshSettingsCustomSelects(); + } + } +} + // 切换漏洞详情展开/折叠 function toggleVulnerabilityDetails(id) { const content = document.getElementById(`content-${id}`); @@ -2415,3 +2488,5 @@ window.bindVulnerabilityProject = bindVulnerabilityProject; window.buildVulnerabilityProjectOptionsHtml = buildVulnerabilityProjectOptionsHtml; window.changeVulnerabilityStatus = changeVulnerabilityStatus; window.openVulnerabilityConversation = openVulnerabilityConversation; +window.loadVulnerabilityAlertSubscription = loadVulnerabilityAlertSubscription; +window.saveVulnerabilityAlertSubscription = saveVulnerabilityAlertSubscription; diff --git a/web/templates/index.html b/web/templates/index.html index 89a2514e..796d94f6 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -3616,6 +3616,39 @@ + +